stackchain-dashboard/tests/test_week_plan_frontend.py
timmy 449ab78991
All checks were successful
CI / lint (pull_request) Successful in 3m58s
CI / build-release (pull_request) Successful in 7s
CI / browser-journey (pull_request) Successful in 5m20s
CI / release-candidate (pull_request) Has been skipped
feat: unplan Week Ahead work with undo (Closes #1240)
2026-08-21 22:45:15 +00:00

1460 lines
82 KiB
Python

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_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_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_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_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_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;},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']) if(!elements.has(selector))elements.set(selector,makeElement());
let writes=0,opened=0,reviewMode=false;
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,
move:()=>true,flush:async()=>{writes+=1;}};
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,
todayWork:{replace:()=>{},replacePlanning:()=>{}},refresh:()=>{},warm:()=>{}});
await workflow.open({disabled:false});
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:reviewDays.innerHTML,status:elements.get('#week-review-status').textContent,
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 "data-week-move" not in result["markup"]
assert result["status"] == "Week Ahead overview · no changes made."
assert result["confirmHidden"] is True
assert result["editWeekHidden"] is False
assert result["editWeekLabel"] == "Edit week"
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
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 "<code>issue:stackchain/dashboard:42:</code>" 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 '<script src="static/week-plan.js"></script>' 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