feat: keep Today-to-Week moves through offline reloads (Closes #1231)
This commit is contained in:
1 parent
edaeba4a4e
commit
e234d6b6c3
5 files changed
+298
-29
No files matched your search
@@ -70,6 +70,14 @@ def test_release_promotion_waits_for_packaged_mobile_journeys():
|
||||
"tests/e2e/test_mobile_today_summary_release.py "
|
||||
"tests/e2e/test_mobile_tomorrow_conflict_release.py "
|
||||
"tests/e2e/test_mobile_week_ahead_release.py "
|
||||
"tests/e2e/test_mobile_today_week_reschedule_release.py "
|
||||
"tests/e2e/test_mobile_wrap_up_handoff_release.py -q"
|
||||
) in browser
|
||||
assert "needs: [lint, build-release, browser-journey]" in release
|
||||
|
||||
|
||||
def test_browser_job_runs_packaged_today_week_reschedule_journey():
|
||||
text = WORKFLOW.read_text()
|
||||
browser = text[text.index(" browser-journey:") : text.index(" release-candidate:")]
|
||||
|
||||
assert "tests/e2e/test_mobile_today_week_reschedule_release.py" in browser
|
||||
@@ -78,18 +78,127 @@ const opened=await controller.open('active');
|
||||
let fullError='',overError='';
|
||||
try{await controller.confirm('2026-08-24',30);}catch(error){fullError=error.message;}
|
||||
try{await controller.confirm('2026-08-25',30);}catch(error){overError=error.message;}
|
||||
const offline=createReschedule({week:{load:async()=>({revision:2,offline_snapshot:true}),review:()=>({days})},api,getToday:()=>({revision:3,ids:['active'],estimates:{active:30}})});
|
||||
let offlineError='';try{await offline.open('active');}catch(error){offlineError=error.message;}
|
||||
console.log(JSON.stringify({opened,fullError,overError,offlineError,calls}));
|
||||
const offline=createReschedule({week:{load:async()=>({revision:2,offline_snapshot:true,days:[]}),review:()=>({days})},api,getToday:()=>({revision:3,ids:['active'],estimates:{active:30}})});
|
||||
let offlineOpened=null;try{offlineOpened=await offline.open('active');}catch(_error){}
|
||||
console.log(JSON.stringify({opened,fullError,overError,offlineOpened,calls}));
|
||||
""")
|
||||
|
||||
assert result["opened"]["days"][0]["eligible"] is False
|
||||
assert result["fullError"] == "That Week Ahead day already has five items."
|
||||
assert result["overError"] == "That move exceeds the day's capacity. Confirm overload before rescheduling."
|
||||
assert result["offlineError"] == "Reconnect before rescheduling Today into Week Ahead."
|
||||
assert result["offlineOpened"]["offline"] is True
|
||||
assert result["calls"] == 0
|
||||
|
||||
|
||||
def test_reschedule_controller_persists_offline_move_before_optimistic_adoption():
|
||||
result = run_controller("""
|
||||
const values=new Map();
|
||||
const events=[];
|
||||
const storage={
|
||||
getItem:key=>values.get(key)||null,
|
||||
setItem:(key,value)=>{events.push('persist');values.set(key,value);},
|
||||
removeItem:key=>values.delete(key),
|
||||
};
|
||||
const today={revision:4,ids:['active','other'],capacity_minutes:120,estimates:{active:45,other:20}};
|
||||
const weekState={revision:7,timezone:'UTC',offline_snapshot:true,days:[
|
||||
{plan_date:'2026-08-25',ids:[],capacity_minutes:90,estimates:{}}
|
||||
]};
|
||||
const week={load:async()=>weekState,review:()=>({days:[
|
||||
{plan_date:'2026-08-25',label:'Tue',ids:[],planned_minutes:0,capacity_minutes:90,estimates:{}}
|
||||
]}),adopt:value=>events.push(['week',value])};
|
||||
const api=async()=>{const error=new Error('offline');error.status=0;throw error;};
|
||||
const controller=createReschedule({week,api,getToday:()=>today,adoptToday:value=>events.push(['today',value]),
|
||||
storage,getLogin:()=> 'Timmy',operationId:()=> 'move-active',now:()=>123});
|
||||
const opened=await controller.open('active');
|
||||
const confirmed=await controller.confirm('2026-08-25',45);
|
||||
console.log(JSON.stringify({opened,confirmed,events,keys:[...values.keys()],record:JSON.parse([...values.values()][0])}));
|
||||
""")
|
||||
|
||||
assert result["opened"]["offline"] is True
|
||||
assert result["confirmed"]["sync_pending"] is True
|
||||
assert result["events"][0] == "persist"
|
||||
assert result["events"][1][0] == "today"
|
||||
assert result["events"][1][1]["ids"] == ["other"]
|
||||
assert result["events"][2][0] == "week"
|
||||
assert result["events"][2][1]["days"][0]["ids"] == ["active"]
|
||||
assert result["keys"] == ["stackchain.today-week-reschedule.v1.timmy"]
|
||||
assert result["record"]["body"]["operation_id"] == "move-active"
|
||||
assert result["record"]["queued_at"] == 123
|
||||
|
||||
|
||||
def test_reschedule_controller_restores_and_delivers_same_operation_after_reload():
|
||||
result = run_controller("""
|
||||
const key='stackchain.today-week-reschedule.v1.timmy';
|
||||
const record={queued_at:123,body:{operation_id:'move-active',identity:'active',estimate_minutes:45,
|
||||
plan_date:'2026-08-25',today_revision:4,week_revision:7,allow_over_capacity:false},
|
||||
today:{revision:4,ids:['other'],estimates:{other:20}},
|
||||
week:{revision:7,timezone:'UTC',days:[{plan_date:'2026-08-25',ids:['active'],capacity_minutes:90,estimates:{active:45}}]}};
|
||||
const values=new Map([[key,JSON.stringify(record)]]);const adopted=[];const requests=[];
|
||||
const storage={getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)};
|
||||
const api=async(url,options)=>{requests.push({url,body:JSON.parse(options.body)});return {
|
||||
today:{revision:5,ids:['other'],estimates:{other:20}},
|
||||
week:{revision:8,timezone:'UTC',days:[{plan_date:'2026-08-25',ids:['active'],capacity_minutes:90,estimates:{active:45}}]}};};
|
||||
const controller=createReschedule({week:{adopt:value=>adopted.push(['week',value])},api,
|
||||
getToday:()=>null,adoptToday:value=>adopted.push(['today',value]),storage,getLogin:()=> 'timmy'});
|
||||
const restored=controller.resume();
|
||||
const delivered=await controller.flush();
|
||||
console.log(JSON.stringify({restored,delivered,adopted,requests,remaining:values.size}));
|
||||
""")
|
||||
|
||||
assert result["restored"]["sync_pending"] is True
|
||||
assert result["adopted"][0][0] == "today"
|
||||
assert result["adopted"][1][0] == "week"
|
||||
assert result["requests"][0]["body"]["operation_id"] == "move-active"
|
||||
assert result["delivered"]["sync_pending"] is False
|
||||
assert result["remaining"] == 0
|
||||
|
||||
|
||||
def test_reschedule_controller_keeps_admitted_operation_when_connection_drops_on_confirm():
|
||||
result = run_controller("""
|
||||
const values=new Map();const events=[];
|
||||
const storage={getItem:key=>values.get(key)||null,setItem:(key,value)=>{events.push('persist');values.set(key,value);},removeItem:key=>values.delete(key)};
|
||||
const weekState={revision:7,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=>events.push(['week',value])};
|
||||
let calls=0;const api=async url=>{if(url==='api/v1/today')return {revision:4,ids:['active'],estimates:{active:45}};
|
||||
calls++;const error=new Error('connection dropped');error.status=0;throw error;};
|
||||
const controller=createReschedule({week,api,getToday:()=>null,adoptToday:value=>events.push(['today',value]),
|
||||
storage,getLogin:()=> 'timmy',operationId:()=> 'stable-operation'});
|
||||
await controller.open('active');
|
||||
const result=await controller.confirm('2026-08-25',45);
|
||||
console.log(JSON.stringify({result,events,calls,pending:controller.pending()}));
|
||||
""")
|
||||
|
||||
assert result["result"]["sync_pending"] is True
|
||||
assert result["events"][0] == "persist"
|
||||
assert result["calls"] == 1
|
||||
assert result["pending"]["body"]["operation_id"] == "stable-operation"
|
||||
|
||||
|
||||
def test_reschedule_controller_restores_server_truth_and_keeps_intent_on_conflict():
|
||||
result = run_controller("""
|
||||
const key='stackchain.today-week-reschedule.v1.timmy';
|
||||
const record={queued_at:123,body:{operation_id:'move-active'},today:{revision:4,ids:[]},week:{revision:7,days:[]}};
|
||||
const values=new Map([[key,JSON.stringify(record)]]);const adopted=[];
|
||||
const storage={getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)};
|
||||
const api=async url=>{
|
||||
if(url==='api/v1/week/reschedule'){const error=new Error('conflict');error.status=409;throw error;}
|
||||
if(url==='api/v1/today')return {revision:9,ids:['active'],estimates:{active:45}};
|
||||
return {revision:12,timezone:'UTC',days:[]};
|
||||
};
|
||||
const controller=createReschedule({week:{adopt:value=>adopted.push(['week',value])},api,
|
||||
adoptToday:value=>adopted.push(['today',value]),storage,getLogin:()=> 'timmy'});
|
||||
let message='';try{await controller.flush();}catch(error){message=error.message;}
|
||||
console.log(JSON.stringify({message,adopted,pending:controller.pending(),remaining:values.size}));
|
||||
""")
|
||||
|
||||
assert result["message"] == "Plans changed on another device. Review and retry this saved move."
|
||||
assert result["adopted"][0][1]["revision"] == 9
|
||||
assert result["adopted"][1][1]["revision"] == 12
|
||||
assert result["pending"]["body"]["operation_id"] == "move-active"
|
||||
assert result["remaining"] == 1
|
||||
|
||||
|
||||
def test_mobile_active_today_reschedule_dialog_is_touch_safe_and_wired_into_release_bundle():
|
||||
index = INDEX.read_text()
|
||||
css = CSS.read_text()
|
||||
@@ -105,5 +214,8 @@ def test_mobile_active_today_reschedule_dialog_is_touch_safe_and_wired_into_rele
|
||||
assert '.today-week-reschedule-days button { min-height:44px;' in css
|
||||
assert '.today-week-reschedule-panel { width:min(100%,560px);' in css
|
||||
assert 'x:currentTodayProgressTarget' in dashboard
|
||||
assert 'restoreWhenOwned()' in CONTROLLER.read_text()
|
||||
assert "runTodayTransition('next')" in dashboard
|
||||
assert 'reschedule:()=>({storage,getLogin})' in (CONTROLLER.parent / "week-plan.js").read_text()
|
||||
assert "addEventListener('online', flushPending)" in CONTROLLER.read_text()
|
||||
assert '"static/today-week-reschedule.js"' in bundle
|
||||
Reference in new issue
Block a user