import json import subprocess from pathlib import Path FRONTEND = Path(__file__).parents[1] / "frontend" CONTROLLER = FRONTEND / "tomorrow-plan.js" INDEX = FRONTEND / "index.html" CSS = FRONTEND / "dashboard.css" def run_controller(scenario: str) -> dict: harness = f""" const createTomorrowPlan = require({json.dumps(str(CONTROLLER))}); (async()=>{{ {scenario} }})().catch(error=>{{console.error(error);process.exit(1);}}); """ completed = subprocess.run( ["node", "-e", harness], check=True, capture_output=True, text=True ) return json.loads(completed.stdout) def test_tomorrow_planner_restores_an_account_bound_plan_before_network_delivery(): result = run_controller(""" const values=new Map(); const storage={ getItem:key=>values.has(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-19',timeZone:()=> 'America/New_York'}; const first=createTomorrowPlan(options); first.adopt({revision:7,ids:[],capacity_minutes:null,estimates:{}}); const queued=first.stage({ids:['issue:r:3:','issue:r:2:'],capacity_minutes:120, estimates:{'issue:r:3:':45,'issue:r:2:':30}}); const restored=await createTomorrowPlan(options).load(); login='alexander'; let otherError=''; try { await createTomorrowPlan(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["requests"] == 1 assert result["otherError"] == "offline" assert result["keys"] == ["stackchain.tomorrow-sync.v1.timmy"] def test_tomorrow_planner_flushes_once_and_removes_pending_only_after_server_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 resolveRequest; let requests=0; const fetchJson=async (_url,options={})=>{ requests+=1; const body=JSON.parse(options.body); await new Promise(resolve=>{resolveRequest=()=>resolve({revision:9,...body});}); return {revision:9,...body}; }; const planner=createTomorrowPlan({storage,getLogin:()=> 'timmy',fetchJson, localDate:()=> '2026-08-19',timeZone:()=> 'UTC'}); planner.adopt({revision:8,ids:[],capacity_minutes:null,estimates:{}}); planner.stage({ids:['issue:r:4:'],capacity_minutes:60,estimates:{'issue:r:4:':30}}); const first=planner.flush(); const second=planner.flush(); await Promise.resolve(); const pendingDuring=planner.pending(); resolveRequest(); const [saved,reused]=await Promise.all([first,second]); console.log(JSON.stringify({requests,pendingDuring,saved,reused,pendingAfter:planner.pending(),keys:[...values.keys()]})); """) assert result["requests"] == 1 assert result["pendingDuring"]["ids"] == ["issue:r:4:"] assert result["saved"]["revision"] == 9 assert result["reused"]["revision"] == 9 assert result["pendingAfter"] is False assert result["keys"] == [] def test_tomorrow_planner_preserves_local_and_server_plans_on_revision_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)}; const requests=[]; const fetchJson=async (_url,options={})=>{ requests.push(options.method||'GET'); if(options.method==='PUT'){const error=new Error('changed elsewhere');error.status=409;throw error;} return {revision:12,ids:['issue:r:server:'],capacity_minutes:60,estimates:{},plan_date:'2026-08-20',timezone:'UTC'}; }; const planner=createTomorrowPlan({storage,getLogin:()=> 'timmy',fetchJson, localDate:()=> '2026-08-19',timeZone:()=> 'UTC'}); planner.adopt({revision:11,ids:[],capacity_minutes:null,estimates:{}}); planner.stage({ids:['issue:r:phone:'],capacity_minutes:90,estimates:{}}); let message=''; try { await planner.flush(); } catch(error) { message=error.message; } console.log(JSON.stringify({requests,message,pending:planner.pending(),conflict:planner.conflict()})); """) assert result["requests"] == ["PUT", "GET"] assert result["message"] == "changed elsewhere" assert result["pending"]["ids"] == ["issue:r:phone:"] assert result["conflict"]["local"]["ids"] == ["issue:r:phone:"] assert result["conflict"]["remote"]["ids"] == ["issue:r:server:"] assert result["conflict"]["remote"]["revision"] == 12 def test_tomorrow_planner_keeps_phone_plan_by_rebasing_once_onto_conflict_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 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 elsewhere');error.status=409;throw error;} if(options.method==='PUT') return {revision:13,...JSON.parse(options.body)}; return {revision:12,ids:['issue:r:server:'],capacity_minutes:60,estimates:{'issue:r:server:':30},plan_date:'2026-08-20',timezone:'UTC'}; }; const planner=createTomorrowPlan({storage,getLogin:()=> 'timmy',fetchJson, localDate:()=> '2026-08-19',timeZone:()=> 'UTC'}); planner.adopt({revision:11,ids:[],capacity_minutes:null,estimates:{}}); planner.stage({ids:['issue:r:phone:'],capacity_minutes:90,estimates:{'issue:r:phone:':45}}); try { await planner.flush(); } catch(_error) {} const saved=await planner.keepLocal(); console.log(JSON.stringify({saved,requests,pending:planner.pending(),conflict:planner.conflict(),state:planner.state()})); """) assert [request["method"] for request in result["requests"]] == ["PUT", "GET", "PUT"] assert result["requests"][2]["body"]["base_revision"] == 12 assert result["requests"][2]["body"]["ids"] == ["issue:r:phone:"] assert result["saved"]["revision"] == 13 assert result["pending"] is False assert result["conflict"] is None assert result["state"]["ids"] == ["issue:r:phone:"] def test_tomorrow_planner_uses_server_plan_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 fetchJson=async (_url,options={})=>{ requests.push(options.method||'GET'); if(options.method==='PUT'){const error=new Error('changed elsewhere');error.status=409;throw error;} return {revision:12,ids:['issue:r:server:'],capacity_minutes:60,estimates:{'issue:r:server:':30},plan_date:'2026-08-20',timezone:'UTC'}; }; const planner=createTomorrowPlan({storage,getLogin:()=> 'timmy',fetchJson, localDate:()=> '2026-08-19',timeZone:()=> 'UTC'}); planner.adopt({revision:11,ids:[],capacity_minutes:null,estimates:{}}); planner.stage({ids:['issue:r:phone:'],capacity_minutes:90,estimates:{'issue:r:phone:':45}}); try { await planner.flush(); } catch(_error) {} const adopted=planner.useRemote(); console.log(JSON.stringify({adopted,requests,pending:planner.pending(),conflict:planner.conflict(),state:planner.state()})); """) assert result["requests"] == ["PUT", "GET"] assert result["adopted"]["revision"] == 12 assert result["adopted"]["ids"] == ["issue:r:server:"] assert result["pending"] is False assert result["conflict"] is None assert result["state"] == result["adopted"] def test_tomorrow_conflict_cannot_be_read_or_resolved_after_account_switch(): 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'; const fetchJson=async (_url,options={})=>{ if(options.method==='PUT'){const error=new Error('changed elsewhere');error.status=409;throw error;} return {revision:12,ids:['issue:private:timmy:'],capacity_minutes:60,estimates:{},plan_date:'2026-08-20',timezone:'UTC'}; }; const planner=createTomorrowPlan({storage,getLogin:()=>login,fetchJson, localDate:()=> '2026-08-19',timeZone:()=> 'UTC'}); planner.adopt({revision:11,ids:[],capacity_minutes:null,estimates:{}}); planner.stage({ids:['issue:private:phone:'],capacity_minutes:90,estimates:{}}); try { await planner.flush(); } catch(_error) {} const before=planner.conflict(); login='alexander'; const after=planner.conflict(); const keep=await planner.keepLocal(); const use=planner.useRemote(); login='timmy'; const restored=planner.conflict(); console.log(JSON.stringify({before,after,keep,use,restored})); """) assert result["before"]["remote"]["ids"] == ["issue:private:timmy:"] assert result["after"] is None assert result["keep"] is False assert result["use"] is False assert result["restored"]["local"]["ids"] == ["issue:private:phone:"] def test_tomorrow_planner_loads_and_saves_independently_from_today(): result = run_controller(""" const requests=[]; const fetchJson=async (url, options={})=>{ requests.push({url,method:options.method||'GET',body:options.body ? JSON.parse(options.body) : null}); if ((options.method||'GET') === 'GET') return { revision:2, ids:['issue:r:2:'], capacity_minutes:90, estimates:{'issue:r:2:':45}, plan_date:'2026-08-20', timezone:'UTC' }; return {revision:3,...JSON.parse(options.body)}; }; const planner=createTomorrowPlan({fetchJson, localDate:()=> '2026-08-19', timeZone:()=> 'UTC'}); const loaded=await planner.load(); const saved=await planner.save({ids:['issue:r:3:'],capacity_minutes:120,estimates:{'issue:r:3:':60}}); console.log(JSON.stringify({loaded,saved,requests})); """) assert result["loaded"]["ids"] == ["issue:r:2:"] assert result["saved"]["plan_date"] == "2026-08-20" assert result["requests"] == [ {"url": "api/v1/tomorrow", "method": "GET", "body": None}, { "url": "api/v1/tomorrow", "method": "PUT", "body": { "base_revision": 2, "ids": ["issue:r:3:"], "capacity_minutes": 120, "estimates": {"issue:r:3:": 60}, "plan_date": "2026-08-20", "timezone": "UTC", }, }, ] assert all(request["url"] != "api/v1/today" for request in result["requests"]) def test_pending_tomorrow_plan_is_not_promoted_before_account_sync(): 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 date='2026-08-19'; const planner=createTomorrowPlan({storage,getLogin:()=> 'timmy',fetchJson:async(url)=>{requests.push(url);return {};}, localDate:()=> date,timeZone:()=> 'UTC'}); planner.adopt({revision:3,ids:['issue:r:2:'],capacity_minutes:60,estimates:{},plan_date:'2026-08-20',timezone:'UTC'}); planner.stage({ids:['issue:r:2:'],capacity_minutes:60,estimates:{}}); date='2026-08-22'; const promoted=await planner.promote(7); console.log(JSON.stringify({promoted,requests,pending:planner.pending()})); """) assert result["promoted"] is False assert result["requests"] == [] assert result["pending"]["ids"] == ["issue:r:2:"] def test_tomorrow_planner_promotes_overdue_plans_with_a_stable_retry_identity(): result = run_controller(""" const requests=[]; const fetchJson=async (url, options={})=>{ requests.push({url,body:options.body ? JSON.parse(options.body) : null}); return {ids:['issue:r:2:'],revision:8}; }; const before=createTomorrowPlan({fetchJson,localDate:()=> '2026-08-19',timeZone:()=> 'UTC'}); before.adopt({revision:3,ids:['issue:r:2:'],capacity_minutes:60,estimates:{},plan_date:'2026-08-20',timezone:'UTC'}); const early=await before.promote(7); const overdue=createTomorrowPlan({fetchJson,localDate:()=> '2026-08-22',timeZone:()=> 'UTC'}); overdue.adopt({revision:3,ids:['issue:r:2:'],capacity_minutes:60,estimates:{},plan_date:'2026-08-20',timezone:'UTC'}); const promoted=await overdue.promote(7); const retried=await overdue.promote(7); console.log(JSON.stringify({early,promoted,retried,requests})); """) assert result["early"] is False assert result["promoted"]["revision"] == 8 assert result["retried"]["revision"] == 8 assert result["requests"] == [ { "url": "api/v1/tomorrow/promote", "body": { "promotion_id": "tomorrow-2026-08-20-r3", "tomorrow_revision": 3, "today_revision": 7, }, }, { "url": "api/v1/tomorrow/promote", "body": { "promotion_id": "tomorrow-2026-08-20-r3", "tomorrow_revision": 3, "today_revision": 7, }, }, ] def test_tomorrow_rollover_checks_startup_reconnect_foreground_and_next_midnight_single_flight(): result = run_controller(""" const listeners={}; const timers=[]; const cleared=[]; let checks=0; const planner=createTomorrowPlan({fetchJson:async()=>({}),localDate:()=> '2026-08-20',timeZone:()=> 'UTC'}); const windowObject={addEventListener:(name,fn)=>listeners['window:'+name]=fn}; const documentObject={hidden:false,addEventListener:(name,fn)=>listeners['document:'+name]=fn}; const lifecycle=planner.startLifecycle({ windowObject,documentObject,check:async()=>{checks+=1;await Promise.resolve();return true;}, setTimer:(fn,delay)=>{timers.push({fn,delay});return timers.length;}, clearTimer:id=>cleared.push(id),nextDelay:()=>1234, }); await lifecycle.run(); const reconnect=listeners['window:online'](); const foreground=listeners['document:visibilitychange'](); await Promise.all([reconnect,foreground]); await timers[timers.length-1].fn(); console.log(JSON.stringify({checks,delays:timers.map(timer=>timer.delay),cleared})); """) assert result["checks"] == 3 assert result["delays"] == [1234, 1234, 1234] assert result["cleared"] == [1, 2] def test_tomorrow_rollover_falls_back_when_the_reported_timezone_is_invalid(): result = run_controller(""" let scheduled=null; const planner=createTomorrowPlan({fetchJson:async()=>({}),localDate:()=> '2026-08-20',timeZone:()=> 'Etc/Unknown'}); const lifecycle=planner.startLifecycle({ check:async()=>false,setTimer:(_fn,delay)=>{scheduled=delay;return 1;},clearTimer:()=>{}, }); await lifecycle.run(); console.log(JSON.stringify({scheduled:Number.isFinite(scheduled)&&scheduled>0})); """) assert result == {"scheduled": True} def test_tomorrow_planner_summarizes_empty_and_capacity_aware_plans_for_mobile_queues(): result = run_controller(""" const planner=createTomorrowPlan({fetchJson:async()=>({}),localDate:()=> '2026-08-20',timeZone:()=> 'UTC'}); const empty=planner.summary({ids:[],capacity_minutes:null,estimates:{}}); const planned=planner.summary({ ids:['issue:r:1:','issue:r:2:','issue:r:3:'], capacity_minutes:120, estimates:{'issue:r:1:':30,'issue:r:2:':45,'issue:r:3:':30} }); const unestimated=planner.summary({ids:['issue:r:4:'],capacity_minutes:null,estimates:{}}); const pending=planner.summary({ids:['issue:r:5:'],capacity_minutes:60,estimates:{'issue:r:5:':30},sync_pending:true}); console.log(JSON.stringify({empty,planned,unestimated,pending})); """) assert result == { "empty": "Nothing planned", "planned": "3 planned · 105 of 120 min", "unestimated": "1 planned", "pending": "1 planned · 30 of 60 min · sync pending", } def test_mobile_tomorrow_conflict_review_exposes_two_touch_safe_resolution_paths(): index = INDEX.read_text() css = CSS.read_text() dashboard = (FRONTEND / "dashboard.js").read_text() assert 'id="tomorrow-conflict-review"' in index assert 'id="tomorrow-conflict-phone-plan"' in index assert 'id="tomorrow-conflict-server-plan"' in index assert 'id="keep-phone-tomorrow"' in index assert 'id="use-server-tomorrow"' in index assert "Conflict · review required" in dashboard assert "if (tomorrowPlan.conflict())" in dashboard assert "tomorrowPlan.keepLocal()" in dashboard assert "tomorrowPlan.useRemote()" in dashboard assert ".tomorrow-conflict-actions button { min-height:44px;" in css def test_mobile_tomorrow_save_is_admitted_before_background_delivery(): dashboard = (FRONTEND / "dashboard.js").read_text() assert "storage:localStorage" in dashboard assert "getLogin:() => planningOwnerLogin" in dashboard assert "const staged = tomorrowPlan.stage(normalized);" in dashboard assert "if (!staged) {" in dashboard assert "Free browser storage and retry." in dashboard assert "renderTomorrowQueueSummary(staged);" in dashboard assert "Tomorrow saved on this phone · sync pending." in dashboard assert "tomorrowPlan.flush().then" in dashboard assert "syncPendingTomorrow()" in dashboard assert dashboard.count("syncPendingTomorrow();") >= 2 assert "Another device changed Tomorrow" in dashboard def test_mobile_queues_expose_tomorrow_and_keep_duplicate_header_actions_hidden(): index = INDEX.read_text() css = CSS.read_text() dashboard = (FRONTEND / "dashboard.js").read_text() assert 'data-mobile-queue="tomorrow"' in index assert 'id="mobile-tomorrow-summary"' in index assert '>Nothing planned<' in index assert "name === 'tomorrow' ? openTomorrowPlanner" in dashboard assert "tomorrowPlan.summary()" in dashboard assert "renderTomorrowQueueSummary(saved)" in dashboard assert "planTodayTrigger?.dataset.mobileQueue === 'tomorrow'" in dashboard assert "qs('#mobile-queue-sheet').showModal()" in dashboard assert ".my-work-actions { display:none; }\n .my-work-actions { display:flex; }" not in css assert ".my-work-actions { display:none; }" in css def test_tomorrow_plan_has_a_touch_safe_mobile_entry_and_reuses_the_ordered_planner(): index = INDEX.read_text() css = CSS.read_text() dashboard = (FRONTEND / "dashboard.js").read_text() assert '' in index assert ".plan-tomorrow { min-height:44px;" in css assert '' in index assert "const tomorrowPlan = createTomorrowPlan" in dashboard assert "qs('#plan-tomorrow').addEventListener('click'" in dashboard assert "planningTomorrow ? saveTomorrowPlan(plan) : saveTodayPlan(plan)" in dashboard assert "tomorrowPlan.load()" in dashboard assert "weekFlow.promote(plan)" in dashboard assert "tomorrowPlan.startLifecycle" in dashboard