import json import subprocess from pathlib import Path import pytest FRONTEND = Path(__file__).parents[1] / "frontend" CONTROLLER = FRONTEND / "week-plan.js" COORDINATOR = FRONTEND / "outbox-coordinator.js" INDEX = FRONTEND / "index.html" CSS = FRONTEND / "dashboard.css" def run_controller(scenario: str) -> dict: harness = f""" const createWeekPlan = require({json.dumps(str(CONTROLLER))}); const createOutboxCoordinator = require({json.dumps(str(COORDINATOR))}); (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_weekly_availability_editor_is_mobile_safe_and_wired_to_durable_defaults(): markup = INDEX.read_text() styles = CSS.read_text() source = CONTROLLER.read_text() assert 'id="open-week-availability"' in markup assert 'id="week-availability-editor"' in markup assert markup.count('data-weekday-capacity=') == 7 assert 'id="save-week-availability"' in markup assert 'id="disable-week-availability"' in markup assert 'aria-labelledby="week-availability-title"' in markup assert ".week-availability-grid input { min-height:44px" in styles assert "grid-template-columns:1fr" in styles assert "stageAvailabilityDefaults(values)" in source assert "Weekly availability saved" in source def test_conflicted_pull_review_has_accessible_touch_safe_recovery_actions(): markup = INDEX.read_text() styles = CSS.read_text() assert 'id="week-pull-conflict-review"' in markup assert 'aria-labelledby="week-pull-conflict-title"' in markup assert 'id="week-pull-conflict-item"' in markup assert 'id="week-pull-conflict-context"' in markup assert 'id="rebase-week-pull"' in markup assert 'id="keep-week-pull"' in markup assert "Add using current plans" in markup assert "Keep in Week Ahead" in markup assert ".week-pull-conflict-actions button { min-height:44px" in styles assert "overflow-wrap:anywhere" in styles def test_week_workflow_wires_both_conflict_decisions_to_the_durable_controller(): source = CONTROLLER.read_text() assert "resolvePullConflict('add')" in source assert "resolvePullConflict('keep')" in source assert "controller.pullConflict?.()" in source assert "renderPullConflict" in source assert "#week-pull-conflict-review" in source def test_week_controller_requires_executable_days_before_calendar_handoff(): result = run_controller(""" const week=createWeekPlan({fetchJson:async()=>({}),localDate:()=> '2026-08-20',timeZone:()=> 'UTC'}); week.adopt({revision:7,timezone:'UTC',days:[ {plan_date:'2026-08-21',ids:['over'],capacity_minutes:30,estimates:{over:45}}, {plan_date:'2026-08-22',ids:['missing-estimate'],capacity_minutes:60,estimates:{}}, {plan_date:'2026-08-23',ids:['missing-capacity'],capacity_minutes:null,estimates:{'missing-capacity':20}}, ]}); console.log(JSON.stringify(week.review())); """) assert result["can_confirm"] is False assert result["blockers"] == [ {"type": "over-capacity", "plan_date": "2026-08-21", "minutes": 15}, {"type": "missing-estimate", "plan_date": "2026-08-22", "id": "missing-estimate"}, {"type": "missing-capacity", "plan_date": "2026-08-23"}, ] def test_week_controller_lists_seven_local_dates_and_saves_one_without_changing_other_days(): result = run_controller(""" const requests=[]; const initial={revision:4,timezone:'UTC',days:[ {plan_date:'2026-08-21',ids:['issue:r:1:'],capacity_minutes:60,estimates:{'issue:r:1:':30}}, {plan_date:'2026-08-23',ids:['issue:r:3:'],capacity_minutes:90,estimates:{'issue:r:3:':45}} ]}; const fetchJson=async(url,options={})=>{ requests.push({url,method:options.method||'GET',body:options.body?JSON.parse(options.body):null}); if(!options.method)return initial; return {revision:5,timezone:'UTC',days:JSON.parse(options.body).days}; }; const week=createWeekPlan({fetchJson,localDate:()=> '2026-08-20',timeZone:()=> 'UTC'}); await week.load(); const dates=week.dates(); const saved=await week.saveDay('2026-08-22',{ids:['issue:r:2:'],capacity_minutes:120,estimates:{'issue:r:2:':50}}); console.log(JSON.stringify({dates,saved,requests})); """) assert [item["date"] for item in result["dates"]] == [ "2026-08-21", "2026-08-22", "2026-08-23", "2026-08-24", "2026-08-25", "2026-08-26", "2026-08-27", ] assert [day["plan_date"] for day in result["saved"]["days"]] == [ "2026-08-21", "2026-08-22", "2026-08-23" ] assert result["requests"][1]["body"]["base_revision"] == 4 assert result["requests"][1]["body"]["days"][0]["ids"] == ["issue:r:1:"] assert result["requests"][1]["body"]["days"][2]["ids"] == ["issue:r:3:"] def test_week_controller_starts_a_selected_future_day_early_and_refreshes_the_week(): result = run_controller(""" const requests=[]; let loadCount=0; const day={plan_date:'2026-08-21',ids:['issue:r:1:'],capacity_minutes:60,estimates:{'issue:r:1:':30}}; const fetchJson=async(url,options={})=>{ requests.push({url,method:options.method||'GET',body:options.body?JSON.parse(options.body):null}); if(url==='api/v1/week/start-early')return {revision:3,...day,timezone:'UTC'}; loadCount+=1; return loadCount===1?{revision:4,timezone:'UTC',days:[day]}:{revision:5,timezone:'UTC',days:[]}; }; const week=createWeekPlan({fetchJson,localDate:()=> '2026-08-20',timeZone:()=> 'UTC'}); await week.load(); const promoted=await week.startEarly('2026-08-21',2); console.log(JSON.stringify({promoted,requests,state:week.state()})); """) assert result["promoted"]["ids"] == ["issue:r:1:"] assert result["requests"][1] == { "url": "api/v1/week/start-early", "method": "POST", "body": { "promotion_id": "early-2026-08-21-r4", "week_revision": 4, "plan_date": "2026-08-21", "today_revision": 2, }, } assert result["state"]["days"] == [] def test_week_controller_advances_through_one_continuous_seven_day_pass(): result = run_controller(""" const week=createWeekPlan({fetchJson:async()=>({}),localDate:()=> '2026-08-20',timeZone:()=> 'UTC'}); week.adopt({revision:4,timezone:'UTC',days:[ {plan_date:'2026-08-21',ids:['planned'],capacity_minutes:60,estimates:{planned:30}}, {plan_date:'2026-08-23',ids:['also-planned'],capacity_minutes:90,estimates:{'also-planned':45}} ]}); const first=week.pass('2026-08-21'); const middle=week.pass('2026-08-24'); const last=week.pass('2026-08-27'); console.log(JSON.stringify({first,middle,last})); """) assert result == { "first": {"position": 1, "total": 7, "planned": 2, "next_date": "2026-08-22", "last": False}, "middle": {"position": 4, "total": 7, "planned": 2, "next_date": "2026-08-25", "last": False}, "last": {"position": 7, "total": 7, "planned": 2, "next_date": None, "last": True}, } def test_week_controller_reviews_capacity_duplicates_and_moves_work_without_copying(): 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 week=createWeekPlan({storage,getLogin:()=> 'timmy',fetchJson:async()=>({}), localDate:()=> '2026-08-20',timeZone:()=> 'UTC'}); week.adopt({revision:6,timezone:'UTC',days:[ {plan_date:'2026-08-21',ids:['shared','one'],capacity_minutes:45,estimates:{shared:30,one:30}}, {plan_date:'2026-08-22',ids:['shared','two'],capacity_minutes:120,estimates:{shared:30,two:40}} ]}); const before=week.review(); const moved=week.move('shared','2026-08-23'); const after=week.review(); console.log(JSON.stringify({before,moved,after,pending:week.pending()})); """) assert len(result["before"]["days"]) == 7 assert result["before"]["days"][0]["planned_minutes"] == 60 assert result["before"]["days"][0]["overloaded"] is True assert result["before"]["duplicates"] == [{ "id": "shared", "dates": ["2026-08-21", "2026-08-22"] }] assert result["moved"] is True assert result["after"]["duplicates"] == [] assert [day["ids"] for day in result["after"]["days"][:3]] == [["one"], ["two"], ["shared"]] assert result["pending"]["days"][2]["estimates"] == {"shared": 30} def test_week_controller_applies_weekday_defaults_to_new_dates_and_keeps_date_overrides(): result = run_controller(""" const values=new Map();const requests=[]; const storage={getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)}; const fetchJson=async(url,options={})=>{ requests.push(options.body?JSON.parse(options.body):null); if(!options.method)return {revision:3,timezone:'UTC',availability_defaults:[60,120,180,240,300,0,0],days:[ {plan_date:'2026-08-22',ids:['override'],capacity_minutes:45,estimates:{override:30}} ]}; return {revision:4,...JSON.parse(options.body)}; }; const week=createWeekPlan({storage,getLogin:()=> 'timmy',fetchJson, localDate:()=> '2026-08-20',timeZone:()=> 'UTC'}); await week.load(); const inherited=week.day('2026-08-21'); const overridden=week.day('2026-08-22'); const staged=week.stageAvailabilityDefaults([90,120,180,240,300,0,0]); await week.flush(); console.log(JSON.stringify({inherited,overridden,staged,request:requests.at(-1),state:week.state()})); """) assert result["inherited"]["capacity_minutes"] == 300 assert result["inherited"]["capacity_source"] == "default" assert result["overridden"]["capacity_minutes"] == 45 assert result["overridden"]["capacity_source"] == "override" assert result["staged"]["sync_pending"] is True assert result["request"]["availability_defaults"] == [90, 120, 180, 240, 300, 0, 0] assert result["state"]["availability_defaults"] == [90, 120, 180, 240, 300, 0, 0] def test_week_controller_applies_seven_imported_capacities_in_one_local_transition(): result = run_controller(""" const values=new Map();let writes=0; const storage={getItem:key=>values.get(key)||null,setItem:(key,value)=>{writes+=1;values.set(key,value);},removeItem:key=>values.delete(key)}; const week=createWeekPlan({storage,getLogin:()=> 'timmy',fetchJson:async()=>({}), localDate:()=> '2026-08-20',timeZone:()=> 'UTC'}); week.adopt({revision:6,timezone:'UTC',days:[ {plan_date:'2026-08-21',ids:['one'],capacity_minutes:60,estimates:{one:30}}, {plan_date:'2026-08-23',ids:['three'],capacity_minutes:90,estimates:{three:45}} ]}); const capacities=week.dates().map((item,index)=>({plan_date:item.date,capacity_minutes:300+index*15})); const applied=week.stageCapacities(capacities); console.log(JSON.stringify({applied,writes,state:week.state(),pending:week.pending()})); """) assert result["writes"] == 1 assert result["applied"]["sync_pending"] is True assert [day["capacity_minutes"] for day in result["state"]["days"]] == [ 300, 315, 330, 345, 360, 375, 390 ] assert result["state"]["days"][0]["ids"] == ["one"] assert result["state"]["days"][2]["ids"] == ["three"] assert result["pending"]["days"] == result["state"]["days"] def test_week_controller_keeps_consented_free_windows_through_pending_flush_and_reload(): result = run_controller(""" const values=new Map();const requests=[]; const storage={getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)}; const fetchJson=async(url,options={})=>{ if(!options.method)return {revision:4,timezone:'UTC',days:[{plan_date:'2026-08-21',ids:['one'],capacity_minutes:420, estimates:{one:45},free_windows:[{start_time:'09:00',end_time:'10:00'},{start_time:'11:00',end_time:'17:00'}]}]}; const body=JSON.parse(options.body);requests.push(body);return {revision:5,timezone:body.timezone,days:body.days}; }; const options={storage,getLogin:()=> 'timmy',fetchJson,localDate:()=> '2026-08-20',timeZone:()=> 'UTC'}; const first=createWeekPlan(options);await first.load(); const capacities=first.dates().map((item,index)=>({plan_date:item.date,capacity_minutes:index?480:420, ...(index?{}:{free_windows:[{start_time:'09:00',end_time:'10:00'},{start_time:'11:00',end_time:'17:00'}]})})); first.stageCapacities(capacities);const pending=first.pending();await first.flush(); const restored=createWeekPlan(options);await restored.load(); console.log(JSON.stringify({pending,request:requests[0],restored:restored.state()})); """) windows = [ {"start_time": "09:00", "end_time": "10:00"}, {"start_time": "11:00", "end_time": "17:00"}, ] assert result["pending"]["days"][0]["free_windows"] == windows assert result["request"]["days"][0]["free_windows"] == windows assert result["restored"]["days"][0]["free_windows"] == windows def test_week_controller_previews_and_atomically_applies_a_capacity_safe_reflow(): result = run_controller(""" const values=new Map();let writes=0; const storage={getItem:key=>values.get(key)||null,setItem:(key,value)=>{writes+=1;values.set(key,value);},removeItem:key=>values.delete(key)}; const week=createWeekPlan({storage,getLogin:()=> 'timmy',fetchJson:async()=>({}), localDate:()=> '2026-08-20',timeZone:()=> 'UTC'}); week.adopt({revision:6,timezone:'UTC',days:[ {plan_date:'2026-08-21',ids:['one','two'],capacity_minutes:45,estimates:{one:30,two:30}}, {plan_date:'2026-08-22',ids:['three'],capacity_minutes:60,estimates:{three:20}}, {plan_date:'2026-08-23',ids:['large'],capacity_minutes:40,estimates:{large:70}}, {plan_date:'2026-08-24',ids:[],capacity_minutes:60,estimates:{}}, {plan_date:'2026-08-25',ids:[],capacity_minutes:0,estimates:{}}, {plan_date:'2026-08-26',ids:[],capacity_minutes:0,estimates:{}}, {plan_date:'2026-08-27',ids:[],capacity_minutes:0,estimates:{}} ]}); const before=week.state(); const preview=week.previewReflow(); const unchanged=week.state(); const applied=week.applyReflow(); console.log(JSON.stringify({before,preview,unchanged,applied,writes,pending:week.pending()})); """) assert result["preview"]["blockers"] == [] assert result["preview"]["unscheduled"] == [ {"id": "large", "estimate": 70, "from_date": "2026-08-23"} ] assert [day["ids"] for day in result["preview"]["days"]] == [ ["one"], ["two", "three"], [], [], [], [], [] ] assert result["unchanged"] == result["before"] assert result["applied"]["sync_pending"] is True assert result["writes"] == 1 assert result["pending"]["days"] == result["preview"]["days"] def test_week_controller_atomically_reflows_against_refreshed_calendar_capacity_and_keeps_windows(): result = run_controller(""" const values=new Map();let writes=0; const storage={getItem:key=>values.get(key)||null,setItem:(key,value)=>{writes+=1;values.set(key,value);},removeItem:key=>values.delete(key)}; const week=createWeekPlan({storage,getLogin:()=> 'timmy',fetchJson:async()=>({}), localDate:()=> '2026-08-20',timeZone:()=> 'UTC'}); week.adopt({revision:6,timezone:'UTC',days:[ {plan_date:'2026-08-21',ids:['one','two'],capacity_minutes:120,estimates:{one:45,two:45},free_windows:[{start_time:'09:00',end_time:'11:00'}]}, {plan_date:'2026-08-22',ids:['three'],capacity_minutes:120,estimates:{three:30}}, {plan_date:'2026-08-23',ids:[],capacity_minutes:120,estimates:{}} ]}); const capacities=week.dates().map((item,index)=>({plan_date:item.date,capacity_minutes:index===0?45:120, free_windows:index===0?[{start_time:'10:00',end_time:'10:45'}]:[{start_time:'09:00',end_time:'11:00'}]})); const before=week.state();const preview=week.previewCapacityReflow(capacities);const unchanged=week.state(); const applied=week.applyCapacityReflow(capacities); console.log(JSON.stringify({before,preview,unchanged,applied,writes,pending:week.pending()})); """) assert result["unchanged"] == result["before"] assert result["writes"] == 1 assert result["applied"]["sync_pending"] is True assert [day["ids"] for day in result["preview"]["days"][:2]] == [["one"], ["two", "three"]] assert result["preview"]["days"][0]["free_windows"] == [ {"start_time": "10:00", "end_time": "10:45"} ] assert result["pending"]["days"] == result["preview"]["days"] assert [item for day in result["pending"]["days"] for item in day["ids"]] == ["one", "two", "three"] def test_week_controller_reflow_never_places_more_than_five_items_on_a_day(): result = run_controller(""" const week=createWeekPlan({fetchJson:async()=>({}),localDate:()=> '2026-08-20',timeZone:()=> 'UTC'}); const estimates=Object.fromEntries(Array.from({length:7},(_,index)=>['item-'+index,10])); week.adopt({revision:6,timezone:'UTC',days:[ {plan_date:'2026-08-21',ids:Object.keys(estimates),capacity_minutes:120,estimates}, {plan_date:'2026-08-22',ids:[],capacity_minutes:120,estimates:{}} ]}); console.log(JSON.stringify(week.previewReflow())); """) assert [day["ids"] for day in result["days"][:2]] == [ ["item-0", "item-1", "item-2", "item-3", "item-4"], ["item-5", "item-6"], ] assert result["unscheduled"] == [] def test_week_controller_reflow_keeps_only_the_first_chronological_assignment(): result = run_controller(""" const week=createWeekPlan({fetchJson:async()=>({}),localDate:()=> '2026-08-20',timeZone:()=> 'UTC'}); week.adopt({revision:6,timezone:'UTC',days:[ {plan_date:'2026-08-21',ids:['one','shared'],capacity_minutes:120,estimates:{one:30,shared:20}}, {plan_date:'2026-08-22',ids:['shared','two'],capacity_minutes:120,estimates:{shared:20,two:30}} ]}); console.log(JSON.stringify(week.previewReflow())); """) assert [item for day in result["days"] for item in day["ids"]] == ["one", "shared", "two"] assert result["unscheduled"] == [] def test_week_controller_reflow_never_backfills_later_work_ahead_of_earlier_work(): result = run_controller(""" const week=createWeekPlan({fetchJson:async()=>({}),localDate:()=> '2026-08-20',timeZone:()=> 'UTC'}); week.adopt({revision:6,timezone:'UTC',days:[ {plan_date:'2026-08-21',ids:['large','small'],capacity_minutes:30,estimates:{large:50,small:20}}, {plan_date:'2026-08-22',ids:[],capacity_minutes:60,estimates:{}}, {plan_date:'2026-08-23',ids:[],capacity_minutes:30,estimates:{}} ]}); console.log(JSON.stringify(week.previewReflow())); """) assert [day["ids"] for day in result["days"][:3]] == [[], ["large"], ["small"]] def test_week_controller_blocks_reflow_when_work_has_no_estimate(): result = run_controller(""" const values=new Map();let writes=0; const storage={getItem:key=>values.get(key)||null,setItem:(key,value)=>{writes+=1;values.set(key,value);},removeItem:key=>values.delete(key)}; const week=createWeekPlan({storage,getLogin:()=> 'timmy',fetchJson:async()=>({}), localDate:()=> '2026-08-20',timeZone:()=> 'UTC'}); week.adopt({revision:6,timezone:'UTC',days:[ {plan_date:'2026-08-21',ids:['known','unknown'],capacity_minutes:90,estimates:{known:30}}, {plan_date:'2026-08-22',ids:[],capacity_minutes:90,estimates:{}} ]}); const before=week.state();const preview=week.previewReflow();const applied=week.applyReflow(); console.log(JSON.stringify({before,preview,applied,after:week.state(),writes})); """) assert result["preview"]["blockers"] == [{ "type": "missing-estimate", "id": "unknown", "plan_date": "2026-08-21" }] assert result["preview"]["days"] == [] assert result["applied"] is False assert result["after"] == result["before"] assert result["writes"] == 0 def test_week_controller_places_new_work_or_explicitly_moves_existing_work_without_duplicates(): 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 week=createWeekPlan({storage,getLogin:()=> 'timmy',fetchJson:async()=>({}), localDate:()=> '2026-08-20',timeZone:()=> 'UTC'}); week.adopt({revision:6,timezone:'UTC',days:[ {plan_date:'2026-08-21',ids:['existing'],capacity_minutes:60,estimates:{existing:30}}, {plan_date:'2026-08-22',ids:['other'],capacity_minutes:120,estimates:{other:45}} ]}); const before=week.placement('existing'); const refused=week.place('existing','2026-08-22',50); const moved=week.place('existing','2026-08-22',50,{move:true}); const added=week.place('new','2026-08-23',25); console.log(JSON.stringify({before,refused,moved,added,state:week.state(),pending:week.pending()})); """) assert result["before"] == {"date": "2026-08-21", "estimate": 30} assert result["refused"] is False assert result["moved"] is True assert result["added"] is True assert [day["ids"] for day in result["state"]["days"]] == [[], ["other", "existing"], ["new"]] assert result["state"]["days"][1]["estimates"]["existing"] == 50 assert result["pending"]["days"] == result["state"]["days"] def test_week_controller_pulls_one_item_into_today_and_adopts_returned_week(): result = run_controller(""" const requests=[]; const week=createWeekPlan({fetchJson:async(url,options={})=>{ const body=JSON.parse(options.body);requests.push({url,body}); return {today:{revision:5,ids:['active',body.identity],capacity_minutes:120,estimates:{[body.identity]:45}}, week:{revision:8,timezone:'UTC',days:[{plan_date:'2026-08-21',ids:['sibling'],capacity_minutes:90,estimates:{sibling:30}}]}}; },localDate:()=> '2026-08-20',timeZone:()=> 'UTC'}); week.adopt({revision:7,timezone:'UTC',days:[{plan_date:'2026-08-21',ids:['pull','sibling'],capacity_minutes:90,estimates:{pull:45,sibling:30}}]}); const pulled=await week.pullItem('pull',{revision:4,ids:['active']},'stable-pull',true); console.log(JSON.stringify({pulled,requests,state:week.state()})); """) assert result["requests"] == [{ "url": "api/v1/week/pull-item", "body": { "operation_id": "stable-pull", "identity": "pull", "today_revision": 4, "week_revision": 7, "allow_over_capacity": True, }, }] assert result["pulled"]["today"]["ids"] == ["active", "pull"] assert result["state"]["revision"] == 8 assert result["state"]["days"][0]["ids"] == ["sibling"] def test_week_controller_replays_a_lost_pull_receipt_with_the_same_operation_after_reload(): result = run_controller(""" const values=new Map(),requests=[]; const storage={getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)}; let loseResponse=true; const fetchJson=async(url,options={})=>{ const body=JSON.parse(options.body);requests.push(body); if(loseResponse){loseResponse=false;throw new Error('connection lost');} return {today:{revision:5,ids:['active',body.identity],capacity_minutes:120,estimates:{active:30,[body.identity]:45}}, week:{revision:8,timezone:'UTC',days:[{plan_date:'2026-08-21',ids:['sibling'],capacity_minutes:90,estimates:{sibling:30}}]}}; }; const options={storage,getLogin:()=> 'Timmy',fetchJson,localDate:()=> '2026-08-20',timeZone:()=> 'UTC'}; const first=createWeekPlan(options); first.adopt({revision:7,timezone:'UTC',days:[{plan_date:'2026-08-21',ids:['pull','sibling'],capacity_minutes:90,estimates:{pull:45,sibling:30}}]}); const pending=await first.pullItem('pull',{revision:4,ids:['active'],capacity_minutes:120,estimates:{active:30}},'stable-pull'); const restored=createWeekPlan(options),resume=restored.resumePull(); const confirmed=await restored.flushPull(); console.log(JSON.stringify({pending,resume,confirmed,requests,keys:[...values.keys()],state:restored.state()})); """) assert result["pending"]["sync_pending"] is True assert result["pending"]["today"]["ids"] == ["active", "pull"] assert result["pending"]["week"]["days"][0]["ids"] == ["sibling"] assert result["resume"]["sync_pending"] is True assert result["resume"]["today"] == result["pending"]["today"] assert [request["operation_id"] for request in result["requests"]] == ["stable-pull", "stable-pull"] assert result["confirmed"]["sync_pending"] is False assert result["confirmed"]["today"]["revision"] == 5 assert result["keys"] == ["stackchain.week-confirmed.v1.timmy"] assert result["state"]["revision"] == 8 def test_week_controller_queues_two_offline_pulls_and_replays_them_fifo_after_reload(): result = run_controller(""" const values=new Map(),delivered=[]; const storage={getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)}; let offline=true; let serverToday={revision:4,ids:['active'],capacity_minutes:180,estimates:{active:30}}; let serverWeek={revision:7,timezone:'UTC',days:[{plan_date:'2026-08-21',ids:['first','second','later'],capacity_minutes:150,estimates:{first:35,second:40,later:45}}]}; const fetchJson=async(url,options={})=>{ if(offline)throw new Error('connection lost'); const body=JSON.parse(options.body);delivered.push(body); if(body.today_revision!==serverToday.revision||body.week_revision!==serverWeek.revision){const error=new Error('changed');error.status=409;throw error;} serverToday={...serverToday,revision:serverToday.revision+1,ids:[...serverToday.ids,body.identity], estimates:{...serverToday.estimates,[body.identity]:serverWeek.days[0].estimates[body.identity]}}; serverWeek={...serverWeek,revision:serverWeek.revision+1,days:serverWeek.days.map(day=>({...day, ids:day.ids.filter(id=>id!==body.identity),estimates:Object.fromEntries(Object.entries(day.estimates).filter(([id])=>id!==body.identity))}))}; return {today:serverToday,week:serverWeek}; }; const options={storage,getLogin:()=> 'Timmy',fetchJson,localDate:()=> '2026-08-20',timeZone:()=> 'UTC'}; const first=createWeekPlan(options);first.adopt(serverWeek); const queuedFirst=await first.pullItem('first',serverToday,'pull-first'); const queuedSecond=await first.pullItem('second',queuedFirst.today,'pull-second'); const restored=createWeekPlan(options),resumed=restored.resumePull(); offline=false; const confirmed=await restored.flushPull(); console.log(JSON.stringify({queuedFirst,queuedSecond,resumed,confirmed,delivered,keys:[...values.keys()],state:restored.state()})); """) assert result["queuedFirst"]["today"]["ids"] == ["active", "first"] assert result["queuedSecond"]["today"]["ids"] == ["active", "first", "second"] assert result["resumed"]["today"]["ids"] == ["active", "first", "second"] assert [request["operation_id"] for request in result["delivered"]] == ["pull-first", "pull-second"] assert result["delivered"][1]["today_revision"] == 5 assert result["delivered"][1]["week_revision"] == 8 assert result["confirmed"]["today"]["ids"] == ["active", "first", "second"] assert result["confirmed"]["week"]["days"][0]["ids"] == ["later"] assert result["confirmed"]["sync_pending"] is False assert result["keys"] == ["stackchain.week-confirmed.v1.timmy"] assert result["state"]["revision"] == 9 @pytest.mark.parametrize( "coordinator_setup", [ """const locks={request:async(name,_options,work)=>{ if(held.has(name))return work(null); held.add(name);try{return await work({name});}finally{held.delete(name);} }}; const coordinatorA=createOutboxCoordinator({storage,locks,channelFactory:null,tabId:'a'}); const coordinatorB=createOutboxCoordinator({storage,locks,channelFactory:null,tabId:'b'});""", """const coordinatorA=createOutboxCoordinator({storage,locks:null,channelFactory:null,tabId:'a',now:()=>100}); const coordinatorB=createOutboxCoordinator({storage,locks:null,channelFactory:null,tabId:'b',now:()=>100});""", ], ids=["navigator-lock", "storage-lease"], ) def test_week_controller_allows_only_one_tab_to_replay_the_shared_pull_queue(coordinator_setup): result = run_controller(""" const values=new Map(),held=new Set(); const storage={getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)}; COORDINATOR_SETUP let offline=true,posts=0,release; const gate=new Promise(resolve=>release=resolve); const fetchJson=async(_url,options={})=>{ if(offline)throw new Error('connection lost'); posts+=1;await gate; const body=JSON.parse(options.body); return {today:{revision:5,ids:['active',body.identity],capacity_minutes:180,estimates:{active:30,[body.identity]:35}}, week:{revision:8,timezone:'UTC',days:[{plan_date:'2026-08-21',ids:[],capacity_minutes:150,estimates:{}}]}}; }; const options={storage,getLogin:()=> 'timmy',fetchJson,localDate:()=> '2026-08-20',timeZone:()=> 'UTC'}; const source=createWeekPlan({...options,coordinator:coordinatorA});source.adopt({revision:7,timezone:'UTC',days:[{plan_date:'2026-08-21',ids:['first'],capacity_minutes:150,estimates:{first:35}}]}); await source.pullItem('first',{revision:4,ids:['active'],capacity_minutes:180,estimates:{active:30}},'pull-first'); offline=false; const first=createWeekPlan({...options,coordinator:coordinatorA}),second=createWeekPlan({...options,coordinator:coordinatorB}); const firstFlush=first.flushPull(); await Promise.resolve(); setImmediate(release); const skipped=await second.flushPull(); release(); const confirmed=await firstFlush; console.log(JSON.stringify({posts,skipped,confirmed,pending:first.pendingPull()})); """.replace("COORDINATOR_SETUP", coordinator_setup)) assert result["posts"] == 1 assert result["skipped"]["lease_skipped"] is True assert result["confirmed"]["sync_pending"] is False assert result["pending"] is None def test_week_controller_resumes_server_truth_when_a_fifo_head_conflicts(): 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 offline=true,posts=0; const remoteToday={revision:10,ids:['server-today'],capacity_minutes:180,estimates:{'server-today':30}}; const remoteWeek={revision:12,timezone:'UTC',days:[{plan_date:'2026-08-21',ids:['second','third','server-week'],capacity_minutes:180,estimates:{second:40,third:45,'server-week':25}}]}; const fetchJson=async(url,options={})=>{ if(offline)throw new Error('connection lost'); if(!options.method)return url.endsWith('/today')?remoteToday:remoteWeek; posts+=1; if(posts===1)return {today:{revision:5,ids:['active','first'],capacity_minutes:180,estimates:{active:30,first:35}}, week:{revision:8,timezone:'UTC',days:[{plan_date:'2026-08-21',ids:['second','third'],capacity_minutes:180,estimates:{second:40,third:45}}]}}; const error=new Error('changed');error.status=409;throw error; }; const options={storage,getLogin:()=> 'timmy',fetchJson,localDate:()=> '2026-08-20',timeZone:()=> 'UTC'}; const first=createWeekPlan(options);first.adopt({revision:7,timezone:'UTC',days:[{plan_date:'2026-08-21',ids:['first','second','third'],capacity_minutes:180,estimates:{first:35,second:40,third:45}}]}); let today={revision:4,ids:['active'],capacity_minutes:180,estimates:{active:30}}; today=(await first.pullItem('first',today,'pull-first')).today; today=(await first.pullItem('second',today,'pull-second')).today; await first.pullItem('third',today,'pull-third'); offline=false;const conflicted=await first.flushPull(); const restored=createWeekPlan(options),resumed=restored.resumePull(); console.log(JSON.stringify({conflicted,resumed,state:restored.state(),pending:restored.pendingPull()})); """) assert result["conflicted"]["conflict"] is True assert result["resumed"]["conflict"] is True assert result["resumed"]["today"]["ids"] == ["server-today"] assert result["state"]["revision"] == 12 assert result["state"]["days"][0]["ids"] == ["second", "third", "server-week"] assert result["pending"]["body"]["operation_id"] == "pull-second" def test_week_controller_preserves_a_conflicted_pull_intent_and_adopts_server_truth(): result = run_controller(""" const values=new Map(),requests=[]; const storage={getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)}; const remoteToday={revision:9,ids:['newer'],capacity_minutes:90,estimates:{newer:30}}; const remoteWeek={revision:12,timezone:'UTC',days:[{plan_date:'2026-08-21',ids:['pull','server'],capacity_minutes:90,estimates:{pull:45,server:30}}]}; const fetchJson=async(url,options={})=>{ requests.push({url,method:options.method||'GET'}); if(options.method){const error=new Error('changed');error.status=409;throw error;} return url.endsWith('/today')?remoteToday:remoteWeek; }; const week=createWeekPlan({storage,getLogin:()=> 'timmy',fetchJson,localDate:()=> '2026-08-20',timeZone:()=> 'UTC'}); week.adopt({revision:7,timezone:'UTC',days:[{plan_date:'2026-08-21',ids:['pull'],capacity_minutes:90,estimates:{pull:45}}]}); const outcome=await week.pullItem('pull',{revision:4,ids:['active'],capacity_minutes:90,estimates:{active:30}},'conflicted-pull'); console.log(JSON.stringify({outcome,state:week.state(),pending:week.pendingPull(),requests})); """) assert result["outcome"]["conflict"] is True assert result["outcome"]["today"]["ids"] == ["newer"] assert result["state"]["revision"] == 12 assert result["state"]["days"][0]["ids"] == ["pull", "server"] assert result["pending"]["body"]["operation_id"] == "conflicted-pull" assert result["pending"]["conflict"] is True assert result["requests"] == [ {"url": "api/v1/week/pull-item", "method": "POST"}, {"url": "api/v1/today", "method": "GET"}, {"url": "api/v1/week", "method": "GET"}, ] def test_week_controller_describes_the_conflicted_transfer_from_current_plans(): 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 remoteToday={revision:9,ids:['active'],capacity_minutes:90,estimates:{active:30}}; const remoteWeek={revision:12,timezone:'UTC',days:[{plan_date:'2026-08-21',ids:['pull'],capacity_minutes:90,estimates:{pull:45}}]}; const fetchJson=async(url,options={})=>{if(options.method){const error=new Error('changed');error.status=409;throw error;}return url.endsWith('/today')?remoteToday:remoteWeek;}; const week=createWeekPlan({storage,getLogin:()=> 'timmy',fetchJson,localDate:()=> '2026-08-20',timeZone:()=> 'UTC'}); week.adopt({revision:7,timezone:'UTC',days:[{plan_date:'2026-08-21',ids:['pull'],capacity_minutes:90,estimates:{pull:45}}]}); week.rememberItems({'pull':{title:'Ship recovery',kind:'issue',repository:'acme/app',number:7}}); await week.pullItem('pull',{revision:4,ids:['old'],capacity_minutes:90,estimates:{old:30}},'stable-pull'); console.log(JSON.stringify(week.pullConflict())); """) assert result == { "identity": "pull", "title": "Ship recovery", "source_date": "2026-08-21", "today_items": 1, "today_minutes": 30, "today_capacity": 90, "operation_id": "stable-pull", "queue_length": 1, } def test_week_controller_rechecks_capacity_before_rebasing_a_conflicted_pull(): result = run_controller(""" const values=new Map();let posts=0; const storage={getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)}; const remoteToday={revision:9,ids:['active'],capacity_minutes:50,estimates:{active:40}}; const remoteWeek={revision:12,timezone:'UTC',days:[{plan_date:'2026-08-21',ids:['pull'],capacity_minutes:90,estimates:{pull:20}}]}; const fetchJson=async(url,options={})=>{if(options.method){posts+=1;const error=new Error('changed');error.status=409;throw error;}return url.endsWith('/today')?remoteToday:remoteWeek;}; const week=createWeekPlan({storage,getLogin:()=> 'timmy',fetchJson,localDate:()=> '2026-08-20',timeZone:()=> 'UTC'}); week.adopt({revision:7,timezone:'UTC',days:[{plan_date:'2026-08-21',ids:['pull'],capacity_minutes:90,estimates:{pull:20}}]}); await week.pullItem('pull',{revision:4,ids:['old'],capacity_minutes:90,estimates:{old:30}},'stable-pull'); let code=null;try{await week.resolvePullConflict('add');}catch(error){code=error.code;} console.log(JSON.stringify({code,posts,pending:week.pendingPull()})); """) assert result["code"] == "today_over_capacity" assert result["posts"] == 1 assert result["pending"]["conflict"] is True def test_week_controller_rebases_a_conflicted_pull_and_drains_the_fifo_with_stable_operations(): result = run_controller(""" const values=new Map(),delivered=[]; const storage={getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)}; let offline=true; let serverToday={revision:10,ids:['active'],capacity_minutes:180,estimates:{active:30}}; let serverWeek={revision:12,timezone:'UTC',days:[{plan_date:'2026-08-21',ids:['first','second'],capacity_minutes:180,estimates:{first:35,second:40}}]}; const fetchJson=async(url,options={})=>{ if(offline)throw new Error('offline'); if(!options.method)return url.endsWith('/today')?serverToday:serverWeek; const body=JSON.parse(options.body);delivered.push(body); if(body.today_revision!==serverToday.revision||body.week_revision!==serverWeek.revision){const error=new Error('changed');error.status=409;throw error;} serverToday={...serverToday,revision:serverToday.revision+1,ids:[...serverToday.ids,body.identity],estimates:{...serverToday.estimates,[body.identity]:serverWeek.days[0].estimates[body.identity]}}; serverWeek={...serverWeek,revision:serverWeek.revision+1,days:serverWeek.days.map(day=>({...day,ids:day.ids.filter(id=>id!==body.identity),estimates:Object.fromEntries(Object.entries(day.estimates).filter(([id])=>id!==body.identity))}))}; return {today:serverToday,week:serverWeek}; }; const week=createWeekPlan({storage,getLogin:()=> 'timmy',fetchJson,localDate:()=> '2026-08-20',timeZone:()=> 'UTC'}); week.adopt({revision:7,timezone:'UTC',days:[{plan_date:'2026-08-21',ids:['first','second'],capacity_minutes:180,estimates:{first:35,second:40}}]}); let today={revision:4,ids:['active'],capacity_minutes:180,estimates:{active:30}}; today=(await week.pullItem('first',today,'pull-first')).today; await week.pullItem('second',today,'pull-second'); offline=false;await week.flushPull(); const resolved=await week.resolvePullConflict('add'); console.log(JSON.stringify({resolved,delivered,pending:week.pendingPull(),state:week.state()})); """) assert [request["operation_id"] for request in result["delivered"]] == [ "pull-first", "pull-first", "pull-second" ] assert result["delivered"][1]["today_revision"] == 10 assert result["delivered"][1]["week_revision"] == 12 assert result["resolved"]["today"]["ids"] == ["active", "first", "second"] assert result["pending"] is None assert result["state"]["revision"] == 14 def test_week_controller_keeps_the_conflicted_item_in_week_and_advances_only_the_fifo_head(): result = run_controller(""" const values=new Map(),delivered=[]; const storage={getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)}; let offline=true; let serverToday={revision:10,ids:['active'],capacity_minutes:180,estimates:{active:30}}; let serverWeek={revision:12,timezone:'UTC',days:[{plan_date:'2026-08-21',ids:['first','second'],capacity_minutes:180,estimates:{first:35,second:40}}]}; const fetchJson=async(url,options={})=>{ if(offline)throw new Error('offline'); if(!options.method)return url.endsWith('/today')?serverToday:serverWeek; const body=JSON.parse(options.body);delivered.push(body); if(body.today_revision!==serverToday.revision||body.week_revision!==serverWeek.revision){const error=new Error('changed');error.status=409;throw error;} serverToday={...serverToday,revision:11,ids:[...serverToday.ids,body.identity],estimates:{...serverToday.estimates,[body.identity]:40}}; serverWeek={...serverWeek,revision:13,days:serverWeek.days.map(day=>({...day,ids:day.ids.filter(id=>id!==body.identity)}))}; return {today:serverToday,week:serverWeek}; }; const week=createWeekPlan({storage,getLogin:()=> 'timmy',fetchJson,localDate:()=> '2026-08-20',timeZone:()=> 'UTC'}); week.adopt({revision:7,timezone:'UTC',days:[{plan_date:'2026-08-21',ids:['first','second'],capacity_minutes:180,estimates:{first:35,second:40}}]}); let today={revision:4,ids:['active'],capacity_minutes:180,estimates:{active:30}}; today=(await week.pullItem('first',today,'pull-first')).today; await week.pullItem('second',today,'pull-second'); offline=false;await week.flushPull(); const resolved=await week.resolvePullConflict('keep'); console.log(JSON.stringify({resolved,delivered,pending:week.pendingPull()})); """) assert [request["operation_id"] for request in result["delivered"]] == ["pull-first", "pull-second"] assert result["delivered"][1]["today_revision"] == 10 assert result["delivered"][1]["week_revision"] == 12 assert result["resolved"]["today"]["ids"] == ["active", "second"] assert result["resolved"]["week"]["days"][0]["ids"] == ["first"] assert result["pending"] is None def test_week_controller_retires_completed_work_from_every_day_in_one_durable_transition(): result = run_controller(""" const values=new Map();let writes=0; const storage={getItem:key=>values.get(key)||null,setItem:(key,value)=>{writes+=1;values.set(key,value);},removeItem:key=>values.delete(key)}; const week=createWeekPlan({storage,getLogin:()=> 'timmy',fetchJson:async()=>({}), localDate:()=> '2026-08-20',timeZone:()=> 'UTC'}); week.adopt({revision:6,timezone:'UTC',days:[ {plan_date:'2026-08-21',ids:['completed','sibling'],capacity_minutes:90,estimates:{completed:30,sibling:45}}, {plan_date:'2026-08-22',ids:['other','completed'],capacity_minutes:120,estimates:{other:60,completed:30}}, {plan_date:'2026-08-23',ids:['untouched'],capacity_minutes:75,estimates:{untouched:25}} ]}); const retired=week.retire('completed'); console.log(JSON.stringify({retired,writes,state:week.state(),pending:week.pending()})); """) assert result["retired"]["removed"] == 2 assert result["retired"]["sync_pending"] is True assert result["writes"] == 1 assert result["state"]["days"] == [ {"plan_date": "2026-08-21", "ids": ["sibling"], "capacity_minutes": 90, "estimates": {"sibling": 45}}, {"plan_date": "2026-08-22", "ids": ["other"], "capacity_minutes": 120, "estimates": {"other": 60}}, {"plan_date": "2026-08-23", "ids": ["untouched"], "capacity_minutes": 75, "estimates": {"untouched": 25}}, ] assert result["pending"]["days"] == result["state"]["days"] def test_week_controller_unplans_active_work_and_restores_exact_placement(): result = run_controller(""" const values=new Map();let writes=0; const storage={getItem:key=>values.get(key)||null,setItem:(key,value)=>{writes+=1;values.set(key,value);},removeItem:key=>values.delete(key)}; const week=createWeekPlan({storage,getLogin:()=> 'timmy',fetchJson:async()=>({}), localDate:()=> '2026-08-20',timeZone:()=> 'UTC'}); week.adopt({revision:6,timezone:'UTC',days:[ {plan_date:'2026-08-21',ids:['first','remove','last'],capacity_minutes:120,estimates:{first:20,remove:35,last:40}}, {plan_date:'2026-08-22',ids:['other'],capacity_minutes:90,estimates:{other:30}} ]}); const receipt=week.unplan('remove'); const removed=week.state(); const restored=week.restore(receipt); console.log(JSON.stringify({receipt,removed,restored,writes,state:week.state(),pending:week.pending()})); """) assert result["receipt"] == { "id": "remove", "plan_date": "2026-08-21", "index": 1, "estimate": 35, } assert result["removed"]["days"][0]["ids"] == ["first", "last"] assert result["restored"]["sync_pending"] is True assert result["writes"] == 2 assert result["state"]["days"][0]["ids"] == ["first", "remove", "last"] assert result["state"]["days"][0]["estimates"]["remove"] == 35 assert result["pending"]["days"] == result["state"]["days"] def test_week_workflow_saves_and_advances_without_closing_the_planner(): result = run_controller(""" const createWorkflow=createWeekPlan.Workflow; const dates=['2026-08-21','2026-08-22','2026-08-23','2026-08-24','2026-08-25','2026-08-26','2026-08-27']; const elements=new Map(); const makeElement=()=>({hidden:false,textContent:'',disabled:false,innerHTML:'',addEventListener:()=>{},focus:()=>{}, querySelectorAll:()=>[],querySelector:()=>null,scrollIntoView:()=>{}}); const root=makeElement(); Object.defineProperty(root,'innerHTML',{set(value){this.value=value;this.buttons=[...value.matchAll(/data-week-plan-date=\"([^\"]+)/g)].map(match=>({ dataset:{weekPlanDate:match[1]},addEventListener:()=>{},scrollIntoView(){this.scrolled=true;} }));},get(){return this.value||'';}}); root.querySelectorAll=()=>root.buttons||[]; root.querySelector=selector=>(root.buttons||[]).find(button=>selector.includes(button.dataset.weekPlanDate))||null; elements.set('#week-plan-dates',root); for(const selector of ['#mobile-week-summary','#my-work-action-status','#week-plan-progress','#save-today-plan', '#week-review','#week-review-days','#week-review-duplicates','#confirm-week-plan','#week-review-status','#edit-week-plan']) elements.set(selector,makeElement()); let opened=0,staged=[]; const controller={dates:()=>dates.map((date,index)=>({date,label:'Day '+(index+1)})),day:date=>({plan_date:date,ids:[]}), pass:date=>{const index=dates.indexOf(date);return {position:index+1,total:7,planned:staged.length,next_date:dates[index+1]||null,last:index===6};}, load:async()=>({}),summary:()=> 'Nothing planned',review:()=>({days:dates.map(date=>({plan_date:date,label:date,ids:[],capacity_minutes:null,estimates:{},planned_minutes:0,overloaded:false})),duplicates:[],blockers:[],can_confirm:true}),pending:()=>false, stageDay:(date,plan)=>{staged.push({date,plan});return true;},flush:async()=>({})}; const workflow=createWorkflow({controller,qs:selector=>elements.get(selector),getLogin:()=> 'timmy', openPlanner:()=>{opened+=1;},escapeHtml:value=>value,escapeAttribute:value=>value, todayWork:{replace:()=>{},replacePlanning:()=>{}},refresh:()=>{},warm:()=>{}}); await workflow.open({disabled:false}); workflow.editWeek(); workflow.save({ids:['issue:r:1:'],capacity_minutes:60,estimates:{'issue:r:1:':30}}); const continued=workflow.advance(); console.log(JSON.stringify({continued,selected:workflow.selectedDate(),opened,staged, progress:elements.get('#week-plan-progress').textContent,label:elements.get('#save-today-plan').textContent})); """) assert result["continued"] is True assert result["selected"] == "2026-08-22" assert result["opened"] == 4 assert result["staged"][0]["date"] == "2026-08-21" assert result["progress"] == "Day 2 of 7 · 1 planned" assert result["label"] == "Save & next" def test_week_workflow_opens_a_read_only_overview_with_next_up_work(): result = run_controller(""" const createWorkflow=createWeekPlan.Workflow; const dates=['2026-08-21','2026-08-22','2026-08-23','2026-08-24','2026-08-25','2026-08-26','2026-08-27']; const elements=new Map(); const makeElement=()=>({hidden:false,textContent:'',disabled:false,innerHTML:'',addEventListener:()=>{},focus:()=>{}, querySelectorAll:()=>[],querySelector:()=>null,scrollIntoView:()=>{}}); const dateRoot=makeElement(); Object.defineProperty(dateRoot,'innerHTML',{set(value){this.value=value;this.buttons=[];},get(){return this.value||'';}}); elements.set('#week-plan-dates',dateRoot); const reviewDays=makeElement(); Object.defineProperty(reviewDays,'innerHTML',{set(value){this.value=value;this.pullButtons=[...value.matchAll(/data-week-pull-item=\"([^\"]+)/g)].map(match=>({ dataset:{weekPullItem:match[1]},listeners:{},disabled:false, addEventListener(name,listener){this.listeners[name]=listener;},focus(){this.focused=true;} }));},get(){return this.value||'';}}); reviewDays.querySelectorAll=selector=>selector==='[data-week-pull-item]'?reviewDays.pullButtons||[]:[]; elements.set('#week-review-days',reviewDays); for(const selector of ['#mobile-week-summary','#my-work-action-status','#week-plan-progress','#save-today-plan', '#week-review','#week-review-duplicates','#confirm-week-plan','#week-review-status','#back-to-week-review', '#edit-week-plan']) if(!elements.has(selector))elements.set(selector,makeElement()); let writes=0,opened=0,reviewMode=false,pullRequest=null,replacedToday=null; const review={days:dates.map((date,index)=>({plan_date:date,label:'Day '+(index+1), ids:index===2?['issue:stackchain/dashboard:42:']:[],capacity_minutes:60, estimates:index===2?{'issue:stackchain/dashboard:42:':30}:{},planned_minutes:index===2?30:0,overloaded:false})), duplicates:[],blockers:[],can_confirm:true}; const controller={dates:()=>dates.map((date,index)=>({date,label:'Day '+(index+1)})),day:date=>review.days.find(day=>day.plan_date===date), pass:date=>{const index=dates.indexOf(date);return {position:index+1,total:7,planned:1,next_date:dates[index+1]||null,last:index===6};}, load:async()=>({}),summary:()=> '1 item across 1 day',review:()=>review,pending:()=>false,offline:()=>false, move:()=>true,flush:async()=>{writes+=1;},pullItem:async(id,today,operationId)=>{ pullRequest={id,today,operationId};return {today:{revision:4,ids:['active',id],estimates:{[id]:30}},week:{revision:8,days:[]}}; }}; const workflow=createWorkflow({controller,qs:selector=>elements.get(selector),getLogin:()=> 'timmy', getItem:id=>id.includes(':42:')?{kind:'issue',title:'Ship mobile overview',repository:'stackchain/dashboard',number:42}:null, openPlanner:()=>{opened+=1;},setReviewMode:value=>{reviewMode=value;},escapeHtml:value=>value,escapeAttribute:value=>value, today:()=>({revision:3,ids:['active'],estimates:{}}),operationId:()=> 'pull-once', todayWork:{replace:ids=>{replacedToday=ids;},replacePlanning:()=>{}},refresh:()=>{},warm:()=>{}}); await workflow.open({disabled:false}); const initialMarkup=reviewDays.innerHTML; await reviewDays.pullButtons[0].listeners.click({currentTarget:reviewDays.pullButtons[0]}); console.log(JSON.stringify({reviewing:workflow.reviewing(),reviewMode,writes,opened,copy:workflow.copy(), reviewHidden:elements.get('#week-review').hidden,datesHidden:elements.get('#week-plan-dates').hidden, markup:initialMarkup,status:elements.get('#week-review-status').textContent,pullRequest,replacedToday, confirmHidden:elements.get('#confirm-week-plan').hidden,editWeekHidden:elements.get('#edit-week-plan').hidden, editWeekLabel:elements.get('#edit-week-plan').textContent})); """) assert result["reviewing"] is True assert result["reviewMode"] is True assert result["writes"] == 0 assert result["copy"] == { "title": "Week Ahead", "heading": "Seven-day overview", "available": "Planned work", "build": "Edit week", } assert result["reviewHidden"] is False assert result["datesHidden"] is True assert "Ship mobile overview" in result["markup"] assert "Next up" in result["markup"] assert "Add to Today" in result["markup"] assert result["pullRequest"]["id"] == "issue:stackchain/dashboard:42:" assert result["pullRequest"]["operationId"] == "pull-once" assert result["replacedToday"] == ["active", "issue:stackchain/dashboard:42:"] assert "data-week-move" not in result["markup"] assert result["status"] == "Ship mobile overview added to Today." assert result["confirmHidden"] is True assert result["editWeekHidden"] is False assert result["editWeekLabel"] == "Edit week" def test_week_overview_reviews_today_capacity_before_confirming_an_overload(): result = run_controller(""" const createWorkflow=createWeekPlan.Workflow; const dates=['2026-08-21','2026-08-22','2026-08-23','2026-08-24','2026-08-25','2026-08-26','2026-08-27']; const elements=new Map(); const makeElement=()=>({hidden:false,textContent:'',disabled:false,innerHTML:'',addEventListener:()=>{},focus:()=>{}, querySelectorAll:()=>[],querySelector:()=>null,scrollIntoView:()=>{}}); elements.set('#week-plan-dates',makeElement()); const reviewDays=makeElement(); Object.defineProperty(reviewDays,'innerHTML',{set(value){this.value=value;this.pullButtons=[...value.matchAll(/data-week-pull-item=\"([^\"]+)/g)].map(match=>({ dataset:{weekPullItem:match[1]},listeners:{},disabled:false, addEventListener(name,listener){this.listeners[name]=listener;},focus(){this.focused=true;} }));},get(){return this.value||'';}}); reviewDays.querySelectorAll=selector=>selector==='[data-week-pull-item]'?reviewDays.pullButtons||[]:[]; elements.set('#week-review-days',reviewDays); for(const selector of ['#mobile-week-summary','#my-work-action-status','#week-plan-progress','#save-today-plan', '#week-review','#week-review-duplicates','#confirm-week-plan','#week-review-status','#back-to-week-review','#edit-week-plan']) if(!elements.has(selector))elements.set(selector,makeElement()); const review={days:dates.map((date,index)=>({plan_date:date,label:'Day '+(index+1), ids:index===0?['issue:stackchain/dashboard:44:']:[],capacity_minutes:60, estimates:index===0?{'issue:stackchain/dashboard:44:':30}:{},planned_minutes:index===0?30:0,overloaded:false})), duplicates:[],blockers:[],can_confirm:true}; const pulls=[],confirmations=[];let approve=false; const controller={dates:()=>dates.map((date,index)=>({date,label:'Day '+(index+1)})),day:date=>review.days.find(day=>day.plan_date===date), pass:date=>({position:dates.indexOf(date)+1,total:7,planned:1,next_date:null,last:false}),load:async()=>({}), summary:()=> '1 item across 1 day',review:()=>review,pending:()=>false,offline:()=>false,move:()=>true,flush:async()=>({}), pullItem:async(id,today,operationId,allowOverCapacity)=>{pulls.push({id,operationId,allowOverCapacity}); return {today:{revision:4,ids:['active',id],capacity_minutes:60,estimates:{active:45,[id]:30}},week:{revision:8,days:[]}};}}; const workflow=createWorkflow({controller,qs:selector=>elements.get(selector), getItem:id=>({kind:'issue',title:'Capacity review',repository:'stackchain/dashboard',number:44}), openPlanner:()=>{},escapeHtml:value=>value,escapeAttribute:value=>value, today:()=>({revision:3,ids:['active'],capacity_minutes:60,estimates:{active:45}}),operationId:()=> 'capacity-pull', confirmEarly:message=>{confirmations.push(message);return approve;}, todayWork:{replace:()=>{},replacePlanning:()=>{}},refresh:()=>{},warm:()=>{}}); await workflow.open({disabled:false}); const markup=reviewDays.innerHTML,button=reviewDays.pullButtons[0]; await button.listeners.click({currentTarget:button}); const afterCancel={pulls:pulls.length,disabled:button.disabled,status:elements.get('#week-review-status').textContent}; approve=true; await button.listeners.click({currentTarget:button}); console.log(JSON.stringify({markup,confirmations,afterCancel,pulls})); """) assert "75 of 60 min" in result["markup"] assert result["afterCancel"] == {"pulls": 0, "disabled": False, "status": "Week Ahead overview · no changes made."} assert result["confirmations"] == [ "Add Capacity review to Today?\n\nToday will be 75 of 60 min · 15 min over capacity.", "Add Capacity review to Today?\n\nToday will be 75 of 60 min · 15 min over capacity.", ] assert result["pulls"] == [{ "id": "issue:stackchain/dashboard:44:", "operationId": "capacity-pull", "allowOverCapacity": True, }] def test_week_overview_removes_active_work_and_undo_restores_it(): result = run_controller(""" const createWorkflow=createWeekPlan.Workflow; const dates=['2026-08-21','2026-08-22','2026-08-23','2026-08-24','2026-08-25','2026-08-26','2026-08-27']; const elements=new Map(); const makeElement=()=>({hidden:false,textContent:'',disabled:false,innerHTML:'',listeners:{},dataset:{}, addEventListener(name,listener){this.listeners[name]=listener;},focus(){this.focused=true;},querySelectorAll:()=>[],querySelector:()=>null}); const reviewDays=makeElement(); Object.defineProperty(reviewDays,'innerHTML',{set(value){this.value=value;this.removeButtons=[...value.matchAll(/data-week-unplan=\"([^\"]+)/g)].map(match=>({ dataset:{weekUnplan:match[1]},listeners:{},addEventListener(name,listener){this.listeners[name]=listener;},focus(){this.focused=true;} }));},get(){return this.value||'';}}); reviewDays.querySelectorAll=selector=>selector==='[data-week-unplan]'?reviewDays.removeButtons||[]:[]; elements.set('#week-review-days',reviewDays); const undo=makeElement();elements.set('#undo-week-unplan',undo); for(const selector of ['#week-plan-dates','#mobile-week-summary','#my-work-action-status','#week-plan-progress','#save-today-plan', '#week-review','#week-review-duplicates','#confirm-week-plan','#week-review-status','#back-to-week-review','#edit-week-plan', '#week-offline-snapshot','#retry-week-live','#open-week-capacity-import','#open-week-reflow','#week-unplan-receipt']) if(!elements.has(selector))elements.set(selector,makeElement()); let ids=['one','remove','last'],unplanned=null,restored=null,flushes=0,timer=null; const buildReview=()=>({days:dates.map((date,index)=>({plan_date:date,label:'Day '+(index+1),ids:index===0?[...ids]:[], capacity_minutes:120,estimates:index===0?{one:20,remove:35,last:40}:{},planned_minutes:index===0?ids.reduce((sum,id)=>sum+({one:20,remove:35,last:40}[id]),0):0,overloaded:false})),duplicates:[],blockers:[],can_confirm:true}); const controller={dates:()=>dates.map((date,index)=>({date,label:'Day '+(index+1)})),day:date=>buildReview().days.find(day=>day.plan_date===date), load:async()=>({}),summary:()=> ids.length+' items across 1 day',review:buildReview,pending:()=>false,offline:()=>false, unplan:id=>{const index=ids.indexOf(id);if(index<0)return false;ids.splice(index,1);return unplanned={id,plan_date:dates[0],index,estimate:35};}, restore:receipt=>{ids.splice(receipt.index,0,receipt.id);restored=receipt;return {};},flush:async()=>{flushes+=1;return {};}}; const workflow=createWorkflow({controller,qs:selector=>elements.get(selector),getItem:id=>({kind:'issue',title:id==='remove'?'Remove me':id,repository:'r',number:2}), openPlanner:()=>{},setReviewMode:()=>{},escapeHtml:value=>value,escapeAttribute:value=>value, setTimer:callback=>{timer=callback;return 1;},clearTimer:()=>{timer=null;}, todayWork:{replace:()=>{},replacePlanning:()=>{}},refresh:()=>{},warm:()=>{}}); await workflow.open({disabled:false}); const remove=reviewDays.removeButtons.find(button=>button.dataset.weekUnplan==='remove'); await remove.listeners.click({currentTarget:remove}); const afterRemove={ids:[...ids],flushes,receiptHidden:elements.get('#week-unplan-receipt').hidden, status:elements.get('#week-unplan-receipt').textContent,undoHidden:undo.hidden,timer:Boolean(timer)}; await undo.listeners.click({currentTarget:undo}); console.log(JSON.stringify({buttonCount:reviewDays.removeButtons.length,unplanned,afterRemove,restored,ids,flushes, receiptHidden:elements.get('#week-unplan-receipt').hidden,undoFocused:undo.focused||false})); """) assert result["buttonCount"] == 3 assert result["unplanned"] == { "id": "remove", "plan_date": "2026-08-21", "index": 1, "estimate": 35, } assert result["afterRemove"] == { "ids": ["one", "last"], "flushes": 1, "receiptHidden": False, "status": "Remove me removed from Week Ahead.", "undoHidden": False, "timer": True, } assert result["restored"] == result["unplanned"] assert result["ids"] == ["one", "remove", "last"] assert result["flushes"] == 2 assert result["receiptHidden"] is True def test_week_overview_previews_cancels_and_applies_one_reflow(): result = run_controller(""" const createWorkflow=createWeekPlan.Workflow; const dates=['2026-08-21','2026-08-22','2026-08-23','2026-08-24','2026-08-25','2026-08-26','2026-08-27']; const elements=new Map(); const makeElement=()=>({hidden:false,textContent:'',disabled:false,innerHTML:'',dataset:{},listeners:{}, addEventListener(name,listener){this.listeners[name]=listener;},focus(){this.focused=true;},querySelectorAll:()=>[],querySelector:()=>null}); for(const selector of ['#week-plan-dates','#week-review-days','#week-review-duplicates','#week-review','#week-review-status', '#confirm-week-plan','#edit-week-plan','#week-plan-progress','#save-today-plan','#mobile-week-summary','#my-work-action-status', '#week-offline-snapshot','#retry-week-live','#open-week-capacity-import','#open-week-reflow','#week-reflow-review', '#week-reflow-days','#week-reflow-unscheduled','#cancel-week-reflow','#apply-week-reflow'])elements.set(selector,makeElement()); const review={days:dates.map((date,index)=>({plan_date:date,label:'Day '+(index+1),ids:index===0?['one','two']:[], capacity_minutes:index===0?30:60,estimates:index===0?{one:30,two:30}:{},planned_minutes:index===0?60:0,overloaded:index===0})), duplicates:[],blockers:[{type:'over-capacity',plan_date:dates[0],minutes:30}],can_confirm:false}; const preview={days:dates.map((date,index)=>({plan_date:date,ids:index<2?[["one"],["two"]][index]:[],capacity_minutes:index===0?30:60, estimates:index===0?{one:30}:index===1?{two:30}:{}})),blockers:[],unscheduled:[{id:'large',estimate:90,from_date:dates[0]}]}; let applied=0,flushed=0; const controller={dates:()=>dates.map((date,index)=>({date,label:'Day '+(index+1)})),day:date=>review.days.find(day=>day.plan_date===date), pass:()=>({}),load:async()=>({}),summary:()=>applied?'2 items across 2 days':'2 items across 1 day',review:()=>review, pending:()=>false,offline:()=>false,move:()=>true,previewReflow:()=>preview,applyReflow:()=>{applied+=1;return {};}, flush:async()=>{flushed+=1;return {};}}; const workflow=createWorkflow({controller,qs:selector=>elements.get(selector),getItem:id=>({kind:'issue',title:id==='large'?'Large task':id,repository:'r',number:1}), openPlanner:()=>{},setReviewMode:()=>{},escapeHtml:value=>value,escapeAttribute:value=>value, todayWork:{replace:()=>{},replacePlanning:()=>{}},refresh:()=>{},warm:()=>{}}); await workflow.open({disabled:false}); const offered=!elements.get('#open-week-reflow').hidden; await elements.get('#open-week-reflow').listeners.click(); const firstPreview={hidden:elements.get('#week-reflow-review').hidden,days:elements.get('#week-reflow-days').innerHTML, unscheduled:elements.get('#week-reflow-unscheduled').textContent}; await elements.get('#cancel-week-reflow').listeners.click(); const afterCancel={hidden:elements.get('#week-reflow-review').hidden,applied,flushed}; await elements.get('#open-week-reflow').listeners.click(); await elements.get('#apply-week-reflow').listeners.click(); console.log(JSON.stringify({offered,firstPreview,afterCancel,applied,flushed,status:elements.get('#week-review-status').textContent, reflowHidden:elements.get('#week-reflow-review').hidden})); """) assert result["offered"] is True assert result["firstPreview"]["hidden"] is False assert "Day 1" in result["firstPreview"]["days"] assert "1 item · 30 of 30 min" in result["firstPreview"]["days"] assert result["firstPreview"]["unscheduled"] == "1 item cannot fit: Large task. It will remain in My Work." assert result["afterCancel"] == {"hidden": True, "applied": 0, "flushed": 0} assert result["applied"] == 1 assert result["flushed"] == 1 assert result["status"] == "Week Ahead reflowed and saved." assert result["reflowHidden"] is True def test_week_overview_confirms_and_starts_the_next_day_early_from_empty_today(): result = run_controller(""" const createWorkflow=createWeekPlan.Workflow; const dates=['2026-08-21','2026-08-22','2026-08-23','2026-08-24','2026-08-25','2026-08-26','2026-08-27']; const elements=new Map(); const makeElement=()=>({hidden:false,textContent:'',disabled:false,innerHTML:'',addEventListener:()=>{},focus:()=>{},querySelectorAll:()=>[],querySelector:()=>null,scrollIntoView:()=>{}}); const root=makeElement(); Object.defineProperty(root,'innerHTML',{set(value){this.value=value;this.startButtons=[...value.matchAll(/data-week-start-early=\"([^\"]+)/g)].map(match=>({dataset:{weekStartEarly:match[1]},listeners:{},disabled:false,addEventListener(name,listener){this.listeners[name]=listener;}}));},get(){return this.value||'';}}); root.querySelectorAll=selector=>selector==='[data-week-start-early]'?root.startButtons||[]:[]; elements.set('#week-review-days',root); elements.set('#week-plan-dates',makeElement()); for(const selector of ['#mobile-week-summary','#my-work-action-status','#week-plan-progress','#save-today-plan','#week-review','#week-review-duplicates','#confirm-week-plan','#week-review-status','#back-to-week-review','#edit-week-plan','#week-offline-snapshot','#retry-week-live','#open-week-capacity-import'])if(!elements.has(selector))elements.set(selector,makeElement()); const day={plan_date:'2026-08-21',label:'Friday, Aug 21',ids:['one','two'],capacity_minutes:90,estimates:{one:30,two:45},planned_minutes:75,overloaded:false}; const review={days:dates.map((date,index)=>index===0?day:{plan_date:date,label:'Day '+(index+1),ids:[],capacity_minutes:null,estimates:{},planned_minutes:0,overloaded:false}),duplicates:[],blockers:[],can_confirm:true}; let request=null,confirmation='',replaced=null,refreshed=0; const controller={dates:()=>dates.map((date,index)=>({date,label:'Day '+(index+1)})),day:date=>review.days.find(value=>value.plan_date===date),pass:()=>({}),load:async()=>({}),summary:()=> 'Nothing planned',review:()=>review,pending:()=>false,offline:()=>false,move:()=>true,flush:async()=>{},startEarly:async(date,revision)=>{request={date,revision};return {ids:['one','two'],capacity_minutes:90,estimates:{one:30,two:45}};}}; const workflow=createWorkflow({controller,qs:selector=>elements.get(selector),openPlanner:()=>{},setReviewMode:()=>{},escapeHtml:value=>value,escapeAttribute:value=>value,today:()=>({revision:7,ids:[]}),confirmEarly:message=>{confirmation=message;return true;},todayWork:{replace:ids=>{replaced=ids;},replacePlanning:()=>{}},refresh:()=>{refreshed+=1;},warm:()=>{}}); await workflow.open({disabled:false}); const button=root.startButtons[0]; await button.listeners.click({currentTarget:button}); console.log(JSON.stringify({buttonCount:root.startButtons.length,markup:root.innerHTML,confirmation,request,replaced,refreshed,status:elements.get('#my-work-action-status').textContent})); """) assert result["buttonCount"] == 1 assert "Start this day early" in result["markup"] assert "Friday, Aug 21" in result["confirmation"] assert "2 items · 75 of 90 min" in result["confirmation"] assert result["request"] == {"date": "2026-08-21", "revision": 7} assert result["replaced"] == ["one", "two"] assert result["refreshed"] == 1 assert result["status"] == "Friday, Aug 21 is now Today." def test_week_overview_opens_known_work_with_the_originating_control_and_leaves_unknown_work_static(): result = run_controller(""" const createWorkflow=createWeekPlan.Workflow; const dates=['2026-08-21','2026-08-22','2026-08-23','2026-08-24','2026-08-25','2026-08-26','2026-08-27']; const elements=new Map(); const makeElement=()=>({hidden:false,textContent:'',disabled:false,innerHTML:'',addEventListener:()=>{},focus:()=>{}, querySelectorAll:()=>[],querySelector:()=>null,scrollIntoView:()=>{}}); const dateRoot=makeElement(); Object.defineProperty(dateRoot,'innerHTML',{set(value){this.value=value;},get(){return this.value||'';}}); elements.set('#week-plan-dates',dateRoot); const reviewDays=makeElement(); Object.defineProperty(reviewDays,'innerHTML',{set(value){ this.value=value; this.openButtons=[...value.matchAll(/data-week-open-item=\"([^\"]+)/g)].map(match=>({ dataset:{weekOpenItem:match[1]},listeners:{},isConnected:true,focused:false, addEventListener(name,listener){this.listeners[name]=listener;},focus(){this.focused=true;} })); },get(){return this.value||'';}}); reviewDays.querySelectorAll=selector=>selector==='[data-week-open-item]'?reviewDays.openButtons||[]:[]; elements.set('#week-review-days',reviewDays); for(const selector of ['#mobile-week-summary','#my-work-action-status','#week-plan-progress','#save-today-plan', '#week-review','#week-review-duplicates','#confirm-week-plan','#week-review-status','#back-to-week-review', '#edit-week-plan','#week-offline-snapshot','#retry-week-live','#open-week-capacity-import']) if(!elements.has(selector))elements.set(selector,makeElement()); const known={kind:'issue',title:'Inspect planned work',repository:'stackchain/dashboard',number:42}; const review={days:dates.map((date,index)=>({plan_date:date,label:'Day '+(index+1), ids:index===0?['known','missing']:[],capacity_minutes:90, estimates:index===0?{known:30,missing:20}:{},planned_minutes:index===0?50:0,overloaded:false})), duplicates:[],blockers:[],can_confirm:true}; let opened=null,writes=0; const controller={dates:()=>dates.map((date,index)=>({date,label:'Day '+(index+1)})),day:date=>review.days.find(day=>day.plan_date===date), pass:()=>({}),load:async()=>({}),summary:()=> '2 items across 1 day',review:()=>review,pending:()=>false,offline:()=>false, move:()=>true,flush:async()=>{writes+=1;}}; const workflow=createWorkflow({controller,qs:selector=>elements.get(selector),getLogin:()=> 'timmy', getItem:id=>id==='known'?known:null,openItem:(item,trigger)=>{opened={item,sameTrigger:trigger===reviewDays.openButtons[0]};}, openPlanner:()=>{},setReviewMode:()=>{},escapeHtml:value=>value,escapeAttribute:value=>value, todayWork:{replace:()=>{},replacePlanning:()=>{}},refresh:()=>{},warm:()=>{}}); await workflow.open({disabled:false}); const button=reviewDays.openButtons[0]; if(button)button.listeners.click({currentTarget:button}); console.log(JSON.stringify({markup:reviewDays.innerHTML,buttonCount:reviewDays.openButtons.length,opened,writes})); """) assert result["buttonCount"] == 1 assert 'class="week-review-item-open"' in result["markup"] assert 'data-week-open-item="known"' in result["markup"] assert 'data-week-open-item="missing"' not in result["markup"] assert result["opened"] == { "item": { "kind": "issue", "title": "Inspect planned work", "repository": "stackchain/dashboard", "number": 42, }, "sameTrigger": True, } assert result["writes"] == 0 def test_dashboard_routes_week_overview_controls_through_existing_detail_flow_with_mobile_targets(): dashboard = (FRONTEND / "dashboard.js").read_text() css = (FRONTEND / "dashboard.css").read_text() workflow_mount = dashboard.split("const weekFlow=createWeekPlanWorkflow({", 1)[1].split("});", 1)[0] assert "openItem:openRoutedWork" in workflow_mount assert "t:v=>v?latestTodayPlan=v:latestTodayPlan" in workflow_mount assert ".week-review-item-open" in css rule = css.split(".week-review-item-open", 1)[1].split("}", 1)[0] assert "min-height:44px" in rule assert "width:100%" in rule assert ".week-start-early" in css start_rule = css.split(".week-start-early", 1)[1].split("}", 1)[0] assert "min-height:44px" in start_rule assert "width:100%" in start_rule assert ".week-review-pull" in css pull_rule = css.split(".week-review-pull", 1)[1].split("}", 1)[0] assert "min-height:44px" in pull_rule assert "width:100%" in pull_rule def test_dashboard_restores_and_retries_a_saved_week_to_today_pull_on_mobile_lifecycle(): dashboard = (FRONTEND / "dashboard.js").read_text() week_plan = (FRONTEND / "week-plan.js").read_text() recovery = dashboard.split(" function syncPendingTomorrow()", 1)[1].split("\n }", 1)[0] assert "weekFlow.resumePull()" in recovery assert "weekFlow.flushPull()" in recovery lifecycle = dashboard.split("tomorrowPlan.startLifecycle({", 1)[1].split(" });", 1)[0] assert "syncPendingTomorrow()" in lifecycle assert dashboard.count("syncPendingTomorrow();") >= 2 assert "result.sync_pending?String(item?.title||'Work')+' added to Today · sync pending.'" in week_plan def test_successful_issue_close_notifies_week_ahead_only_after_upstream_confirmation(): issue_sheet = (FRONTEND / "issue-sheet.js").read_text() week_plan = (FRONTEND / "week-plan.js").read_text() close_method = issue_sheet.split(" close(item) {", 1)[1].split(" release(item)", 1)[0] confirmed = close_method.index("result?.state !== 'closed'") notified = close_method.index("stackchain:issue-closed") assert confirmed < notified assert "detail:item" in close_method assert "addEventListener?.('stackchain:issue-closed'" in week_plan assert "retire(event.detail)" in week_plan def test_week_workflow_refreshes_the_open_overview_and_reports_pending_retirement(): result = run_controller(""" const createWorkflow=createWeekPlan.Workflow; const dates=['2026-08-21','2026-08-22','2026-08-23','2026-08-24','2026-08-25','2026-08-26','2026-08-27']; const elements=new Map(); const makeElement=()=>({hidden:false,textContent:'',disabled:false,innerHTML:'',dataset:{},focused:false, addEventListener:()=>{},focus(){this.focused=true;},querySelectorAll:()=>[],querySelector:()=>null,scrollIntoView:()=>{}}); elements.set('#week-plan-dates',makeElement()); const reviewDays=makeElement();elements.set('#week-review-days',reviewDays); for(const selector of ['#mobile-week-summary','#my-work-action-status','#week-plan-progress','#save-today-plan','#week-review', '#week-review-duplicates','#confirm-week-plan','#week-review-status','#back-to-week-review','#edit-week-plan', '#week-offline-snapshot','#retry-week-live','#open-week-capacity-import','#open-week-reflow','#week-reflow-review']) if(!elements.has(selector))elements.set(selector,makeElement()); let ids=['completed','sibling']; const review=()=>({days:dates.map((date,index)=>({plan_date:date,label:'Day '+(index+1),ids:index===0?[...ids]:[], capacity_minutes:90,estimates:index===0?Object.fromEntries(ids.map(id=>[id,id==='completed'?30:45])):{}, planned_minutes:index===0?ids.reduce((sum,id)=>sum+(id==='completed'?30:45),0):0,overloaded:false})), duplicates:[],blockers:[],can_confirm:true}); const controller={dates:()=>dates.map((date,index)=>({date,label:'Day '+(index+1)})),day:date=>review().days.find(day=>day.plan_date===date), load:async()=>({}),summary:()=>ids.length+' items across 1 day',review,pending:()=>false,offline:()=>false, retire:id=>{ids=ids.filter(value=>value!==id);return {removed:1,sync_pending:true};}, flush:async()=>{throw new Error('offline');}}; let identityKind=null; const workflow=createWorkflow({controller,qs:selector=>elements.get(selector), getItem:id=>({kind:'issue',title:id==='completed'?'Completed work':'Sibling work',repository:'r',number:id==='completed'?1:2}), openPlanner:()=>{},setReviewMode:()=>{},escapeHtml:value=>value,escapeAttribute:value=>value, todayWork:{identity:item=>{identityKind=item.kind;return item.id;},replace:()=>{},replacePlanning:()=>{}},refresh:()=>{},warm:()=>{}}); await workflow.open({disabled:false}); const outcome=await workflow.retire({id:'completed',title:'Completed work'}); console.log(JSON.stringify({outcome,identityKind,markup:reviewDays.innerHTML,status:elements.get('#week-review-status').textContent, editFocused:elements.get('#edit-week-plan').focused})); """) assert result["outcome"] == "pending" assert result["identityKind"] == "issue" assert "Completed work" not in result["markup"] assert "Sibling work" in result["markup"] assert result["status"] == "Completed work removed · sync pending." assert result["editFocused"] is True def test_week_workflow_marks_a_confirmed_fallback_read_only_and_retries_live_data(): result = run_controller(""" const createWorkflow=createWeekPlan.Workflow; const dates=['2026-08-21','2026-08-22','2026-08-23','2026-08-24','2026-08-25','2026-08-26','2026-08-27']; const elements=new Map(); const makeElement=()=>({hidden:false,textContent:'',disabled:false,innerHTML:'',dataset:{},listeners:{}, addEventListener(name,listener){this.listeners[name]=listener;},focus:()=>{},querySelectorAll:()=>[],querySelector:()=>null}); const dateRoot=makeElement();Object.defineProperty(dateRoot,'innerHTML',{set(value){this.value=value;this.buttons=[];},get(){return this.value||'';}}); elements.set('#week-plan-dates',dateRoot); const reviewDays=makeElement();Object.defineProperty(reviewDays,'innerHTML',{set(value){this.value=value;this.editButtons=[...value.matchAll(/data-week-edit-day/g)];},get(){return this.value||'';}}); elements.set('#week-review-days',reviewDays); for(const selector of ['#mobile-week-summary','#my-work-action-status','#week-plan-progress','#save-today-plan','#week-review', '#week-review-duplicates','#confirm-week-plan','#week-review-status','#back-to-week-review','#edit-week-plan', '#week-offline-snapshot','#retry-week-live','#open-week-capacity-import']) if(!elements.has(selector))elements.set(selector,makeElement()); let offline=true,loads=0,remembered=null; const review={days:dates.map((date,index)=>({plan_date:date,label:'Day '+(index+1),ids:index===0?['issue:r:42:']:[], capacity_minutes:60,estimates:index===0?{'issue:r:42:':30}:{},planned_minutes:index===0?30:0,overloaded:false})), duplicates:[],blockers:[],can_confirm:true}; const controller={dates:()=>dates.map((date,index)=>({date,label:'Day '+(index+1)})),day:date=>review.days.find(day=>day.plan_date===date), pass:()=>({}),load:async()=>{loads+=1;const value={offline_snapshot:offline,refreshed_at:'2026-08-20T12:34:00.000Z'};offline=false;return value;}, summary:()=> '1 item across 1 day',review:()=>review,pending:()=>false,offline:()=>loads===1, item:id=>id?{kind:'issue',title:'Cached work title',repository:'r',number:42}:null, rememberItems:value=>{remembered=value;},move:()=>true,flush:async()=>({})}; const workflow=createWorkflow({controller,qs:selector=>elements.get(selector),getLogin:()=> 'timmy',getItem:()=>null, openPlanner:()=>{},setReviewMode:()=>{},escapeHtml:value=>value,escapeAttribute:value=>value, todayWork:{replace:()=>{},replacePlanning:()=>{}},refresh:()=>{},warm:()=>{}}); await workflow.open({disabled:false}); const fallback={noticeHidden:elements.get('#week-offline-snapshot').hidden, notice:elements.get('#week-offline-snapshot').textContent,markup:reviewDays.innerHTML, editWeekHidden:elements.get('#edit-week-plan').hidden,capacityHidden:elements.get('#open-week-capacity-import').hidden}; await elements.get('#retry-week-live').listeners.click({currentTarget:elements.get('#retry-week-live')}); console.log(JSON.stringify({fallback,loads,live:{noticeHidden:elements.get('#week-offline-snapshot').hidden, editWeekHidden:elements.get('#edit-week-plan').hidden,capacityHidden:elements.get('#open-week-capacity-import').hidden},remembered})); """) assert result["fallback"]["noticeHidden"] is False assert "Offline snapshot" in result["fallback"]["notice"] assert "Aug" in result["fallback"]["notice"] assert "Cached work title" in result["fallback"]["markup"] assert "data-week-edit-day" not in result["fallback"]["markup"] assert result["fallback"]["editWeekHidden"] is True assert result["fallback"]["capacityHidden"] is True assert result["loads"] == 2 assert result["live"] == {"noticeHidden": True, "editWeekHidden": False, "capacityHidden": False} assert result["remembered"] == {} def test_week_workflow_opens_review_after_day_seven_and_blocks_duplicate_confirmation(): result = run_controller(""" const createWorkflow=createWeekPlan.Workflow; const dates=['2026-08-21','2026-08-22','2026-08-23','2026-08-24','2026-08-25','2026-08-26','2026-08-27']; const elements=new Map(); const makeElement=()=>({hidden:false,textContent:'',disabled:false,innerHTML:'',addEventListener:()=>{},focus:()=>{}, querySelectorAll:()=>[],querySelector:()=>null,scrollIntoView:()=>{}}); const root=makeElement(); Object.defineProperty(root,'innerHTML',{set(value){this.value=value;this.buttons=[];},get(){return this.value||'';}}); elements.set('#week-plan-dates',root); for(const selector of ['#mobile-week-summary','#my-work-action-status','#week-plan-progress','#save-today-plan', '#week-review','#week-review-days','#week-review-duplicates','#confirm-week-plan','#week-review-status','#edit-week-plan']) elements.set(selector,makeElement()); let reviewMode=false; const review={days:dates.map((date,index)=>({plan_date:date,label:'Day '+(index+1),ids:index<2?['shared']:[], capacity_minutes:index===0?30:60,planned_minutes:index===0?45:15,overloaded:index===0})), duplicates:[{id:'shared',dates:dates.slice(0,2)}],can_confirm:false}; const controller={dates:()=>dates.map((date,index)=>({date,label:'Day '+(index+1)})),day:date=>review.days.find(day=>day.plan_date===date), pass:date=>{const index=dates.indexOf(date);return {position:index+1,total:7,planned:2,next_date:dates[index+1]||null,last:index===6};}, load:async()=>({}),summary:()=> '2 items across 2 days',review:()=>review,move:()=>true,flush:async()=>({})}; const workflow=createWorkflow({controller,qs:selector=>elements.get(selector),getLogin:()=> 'timmy', openPlanner:()=>{},setReviewMode:value=>{reviewMode=value;},escapeHtml:value=>value,escapeAttribute:value=>value, todayWork:{replace:()=>{},replacePlanning:()=>{}},refresh:()=>{},warm:()=>{}}); await workflow.open({disabled:false}); workflow.editWeek(); for(let index=0;index<6;index++) workflow.advance(); const advanced=workflow.advance(); console.log(JSON.stringify({advanced,reviewing:workflow.reviewing(),reviewMode, reviewHidden:elements.get('#week-review').hidden,progress:elements.get('#week-plan-progress').textContent, confirmDisabled:elements.get('#confirm-week-plan').disabled,days:elements.get('#week-review-days').innerHTML, duplicates:elements.get('#week-review-duplicates').innerHTML})); """) assert result["advanced"] is True assert result["reviewing"] is True assert result["reviewMode"] is True assert result["reviewHidden"] is False assert result["progress"] == "Review week · 2 planned" assert result["confirmDisabled"] is True assert "45 of 30 min · over capacity" in result["days"] assert "shared" in result["duplicates"] def test_week_review_names_work_and_can_edit_a_day_then_return_to_review(): result = run_controller(""" const createWorkflow=createWeekPlan.Workflow; const dates=['2026-08-21','2026-08-22','2026-08-23','2026-08-24','2026-08-25','2026-08-26','2026-08-27']; const elements=new Map(); const makeElement=()=>({hidden:false,textContent:'',disabled:false,innerHTML:'',dataset:{},listeners:{}, addEventListener(name,listener){this.listeners[name]=listener;},focus(){this.focused=true;}, querySelectorAll:()=>[],querySelector:()=>null,scrollIntoView:()=>{}}); const dateRoot=makeElement(); Object.defineProperty(dateRoot,'innerHTML',{set(value){this.value=value;this.buttons=[...value.matchAll(/data-week-plan-date=\"([^\"]+)/g)].map(match=>({ dataset:{weekPlanDate:match[1]},addEventListener:()=>{},scrollIntoView:()=>{},focus:()=>{} }));},get(){return this.value||'';}}); dateRoot.querySelectorAll=()=>dateRoot.buttons||[]; dateRoot.querySelector=selector=>(dateRoot.buttons||[]).find(button=>selector.includes(button.dataset.weekPlanDate))||null; elements.set('#week-plan-dates',dateRoot); const reviewDays=makeElement(); Object.defineProperty(reviewDays,'innerHTML',{set(value){ this.value=value;this.editButtons=[...value.matchAll(/data-week-edit-day=\"([^\"]+)/g)].map(match=>{ const button=makeElement();button.dataset.weekEditDay=match[1];return button; }); },get(){return this.value||'';}}); reviewDays.querySelectorAll=selector=>selector==='[data-week-edit-day]'?reviewDays.editButtons||[]:[]; reviewDays.querySelector=selector=>(reviewDays.editButtons||[]).find(button=>selector.includes(button.dataset.weekEditDay))||null; elements.set('#week-review-days',reviewDays); for(const selector of ['#mobile-week-summary','#my-work-action-status','#week-plan-progress','#save-today-plan', '#week-review','#week-review-duplicates','#confirm-week-plan','#week-review-status','#back-to-week-review']) { if(!elements.has(selector))elements.set(selector,makeElement()); } let reviewMode=false; const review={days:dates.map((date,index)=>({plan_date:date,label:'Day '+(index+1), ids:index===0?['issue:stackchain/dashboard:42:']:[],capacity_minutes:60, estimates:index===0?{'issue:stackchain/dashboard:42:':30}:{},planned_minutes:index===0?30:0,overloaded:false})), duplicates:[],can_confirm:true}; const controller={dates:()=>dates.map((date,index)=>({date,label:'Day '+(index+1)})),day:date=>review.days.find(day=>day.plan_date===date), pass:date=>{const index=dates.indexOf(date);return {position:index+1,total:7,planned:1,next_date:dates[index+1]||null,last:index===6};}, load:async()=>({}),summary:()=> '1 item across 1 day',review:()=>review,pending:()=>false,move:()=>true,flush:async()=>({})}; const workflow=createWorkflow({controller,qs:selector=>elements.get(selector),getLogin:()=> 'timmy', getItem:id=>id==='issue:stackchain/dashboard:42:'?{kind:'issue',title:'Fix mobile sign in',repository:'stackchain/dashboard',number:42}:null, openPlanner:()=>{},setReviewMode:value=>{reviewMode=value;},escapeHtml:value=>value,escapeAttribute:value=>value, todayWork:{replace:()=>{},replacePlanning:()=>{}},refresh:()=>{},warm:()=>{}}); await workflow.open({disabled:false}); for(let index=0;index<7;index++)workflow.advance(); const markup=reviewDays.innerHTML; const edit=reviewDays.editButtons[0];edit.listeners.click(); const whileEditing={selected:workflow.selectedDate(),reviewing:workflow.reviewing(),reviewMode, reviewHidden:elements.get('#week-review').hidden,backHidden:elements.get('#back-to-week-review').hidden}; elements.get('#back-to-week-review').listeners.click(); console.log(JSON.stringify({markup,whileEditing,after:{selected:workflow.selectedDate(),reviewing:workflow.reviewing(),reviewMode, reviewHidden:elements.get('#week-review').hidden,editFocused:reviewDays.editButtons[0].focused}})); """) assert "Fix mobile sign in" in result["markup"] assert "stackchain/dashboard #42 · Issue · 30 min" in result["markup"] assert "issue:stackchain/dashboard:42:" not in result["markup"] assert result["whileEditing"] == { "selected": "2026-08-21", "reviewing": False, "reviewMode": False, "reviewHidden": True, "backHidden": False, } assert result["after"] == { "selected": None, "reviewing": True, "reviewMode": True, "reviewHidden": False, "editFocused": True, } def test_week_controller_preserves_both_versions_when_another_device_changes_the_week(): result = run_controller(""" let calls=0; const remote={revision:8,timezone:'UTC',days:[{plan_date:'2026-08-22',ids:['server'],capacity_minutes:60,estimates:{}}]}; const fetchJson=async(_url,options={})=>{ calls+=1; if(options.method==='PUT'){const error=new Error('changed');error.status=409;throw error;} return calls===1?{revision:7,timezone:'UTC',days:[]}:remote; }; const week=createWeekPlan({fetchJson,localDate:()=> '2026-08-20',timeZone:()=> 'UTC'}); await week.load(); try { await week.saveDay('2026-08-21',{ids:['phone'],capacity_minutes:90,estimates:{}}); } catch(_error) {} console.log(JSON.stringify({conflict:week.conflict(),state:week.state()})); """) assert result["conflict"]["local"]["days"][0]["ids"] == ["phone"] assert result["conflict"]["remote"]["days"][0]["ids"] == ["server"] assert result["state"]["revision"] == 7 def test_week_controller_automatically_merges_changes_made_to_different_days(): 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 puts=0; const base={revision:7,timezone:'UTC',days:[ {plan_date:'2026-08-21',ids:['tuesday-base'],capacity_minutes:60,estimates:{'tuesday-base':30}}, {plan_date:'2026-08-22',ids:['friday-base'],capacity_minutes:90,estimates:{'friday-base':45}} ]}; const remote={revision:8,timezone:'UTC',days:[ {plan_date:'2026-08-21',ids:['tuesday-account'],capacity_minutes:60,estimates:{'tuesday-account':30}}, {plan_date:'2026-08-22',ids:['friday-base'],capacity_minutes:90,estimates:{'friday-base':45}} ]}; const fetchJson=async(_url,options={})=>{ const body=options.body?JSON.parse(options.body):null;requests.push({method:options.method||'GET',body}); if(options.method==='PUT'&&puts++===0){const error=new Error('changed');error.status=409;throw error;} if(options.method==='PUT')return {revision:9,timezone:body.timezone,days:body.days}; return remote; }; const week=createWeekPlan({storage,getLogin:()=> 'timmy',fetchJson,localDate:()=> '2026-08-20',timeZone:()=> 'UTC'}); week.adopt(base); week.stageDay('2026-08-22',{ids:['friday-phone'],capacity_minutes:90,estimates:{'friday-phone':50}}); const saved=await week.flush(); console.log(JSON.stringify({saved,requests,pending:week.pending(),conflict:week.conflict()})); """) assert [request["method"] for request in result["requests"]] == ["PUT", "GET", "PUT"] assert result["requests"][2]["body"]["base_revision"] == 8 assert result["saved"]["days"] == [ {"plan_date": "2026-08-21", "ids": ["tuesday-account"], "capacity_minutes": 60, "estimates": {"tuesday-account": 30}}, {"plan_date": "2026-08-22", "ids": ["friday-phone"], "capacity_minutes": 90, "estimates": {"friday-phone": 50}}, ] assert result["pending"] is False assert result["conflict"] is None def test_week_controller_resolves_only_the_same_day_conflict_and_keeps_other_account_changes(): 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 puts=0; const base={revision:4,timezone:'UTC',days:[ {plan_date:'2026-08-21',ids:['same-base'],capacity_minutes:60,estimates:{}}, {plan_date:'2026-08-22',ids:['other-base'],capacity_minutes:60,estimates:{}} ]}; const remote={revision:5,timezone:'UTC',days:[ {plan_date:'2026-08-21',ids:['same-account'],capacity_minutes:60,estimates:{}}, {plan_date:'2026-08-22',ids:['other-account'],capacity_minutes:60,estimates:{}} ]}; const fetchJson=async(_url,options={})=>{ const body=options.body?JSON.parse(options.body):null;requests.push({method:options.method||'GET',body}); if(options.method==='PUT'&&puts++===0){const error=new Error('changed');error.status=409;throw error;} if(options.method==='PUT')return {revision:6,timezone:body.timezone,days:body.days}; return remote; }; const week=createWeekPlan({storage,getLogin:()=> 'timmy',fetchJson,localDate:()=> '2026-08-20',timeZone:()=> 'UTC'}); week.adopt(base); week.stageDay('2026-08-21',{ids:['same-phone'],capacity_minutes:90,estimates:{}}); try { await week.flush(); } catch(_error) {} const before=week.conflict(); week.chooseDay('2026-08-21','phone'); const selected=JSON.parse(values.get('stackchain.week-sync.v1.timmy')); const saved=await week.saveMerged(); console.log(JSON.stringify({before,selected,saved,requests,pending:week.pending(),conflict:week.conflict()})); """) assert [item["plan_date"] for item in result["before"]["conflicts"]] == ["2026-08-21"] assert result["before"]["conflicts"][0]["local"]["ids"] == ["same-phone"] assert result["before"]["conflicts"][0]["remote"]["ids"] == ["same-account"] assert result["selected"]["resolutions"] == {"2026-08-21": "phone"} assert result["requests"][2]["body"]["base_revision"] == 5 assert [day["ids"] for day in result["saved"]["days"]] == [["same-phone"], ["other-account"]] assert result["pending"] is False assert result["conflict"] is None def test_week_controller_restores_an_account_bound_week_before_network_delivery(): 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 login='Timmy'; let requests=0; const options={storage,getLogin:()=>login,fetchJson:async()=>{requests+=1;throw new Error('offline');}, localDate:()=> '2026-08-20',timeZone:()=> 'America/New_York'}; const first=createWeekPlan(options); first.adopt({revision:7,timezone:'UTC',days:[{plan_date:'2026-08-21',ids:['old'],capacity_minutes:30,estimates:{old:15}}]}); const queued=first.stageDay('2026-08-22',{ids:['issue:r:3:','issue:r:2:'],capacity_minutes:120, estimates:{'issue:r:3:':45,'issue:r:2:':30}}); const restored=await createWeekPlan(options).load(); login='alexander'; let otherError=''; try { await createWeekPlan(options).load(); } catch(error) { otherError=error.message; } console.log(JSON.stringify({queued,restored,requests,otherError,keys:[...values.keys()]})); """) assert result["queued"]["sync_pending"] is True assert result["restored"] == result["queued"] assert result["restored"]["timezone"] == "America/New_York" assert [day["plan_date"] for day in result["restored"]["days"]] == ["2026-08-21", "2026-08-22"] assert result["requests"] == 1 assert result["otherError"] == "offline" assert result["keys"] == ["stackchain.week-sync.v1.timmy"] def test_week_controller_falls_back_to_an_account_bound_confirmed_read_snapshot(): 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 login='Timmy',online=true,revision=7; const options={storage,getLogin:()=>login,now:()=>Date.parse('2026-08-20T12:34:00Z'), fetchJson:async()=>{if(!online)throw new Error('offline');return {revision,timezone:'UTC',days:[ {plan_date:'2026-08-21',ids:['issue:r:42:'],capacity_minutes:60,estimates:{'issue:r:42:':30}} ]};},localDate:()=> '2026-08-20',timeZone:()=> 'UTC'}; const live=createWeekPlan(options); const loaded=await live.load(); live.rememberItems({'issue:r:42:':{kind:'issue',title:'Ship offline overview',repository:'r',number:42,secret:'drop'}}); online=false; const fallback=createWeekPlan(options); const restored=await fallback.load(); login='alexander'; let otherError=''; try { await createWeekPlan(options).load(); } catch(error) { otherError=error.message; } login='timmy';online=true;revision=8; const refreshed=await fallback.load(); console.log(JSON.stringify({loaded,restored,item:fallback.item('issue:r:42:'),otherError,refreshed, keys:[...values.keys()],snapshot:JSON.parse(values.get('stackchain.week-confirmed.v1.timmy'))})); """) assert result["loaded"]["offline_snapshot"] is False assert result["restored"]["offline_snapshot"] is True assert result["restored"]["refreshed_at"] == "2026-08-20T12:34:00.000Z" assert result["restored"]["days"][0]["ids"] == ["issue:r:42:"] assert result["item"] == { "kind": "issue", "title": "Ship offline overview", "repository": "r", "number": 42, } assert result["otherError"] == "offline" assert result["refreshed"]["revision"] == 8 assert result["refreshed"]["offline_snapshot"] is False assert result["keys"] == ["stackchain.week-confirmed.v1.timmy"] assert "secret" not in result["snapshot"]["items"]["issue:r:42:"] def test_week_controller_promotes_bounded_pending_work_details_after_matching_receipt(): 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 online=true;const requests=[]; const options={storage,getLogin:()=> 'timmy',now:()=>Date.parse('2026-08-20T12:34:00Z'), fetchJson:async(_url,request={})=>{ requests.push(request.body?JSON.parse(request.body):null); if(!online)throw new Error('offline'); if(request.method==='PUT')return {revision:8,timezone:'UTC',days:JSON.parse(request.body).days}; return {revision:7,timezone:'UTC',days:[]}; },localDate:()=> '2026-08-20',timeZone:()=> 'UTC'}; const live=createWeekPlan(options);await live.load(); live.place('issue:r:42:','2026-08-21',45); const admitted=live.rememberPendingItem('issue:r:42:',{kind:'issue',title:'T'.repeat(600),repository:'r'.repeat(300), number:42,body:'private body',token:'drop'}); const queued=live.pending(); await live.flush(); const confirmed=JSON.parse(values.get('stackchain.week-confirmed.v1.timmy')); online=false;const offline=createWeekPlan(options);const restored=await offline.load(); console.log(JSON.stringify({admitted,queued,confirmed,restored,item:offline.item('issue:r:42:'),requests})); """) assert result["admitted"] is True assert result["queued"]["items"]["issue:r:42:"]["title"] == "T" * 500 assert len(result["queued"]["items"]["issue:r:42:"]["repository"]) == 255 assert result["requests"][1].keys() == {"base_revision", "timezone", "days"} assert result["confirmed"]["week"]["revision"] == 8 assert result["confirmed"]["items"]["issue:r:42:"] == result["item"] assert result["item"]["number"] == 42 assert "body" not in result["item"] and "token" not in result["item"] assert result["restored"]["offline_snapshot"] is True def test_week_controller_keeps_pending_work_details_across_a_cold_offline_reload(): 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 options={storage,getLogin:()=> 'timmy',fetchJson:async()=>{throw new Error('offline');}, localDate:()=> '2026-08-20',timeZone:()=> 'UTC'}; const first=createWeekPlan(options);first.adopt({revision:7,timezone:'UTC',days:[]}); first.place('issue:r:42:','2026-08-21',45); first.rememberPendingItem('issue:r:42:',{kind:'issue',title:'Pending work',repository:'r',number:42}); const restored=createWeekPlan(options);const state=await restored.load(); console.log(JSON.stringify({state,item:restored.item('issue:r:42:'),pending:restored.pending()})); """) assert result["state"]["sync_pending"] is True assert result["item"] == {"kind": "issue", "title": "Pending work", "repository": "r", "number": 42} assert result["pending"]["items"]["issue:r:42:"] == result["item"] def test_week_controller_does_not_use_a_confirmed_snapshot_after_authentication_fails(): 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 denied=false; const options={storage,getLogin:()=> 'timmy',now:()=>1,localDate:()=> '2026-08-20',timeZone:()=> 'UTC',fetchJson:async()=>{ if(denied){const error=new Error('sign in');error.status=401;throw error;} return {revision:7,timezone:'UTC',days:[]};}}; await createWeekPlan(options).load();denied=true; let status=0; try { await createWeekPlan(options).load(); } catch(error) { status=error.status; } console.log(JSON.stringify({status})); """) assert result["status"] == 401 def test_week_controller_never_mutates_an_offline_confirmed_snapshot(): 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 online=true,requests=[]; const options={storage,getLogin:()=> 'timmy',now:()=>1,localDate:()=> '2026-08-20',timeZone:()=> 'UTC', fetchJson:async(url,request={})=>{requests.push(request.method||'GET');if(!online)throw new Error('offline');return { revision:7,timezone:'UTC',days:[{plan_date:'2026-08-21',ids:['one'],capacity_minutes:60,estimates:{one:30}}]};}}; await createWeekPlan(options).load();online=false; const week=createWeekPlan(options);await week.load(); const staged=week.stageDay('2026-08-22',{ids:['two'],capacity_minutes:60,estimates:{two:30}}); let saved=await week.saveDay('2026-08-22',{ids:['two'],capacity_minutes:60,estimates:{two:30}}); const promoted=await week.promote(3); console.log(JSON.stringify({staged,saved,promoted,requests,pending:week.pending(),state:week.state()})); """) assert result["staged"] is False assert result["saved"] is False assert result["promoted"] is False assert result["requests"] == ["GET", "GET"] assert result["pending"] is False assert result["state"]["days"][0]["ids"] == ["one"] def test_week_controller_refreshes_the_confirmed_snapshot_after_a_successful_save(): 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 tick=0; const fetchJson=async(_url,options={})=>options.method==='PUT'?{revision:8,timezone:'UTC',days:JSON.parse(options.body).days}: {revision:7,timezone:'UTC',days:[{plan_date:'2026-08-21',ids:['one'],capacity_minutes:60,estimates:{one:30}}]}; const week=createWeekPlan({storage,getLogin:()=> 'timmy',now:()=>++tick,fetchJson, localDate:()=> '2026-08-20',timeZone:()=> 'UTC'}); await week.load();week.rememberItems({one:{kind:'issue',title:'One',repository:'r',number:1}}); week.stageDay('2026-08-22',{ids:['two'],capacity_minutes:90,estimates:{two:45}}); await week.flush(); console.log(values.get('stackchain.week-confirmed.v1.timmy')); """) assert result["week"]["revision"] == 8 assert [day["ids"] for day in result["week"]["days"]] == [["one"], ["two"]] assert result["items"]["one"]["title"] == "One" def test_week_controller_flushes_once_and_removes_only_the_matching_pending_week(): 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 release; let requests=0; const fetchJson=async(_url,options={})=>{ requests+=1;const body=JSON.parse(options.body); await new Promise(resolve=>{release=resolve}); return {revision:9,timezone:body.timezone,days:body.days}; }; const week=createWeekPlan({storage,getLogin:()=> 'timmy',fetchJson,localDate:()=> '2026-08-20',timeZone:()=> 'UTC'}); week.adopt({revision:8,timezone:'UTC',days:[]}); week.stageDay('2026-08-21',{ids:['phone'],capacity_minutes:60,estimates:{phone:30}}); const first=week.flush(),second=week.flush(); await Promise.resolve(); const during=week.pending(); release(); const [saved,reused]=await Promise.all([first,second]); console.log(JSON.stringify({requests,during,saved,reused,pendingAfter:week.pending(),state:week.state()})); """) assert result["requests"] == 1 assert result["during"]["days"][0]["ids"] == ["phone"] assert result["saved"]["revision"] == 9 assert result["reused"]["revision"] == 9 assert result["pendingAfter"] is False assert result["state"]["revision"] == 9 def test_week_controller_drains_work_staged_while_a_save_is_in_flight(): 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 releaseFirst;const bodies=[]; const fetchJson=async(_url,options={})=>{ const body=JSON.parse(options.body);bodies.push(body); if(bodies.length===1)await new Promise(resolve=>{releaseFirst=resolve}); return {revision:8+bodies.length,timezone:body.timezone,days:body.days}; }; const week=createWeekPlan({storage,getLogin:()=> 'timmy',fetchJson,localDate:()=> '2026-08-20',timeZone:()=> 'UTC'}); week.adopt({revision:8,timezone:'UTC',days:[]}); week.stageDay('2026-08-21',{ids:['one'],capacity_minutes:60,estimates:{one:30}}); const first=week.flush();await Promise.resolve(); week.stageDay('2026-08-22',{ids:['two'],capacity_minutes:90,estimates:{two:45}}); const second=week.flush();releaseFirst(); const [saved,reused]=await Promise.all([first,second]); console.log(JSON.stringify({bodies,saved,reused,pending:week.pending(),state:week.state()})); """) assert len(result["bodies"]) == 2 assert [day["plan_date"] for day in result["bodies"][1]["days"]] == ["2026-08-21", "2026-08-22"] assert result["bodies"][1]["base_revision"] == 9 assert result["saved"]["revision"] == 10 assert result["reused"]["revision"] == 10 assert result["pending"] is False assert result["state"]["revision"] == 10 def test_week_controller_keeps_phone_week_by_rebasing_onto_the_latest_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 puts=0; const remote={revision:12,timezone:'UTC',days:[{plan_date:'2026-08-21',ids:['server'],capacity_minutes:60,estimates:{}}]}; const fetchJson=async(_url,options={})=>{ requests.push({method:options.method||'GET',body:options.body?JSON.parse(options.body):null}); if(options.method==='PUT'&&puts++===0){const error=new Error('changed');error.status=409;throw error;} if(options.method==='PUT')return {revision:13,timezone:'UTC',days:JSON.parse(options.body).days}; return remote; }; const week=createWeekPlan({storage,getLogin:()=> 'timmy',fetchJson,localDate:()=> '2026-08-20',timeZone:()=> 'UTC'}); week.adopt({revision:11,timezone:'UTC',days:[]}); week.stageDay('2026-08-21',{ids:['phone'],capacity_minutes:90,estimates:{phone:45}}); try { await week.flush(); } catch(_error) {} const before=week.conflict(); const saved=await week.keepLocal(); console.log(JSON.stringify({before,saved,requests,pending:week.pending(),conflict:week.conflict()})); """) assert result["before"]["local"]["days"][0]["ids"] == ["phone"] assert result["before"]["remote"]["days"][0]["ids"] == ["server"] assert [request["method"] for request in result["requests"]] == ["PUT", "GET", "PUT"] assert result["requests"][2]["body"]["base_revision"] == 12 assert result["saved"]["revision"] == 13 assert result["pending"] is False assert result["conflict"] is None def test_week_controller_refreshes_account_week_after_a_second_conflict(): 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 revision=12; const fetchJson=async(_url,options={})=>{ if(options.method==='PUT'){const error=new Error('changed again');error.status=409;throw error;} return {revision:revision++,timezone:'UTC',days:[{plan_date:'2026-08-21',ids:['server-'+(revision-1)],capacity_minutes:60,estimates:{}}]}; }; const week=createWeekPlan({storage,getLogin:()=> 'timmy',fetchJson,localDate:()=> '2026-08-20',timeZone:()=> 'UTC'}); week.adopt({revision:11,timezone:'UTC',days:[]}); week.stageDay('2026-08-21',{ids:['phone'],capacity_minutes:90,estimates:{}}); try { await week.flush(); } catch(_error) {} try { await week.keepLocal(); } catch(_error) {} console.log(JSON.stringify({conflict:week.conflict(),pending:week.pending()})); """) assert result["conflict"]["local"]["days"][0]["ids"] == ["phone"] assert result["conflict"]["remote"]["revision"] == 13 assert result["conflict"]["remote"]["days"][0]["ids"] == ["server-13"] assert result["pending"]["days"][0]["ids"] == ["phone"] def test_week_controller_uses_account_week_without_a_second_write(): 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=[]; const remote={revision:12,timezone:'UTC',days:[{plan_date:'2026-08-21',ids:['server'],capacity_minutes:60,estimates:{server:30}}]}; const fetchJson=async(_url,options={})=>{ requests.push(options.method||'GET'); if(options.method==='PUT'){const error=new Error('changed');error.status=409;throw error;} return remote; }; const week=createWeekPlan({storage,getLogin:()=> 'timmy',fetchJson,localDate:()=> '2026-08-20',timeZone:()=> 'UTC'}); week.adopt({revision:11,timezone:'UTC',days:[]}); week.stageDay('2026-08-21',{ids:['phone'],capacity_minutes:90,estimates:{}}); try { await week.flush(); } catch(_error) {} const adopted=week.useRemote(); console.log(JSON.stringify({adopted,requests,pending:week.pending(),conflict:week.conflict(),state:week.state()})); """) assert result["requests"] == ["PUT", "GET"] assert result["adopted"]["days"][0]["ids"] == ["server"] assert result["pending"] is False assert result["conflict"] is None assert result["state"]["revision"] == 12 def test_week_controller_promotes_only_the_earliest_due_plan_with_stable_identity(): result = run_controller(""" const requests=[]; const week=createWeekPlan({fetchJson:async(url,options={})=>{ requests.push({url,body:options.body?JSON.parse(options.body):null}); return {revision:9,ids:['oldest']}; },localDate:()=> '2026-08-23',timeZone:()=> 'UTC'}); week.adopt({revision:6,timezone:'UTC',days:[ {plan_date:'2026-08-21',ids:['oldest'],capacity_minutes:60,estimates:{}}, {plan_date:'2026-08-22',ids:['newer'],capacity_minutes:60,estimates:{}} ]}); const promoted=await week.promote(3); console.log(JSON.stringify({promoted,requests})); """) assert result["requests"] == [{ "url": "api/v1/week/promote", "body": { "promotion_id": "week-2026-08-21-r6", "week_revision": 6, "plan_date": "2026-08-21", "today_revision": 3, }, }] def test_week_controller_reconciles_reviewed_today_and_adopts_both_results(): result = run_controller(""" const requests=[]; const week=createWeekPlan({fetchJson:async(url,options={})=>{ const body=JSON.parse(options.body);requests.push({url,body}); return {today:{revision:5,ids:body.ids,capacity_minutes:body.capacity_minutes,estimates:body.estimates}, week:{revision:7,timezone:'UTC',days:[{plan_date:'2026-08-23',ids:['later'],capacity_minutes:30,estimates:{later:30}}]}}; },localDate:()=> '2026-08-23',timeZone:()=> 'UTC'}); week.adopt({revision:6,timezone:'UTC',days:[{plan_date:'2026-08-21',ids:['due'],capacity_minutes:60,estimates:{due:30}}]}); const reconciled=await week.reconcile({today:{revision:4,ids:['unfinished']},week:week.state()}, {ids:['unfinished','due'],capacity_minutes:90,estimates:{unfinished:30,due:30}}); console.log(JSON.stringify({reconciled,requests,state:week.state()})); """) assert result["requests"][0]["url"] == "api/v1/week/reconcile" assert result["requests"][0]["body"]["ids"] == ["unfinished", "due"] assert result["requests"][0]["body"]["today_revision"] == 4 assert result["state"]["revision"] == 7 assert result["state"]["days"][0]["ids"] == ["later"] assert result["reconciled"]["today"]["ids"] == ["unfinished", "due"] def test_week_workflow_routes_blocked_rollover_into_atomic_combined_review(): result = run_controller(""" const createWorkflow=createWeekPlan.Workflow; const elements={'#mobile-week-summary':{textContent:''},'#my-work-action-status':{textContent:''}}; let opened=0,reconciled=null,replaced=[]; const blocked=new Error('blocked');blocked.code='week_today_in_progress'; blocked.today={revision:4,ids:['unfinished'],capacity_minutes:45,estimates:{unfinished:30}}; blocked.week={revision:6,timezone:'UTC',days:[{plan_date:'2026-08-21',ids:['due','unfinished'],capacity_minutes:90,estimates:{due:45,unfinished:30}}]}; const controller={load:async()=>({}),promote:async()=>{throw blocked;},state:()=>blocked.week,summary:()=> '1 item across 1 day', reconcile:async(preserved,plan)=>{reconciled={preserved,plan};return {today:{revision:5,...plan},week:{revision:7,timezone:'UTC',days:[]}};}}; const workflow=createWorkflow({controller,qs:selector=>elements[selector],getLogin:()=> 'timmy',openPlanner:()=>{opened+=1;}, escapeHtml:value=>value,escapeAttribute:value=>value, todayWork:{replace:value=>replaced.push(value),replacePlanning:value=>replaced.push(value)},refresh:()=>{},warm:()=>{}, }); const promoted=await workflow.promote({revision:4,ids:['unfinished']}); const review={copy:workflow.copy(),day:workflow.day()}; const saved=workflow.save({ids:['unfinished','due'],capacity_minutes:90,estimates:{unfinished:30,due:45}}); await new Promise(resolve=>setTimeout(resolve,0)); console.log(JSON.stringify({promoted,opened,review,saved,reconciled,replaced,status:elements['#my-work-action-status'].textContent})); """) assert result["promoted"] is False assert result["opened"] == 1 assert result["review"]["copy"]["title"] == "Start today's plan" assert result["review"]["day"]["ids"] == ["unfinished", "due"] assert result["saved"] is True assert result["reconciled"]["plan"]["ids"] == ["unfinished", "due"] assert result["replaced"][0] == ["unfinished", "due"] assert result["status"] == "Today started from unfinished and Week Ahead work." def test_week_controller_does_not_promote_a_pending_week(): 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 requests=0; const week=createWeekPlan({storage,getLogin:()=> 'timmy',fetchJson:async()=>{requests+=1;return {};}, localDate:()=> '2026-08-23',timeZone:()=> 'UTC'}); week.adopt({revision:6,timezone:'UTC',days:[]}); week.stageDay('2026-08-21',{ids:['oldest'],capacity_minutes:60,estimates:{}}); const promoted=await week.promote(3); console.log(JSON.stringify({promoted,requests})); """) assert result == {"promoted": False, "requests": 0} def test_mobile_week_ahead_entry_and_date_strip_are_touch_safe(): index = (FRONTEND / "index.html").read_text() css = (FRONTEND / "dashboard.css").read_text() dashboard = (FRONTEND / "dashboard.js").read_text() assert 'data-mobile-queue="week"' in index assert 'id="week-plan-dates"' in index assert '' in index assert "name === 'week' ? openWeekPlanner" in dashboard assert "weekFlow.save" in dashboard assert ".week-plan-dates button { min-height:44px;" in css assert "overflow-x:auto" in css def test_mobile_week_ahead_review_is_rendered_touch_safe_and_confirmed_explicitly(): index = (FRONTEND / "index.html").read_text() css = (FRONTEND / "dashboard.css").read_text() dashboard = (FRONTEND / "dashboard.js").read_text() assert 'id="week-review"' in index assert 'id="week-review-days"' in index assert 'id="week-review-duplicates"' in index assert 'id="confirm-week-plan"' in index assert "setReviewMode:v=>qs('#plan-today-sheet').classList.toggle('week-review-mode',v)" in dashboard assert "weekFlow.confirm()" in dashboard assert ".week-review-day.is-overloaded" in css assert ".week-review-day button { min-height:44px;" in css assert ".week-review-mode .mobile-plan-today-nav" in css def test_mobile_week_ahead_reflow_has_a_reviewed_touch_safe_flow(): index = (FRONTEND / "index.html").read_text() css = (FRONTEND / "dashboard.css").read_text() for element_id in [ "open-week-reflow", "week-reflow-review", "week-reflow-days", "week-reflow-unscheduled", "cancel-week-reflow", "apply-week-reflow", ]: assert f'id="{element_id}"' in index assert ".week-reflow-actions button" in css rule = css.split(".week-reflow-actions button", 1)[1].split("}", 1)[0] assert "min-height:48px" in rule assert ".week-reflow-review" in css assert "overflow-x:hidden" in css def test_mobile_week_ahead_has_a_touch_safe_offline_snapshot_notice_and_retry(): index = (FRONTEND / "index.html").read_text() css = (FRONTEND / "dashboard.css").read_text() assert 'id="week-offline-snapshot"' in index assert 'id="retry-week-live"' in index assert 'role="status"' in index[index.index('id="week-offline-snapshot"') - 100:index.index('id="week-offline-snapshot"') + 300] assert ".week-offline-snapshot" in css assert "#retry-week-live { min-height:44px;" in css def test_mobile_week_ahead_exposes_durable_save_and_touch_safe_conflict_choices(): index = (FRONTEND / "index.html").read_text() css = (FRONTEND / "dashboard.css").read_text() dashboard = (FRONTEND / "dashboard.js").read_text() assert 'id="week-conflict-review"' in index assert 'id="week-conflict-days"' in index assert 'id="save-merged-week"' in index assert 'id="keep-phone-week"' in index assert 'id="use-server-week"' in index assert "storage:localStorage" in dashboard assert "controller.stageDay" in CONTROLLER.read_text() assert "weekPlan.flush" in dashboard assert "week-conflict-mode" in dashboard assert "controller.chooseDay" in CONTROLLER.read_text() assert "weekPlan.saveMerged" in dashboard assert 'data-week-conflict-choice' in CONTROLLER.read_text() assert ".week-conflict-actions button { min-height:44px;" in css assert ".week-conflict-choice { min-height:44px;" in css