import json import subprocess from pathlib import Path FRONTEND = Path(__file__).parents[1] / "frontend" CONTROLLER = FRONTEND / "week-plan.js" def run_controller(scenario: str) -> dict: harness = f""" const createWeekPlan = 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_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_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_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_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 "weekWorkflow.save" in dashboard assert ".week-plan-dates button { min-height:44px;" in css assert "overflow-x:auto" in css