import json import subprocess from pathlib import Path CONTROLLER = Path(__file__).parents[1] / "frontend" / "today-week-reschedule.js" INDEX = CONTROLLER.parent / "index.html" CSS = CONTROLLER.parent / "dashboard.css" DASHBOARD = CONTROLLER.parent / "dashboard.js" BUNDLE = CONTROLLER.parents[1] / "src" / "frontend_bundle.py" def run_controller(scenario: str) -> dict: harness = f""" const createReschedule = require({json.dumps(str(CONTROLLER))}); (async()=>{{ {scenario} }})().catch(error=>{{console.error(error);process.exit(1);}}); """ completed = subprocess.run( ["node", "-e", harness], check=True, capture_output=True, text=True ) return json.loads(completed.stdout) def test_reschedule_controller_reviews_load_and_atomically_confirms_selected_day(): result = run_controller(""" const requests=[]; const weekState={revision:7,offline_snapshot:false,days:[]}; const review={days:[ {plan_date:'2026-08-24',label:'Mon, Aug 24',ids:['one'],planned_minutes:30,capacity_minutes:60,overloaded:false}, {plan_date:'2026-08-25',label:'Tue, Aug 25',ids:[],planned_minutes:0,capacity_minutes:90,overloaded:false} ]}; const week={load:async()=>weekState,review:()=>review,adopt:value=>{weekState.revision=value.revision;weekState.days=value.days;}}; const api=async(url,options)=>{ if(url==='api/v1/today')return {revision:4,ids:['active','other'],estimates:{active:45}}; requests.push({url,body:JSON.parse(options.body)});return { today:{revision:5,ids:['other'],capacity_minutes:120,estimates:{other:20}}, week:{revision:8,timezone:'UTC',days:[{plan_date:'2026-08-25',ids:['active'],capacity_minutes:90,estimates:{active:45}}]} };}; let adoptedToday=null; const controller=createReschedule({week,api,getToday:()=>({revision:4,ids:['active','other'],estimates:{active:45}}), adoptToday:value=>{adoptedToday=value;},operationId:()=> 'move-active'}); const opened=await controller.open('active'); const confirmed=await controller.confirm('2026-08-25',45); console.log(JSON.stringify({opened,confirmed,requests,adoptedToday,weekState})); """) assert result["opened"]["estimate_minutes"] == 45 assert result["opened"]["days"][0]["load"] == "30 / 60 min" assert result["opened"]["days"][1]["eligible"] is True assert result["requests"] == [{ "url": "api/v1/week/reschedule", "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, }, }] assert result["adoptedToday"]["ids"] == ["other"] assert result["weekState"]["revision"] == 8 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; const days=[ {plan_date:'2026-08-24',label:'Mon',ids:['1','2','3','4','5'],planned_minutes:50,capacity_minutes:60}, {plan_date:'2026-08-25',label:'Tue',ids:['1'],planned_minutes:50,capacity_minutes:60} ]; const week={load:async()=>({revision:2,offline_snapshot:false}),review:()=>({days}),adopt:()=>{}}; const api=async url=>{if(url==='api/v1/today')return {revision:3,ids:['active'],estimates:{active:30}};calls++;}; const controller=createReschedule({week,api,getToday:()=>({revision:3,ids:['active'],estimates:{active:30}})}); 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,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["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"]["version"] == 2 assert result["record"]["records"][0]["body"]["operation_id"] == "move-active" assert result["record"]["records"][0]["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_queues_two_offline_moves_without_replacing_the_first(): 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)}; let today={revision:4,ids:['active','other'],capacity_minutes:120,estimates:{active:45,other:20}}; let weekState={revision:7,timezone:'UTC',offline_snapshot:true,days:[ {plan_date:'2026-08-25',ids:[],capacity_minutes:90,estimates:{}} ]}; const confirmedWeek=JSON.parse(JSON.stringify(weekState));let loads=0; const week={load:async()=>{loads++;weekState=JSON.parse(JSON.stringify(confirmedWeek));return weekState;},state:()=>weekState,review:()=>({days:[ {...weekState.days[0],label:'Tue',planned_minutes:Object.values(weekState.days[0].estimates).reduce((a,b)=>a+b,0)} ]}),adopt:value=>{weekState={...value,offline_snapshot:true};}}; const api=async()=>{const error=new Error('offline');error.status=0;throw error;}; let n=0; const controller=createReschedule({week,api,getToday:()=>today,adoptToday:value=>{today=value;}, storage,getLogin:()=> 'timmy',operationId:()=> `move-${++n}`,now:()=>100+n}); await controller.open('active'); await controller.confirm('2026-08-25',45); await controller.open('other'); const confirmed=await controller.confirm('2026-08-25',20); const stored=JSON.parse([...values.values()][0]); console.log(JSON.stringify({confirmed,stored,today,weekState,loads})); """) assert [entry["body"]["operation_id"] for entry in result["stored"]["records"]] == [ "move-1", "move-2", ] assert result["confirmed"]["pending_count"] == 2 assert result["today"]["ids"] == [] assert result["weekState"]["days"][0]["ids"] == ["active", "other"] assert result["loads"] == 1 def test_reschedule_controller_drains_fifo_and_rebases_each_next_move(): result = run_controller(""" const key='stackchain.today-week-reschedule.v1.timmy'; const make=(id,identity,today,week)=>({queued_at:1,body:{operation_id:id,identity,estimate_minutes:20, plan_date:'2026-08-25',today_revision:today,week_revision:week,allow_over_capacity:false}, today:{revision:today,ids:[]},week:{revision:week,days:[]}}); const values=new Map([[key,JSON.stringify({version:2,records:[make('move-1','one',4,7),make('move-2','two',4,7)]})]]); const storage={getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)}; const requests=[];let active=0,maxActive=0; const api=async(_url,options)=>{active++;maxActive=Math.max(maxActive,active); const body=JSON.parse(options.body);requests.push(body);await new Promise(resolve=>setTimeout(resolve,1));active--; const step=requests.length;return {today:{revision:4+step,ids:[]},week:{revision:7+step,days:[]}};}; const controller=createReschedule({week:{adopt:()=>{}},api,adoptToday:()=>{},storage,getLogin:()=> 'timmy'}); const delivered=await controller.flush(); console.log(JSON.stringify({requests,maxActive,delivered,remaining:values.size})); """) assert [request["operation_id"] for request in result["requests"]] == ["move-1", "move-2"] assert result["requests"][1]["today_revision"] == 5 assert result["requests"][1]["week_revision"] == 8 assert result["maxActive"] == 1 assert result["delivered"]["pending_count"] == 0 assert result["remaining"] == 0 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() dashboard = DASHBOARD.read_text() bundle = BUNDLE.read_text() assert 'data-work-session-reschedule-week' in index assert 'id="today-week-reschedule"' in index assert 'id="today-week-reschedule-days"' in index assert 'id="today-week-reschedule-estimate"' in index assert 'id="confirm-today-week-reschedule"' in index assert 'id="cancel-today-week-reschedule"' in index 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