Merge pull request 'feat: CI browser journey intermittently leaves week reschedule dialog open' (#1312) from timmy/1311-ci-browser-journey-intermittently-leaves-week-re into main
This commit is contained in:
commit
187b0decae
|
|
@ -70,11 +70,12 @@ function createTodayWeekReschedule({
|
|||
throw conflict;
|
||||
}
|
||||
|
||||
function flush() {
|
||||
function flush(retryConflict = false) {
|
||||
if (flushing) return flushing;
|
||||
if (!pending()) return Promise.resolve(false);
|
||||
flushing = (async () => {
|
||||
let result = null;
|
||||
let conflictRetried = false;
|
||||
while (true) {
|
||||
const records = readQueue();
|
||||
const record = records[0];
|
||||
|
|
@ -83,7 +84,25 @@ function createTodayWeekReschedule({
|
|||
result = await api('api/v1/week/reschedule', {
|
||||
method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(record.body),
|
||||
});
|
||||
} catch (error) { await restoreConflict(error); }
|
||||
} catch (error) {
|
||||
if (error?.status === 409 && retryConflict && !conflictRetried) {
|
||||
const [today, weekState] = await Promise.all([api('api/v1/today'), api('api/v1/week')]);
|
||||
adoptToday(today); week.adopt(weekState);
|
||||
const latest = readQueue();
|
||||
if (today?.ids?.includes(record.body.identity) &&
|
||||
latest[0]?.body?.operation_id === record.body.operation_id) {
|
||||
latest[0] = {...latest[0], body:{
|
||||
...latest[0].body,
|
||||
today_revision:today.revision,
|
||||
week_revision:weekState.revision,
|
||||
}};
|
||||
if (!saveQueue(latest)) throw new Error('Could not update this saved move after plans changed.');
|
||||
conflictRetried = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
await restoreConflict(error);
|
||||
}
|
||||
adoptToday(result.today);
|
||||
week.adopt(result.week);
|
||||
const latest = readQueue();
|
||||
|
|
@ -207,9 +226,24 @@ function createTodayWeekReschedule({
|
|||
}
|
||||
let result;
|
||||
try {
|
||||
result = admitted ? await flush() : await api('api/v1/week/reschedule', {
|
||||
method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(body),
|
||||
});
|
||||
if (admitted) {
|
||||
result = await flush(true);
|
||||
} else {
|
||||
try {
|
||||
result = await api('api/v1/week/reschedule', {
|
||||
method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(body),
|
||||
});
|
||||
} catch (error) {
|
||||
if (error?.status !== 409) throw error;
|
||||
const [today, weekState] = await Promise.all([api('api/v1/today'), api('api/v1/week')]);
|
||||
adoptToday(today); week.adopt(weekState);
|
||||
if (!today?.ids?.includes(body.identity)) throw error;
|
||||
body.today_revision = today.revision; body.week_revision = weekState.revision;
|
||||
result = await api('api/v1/week/reschedule', {
|
||||
method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(body),
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch(error) {
|
||||
if (admitted && error?.status === 409) await restoreConflict(error);
|
||||
if (admitted && error?.status !== 401 && error?.status !== 403) {
|
||||
|
|
|
|||
|
|
@ -64,6 +64,73 @@ console.log(JSON.stringify({opened,confirmed,requests,adoptedToday,weekState}));
|
|||
assert result["confirmed"]["today"]["revision"] == 5
|
||||
|
||||
|
||||
def test_reschedule_controller_retries_one_stale_revision_when_active_intent_is_unchanged():
|
||||
result = run_controller("""
|
||||
const requests=[];
|
||||
let attempts=0;
|
||||
const weekState={revision:7,offline_snapshot:false,timezone:'UTC',days:[
|
||||
{plan_date:'2026-08-25',ids:[],capacity_minutes:90,estimates:{}}
|
||||
]};
|
||||
const week={
|
||||
load:async()=>weekState,
|
||||
review:()=>({days:[{...weekState.days[0],label:'Tue',planned_minutes:0}]}),
|
||||
adopt:value=>Object.assign(weekState,value),
|
||||
};
|
||||
const api=async(url,options)=>{
|
||||
if(url==='api/v1/today')return {revision:5,ids:['active'],estimates:{active:45}};
|
||||
if(url==='api/v1/week')return {revision:8,offline_snapshot:false,timezone:'UTC',days:[
|
||||
{plan_date:'2026-08-25',ids:[],capacity_minutes:90,estimates:{}}
|
||||
]};
|
||||
const body=JSON.parse(options.body);requests.push(body);attempts++;
|
||||
if(attempts===1){const error=new Error('stale revision');error.status=409;throw error;}
|
||||
return {today:{revision:6,ids:[],estimates:{}},week:{revision:9,timezone:'UTC',days:[
|
||||
{plan_date:'2026-08-25',ids:['active'],capacity_minutes:90,estimates:{active:45}}
|
||||
]}};
|
||||
};
|
||||
const controller=createReschedule({week,api,getToday:()=>null,adoptToday:()=>{},operationId:()=> 'move-active'});
|
||||
await controller.open('active');
|
||||
let message='',confirmed=null;
|
||||
try{confirmed=await controller.confirm('2026-08-25',45);}catch(error){message=error.message;}
|
||||
console.log(JSON.stringify({message,confirmed,requests}));
|
||||
""")
|
||||
|
||||
assert result["message"] == ""
|
||||
assert result["confirmed"]["today"]["revision"] == 6
|
||||
assert [request["today_revision"] for request in result["requests"]] == [5, 5]
|
||||
assert [request["week_revision"] for request in result["requests"]] == [7, 8]
|
||||
assert result["requests"][0]["operation_id"] == result["requests"][1]["operation_id"]
|
||||
|
||||
|
||||
def test_reschedule_controller_rebases_admitted_move_once_after_stale_revision():
|
||||
result = run_controller("""
|
||||
const values=new Map();
|
||||
const storage={getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)};
|
||||
const requests=[];let attempts=0;
|
||||
const initialWeek={revision:7,offline_snapshot:false,timezone:'UTC',days:[
|
||||
{plan_date:'2026-08-25',ids:[],capacity_minutes:90,estimates:{}}
|
||||
]};
|
||||
const week={load:async()=>initialWeek,review:()=>({days:[{...initialWeek.days[0],label:'Tue',planned_minutes:0}]}),adopt:()=>{}};
|
||||
const api=async(url,options)=>{
|
||||
if(url==='api/v1/today')return {revision:5,ids:['active'],estimates:{active:45}};
|
||||
if(url==='api/v1/week')return {revision:8,offline_snapshot:false,timezone:'UTC',days:initialWeek.days};
|
||||
const body=JSON.parse(options.body);requests.push(body);attempts++;
|
||||
if(attempts===1){const error=new Error('stale revision');error.status=409;throw error;}
|
||||
return {today:{revision:6,ids:[],estimates:{}},week:{revision:9,timezone:'UTC',days:[]}};
|
||||
};
|
||||
const controller=createReschedule({week,api,adoptToday:()=>{},storage,getLogin:()=> 'timmy',operationId:()=> 'move-active'});
|
||||
await controller.open('active');
|
||||
let message='',confirmed=null;
|
||||
try{confirmed=await controller.confirm('2026-08-25',45);}catch(error){message=error.message;}
|
||||
console.log(JSON.stringify({message,confirmed,requests,remaining:values.size}));
|
||||
""")
|
||||
|
||||
assert result["message"] == ""
|
||||
assert result["confirmed"]["today"]["revision"] == 6
|
||||
assert [request["today_revision"] for request in result["requests"]] == [5, 5]
|
||||
assert [request["week_revision"] for request in result["requests"]] == [7, 8]
|
||||
assert result["remaining"] == 0
|
||||
|
||||
|
||||
def test_reschedule_controller_blocks_offline_full_and_overloaded_days_without_mutation():
|
||||
result = run_controller("""
|
||||
let calls=0;
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user