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_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_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:{}}); console.log(JSON.stringify({empty,planned,unestimated})); """) assert result == { "empty": "Nothing planned", "planned": "3 planned ยท 105 of 120 min", "unestimated": "1 planned", } 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 "tomorrowPlan.promote(plan.revision)" in dashboard assert "tomorrowPlan.startLifecycle" in dashboard