import json import subprocess from pathlib import Path WEEK_CALENDAR = Path(__file__).parents[1] / "frontend" / "week-calendar.js" INDEX = Path(__file__).parents[1] / "frontend" / "index.html" DASHBOARD = Path(__file__).parents[1] / "frontend" / "dashboard.js" CSS = Path(__file__).parents[1] / "frontend" / "dashboard.css" BUNDLE = Path(__file__).parents[1] / "src" / "frontend_bundle.py" def run_node(source: str): completed = subprocess.run(["node", "-e", source], capture_output=True, text=True) assert completed.returncode == 0, completed.stderr return json.loads(completed.stdout) def test_week_calendar_builds_ordered_local_time_blocks_and_serializes_opaque_events(): plan = { "timezone": "America/New_York", "revision": 7, "days": [{ "plan_date": "2026-08-21", "ids": ["issue:stackchain/dashboard:12:", "pull:stackchain/api:7:"], "capacity_minutes": 120, "estimates": { "issue:stackchain/dashboard:12:": 45, "pull:stackchain/api:7:": 30, }, }], } items = { "issue:stackchain/dashboard:12:": { "id": 401, "title": "Plan, ship; verify", "repository": "stackchain/dashboard", "number": 12, "url": "https://forge.alexanderwhitestone.com/git/stackchain/stackchain-dashboard/issues/12", }, "pull:stackchain/api:7:": { "title": "API release", "repository": "stackchain/api", "number": 7, "url": "https://forge.alexanderwhitestone.com/git/stackchain/api/pulls/7", }, } source = f""" const calendar=require({json.dumps(str(WEEK_CALENDAR))}); const plan={json.dumps(plan)}, items={json.dumps(items)}; const blocks=calendar.buildWeekBlocks(plan, {{'2026-08-21':'09:00'}}, id=>items[id]); const text=calendar.serializeWeekCalendar(blocks, {{timezone:plan.timezone,revision:plan.revision,generatedAt:'20260820T120000Z'}}); console.log(JSON.stringify({{blocks,text}})); """ result = run_node(source) assert [(block["start_time"], block["end_time"]) for block in result["blocks"]] == [ ("09:00", "09:45"), ("09:45", "10:15") ] text = result["text"] assert "TZID:America/New_York" in text assert "DTSTART;TZID=America/New_York:20260821T090000" in text assert "DTEND;TZID=America/New_York:20260821T094500" in text assert "DTSTART;TZID=America/New_York:20260821T094500" in text assert "DTEND;TZID=America/New_York:20260821T101500" in text assert "TRANSP:OPAQUE" in text assert "UID:week-issue-stackchain-dashboard-12@stackchain" in text assert "SEQUENCE:7" in text assert "SUMMARY:Plan\\, ship\\; verify" in text unfolded = text.replace("\r\n ", "") assert "URL:https://forge.alexanderwhitestone.com/git/stackchain/stackchain-dashboard/issues/12" in unfolded def test_week_calendar_reexports_update_stable_distinct_work_events(): source = f""" const calendar=require({json.dumps(str(WEEK_CALENDAR))}); const issue={{id:'issue:stackchain/dashboard:12:',title:'Ship it',repository:'stackchain/dashboard',number:12, plan_date:'2026-08-21',start_time:'09:00',end_time:'09:30'}}; const moved={{...issue,plan_date:'2026-08-22',start_time:'14:00',end_time:'14:30'}}; const pull={{...moved,id:'pull:stackchain/dashboard:12:'}}; const first=calendar.serializeWeekCalendar([issue],{{timezone:'UTC',revision:7,generatedAt:'20260820T120000Z'}}); const second=calendar.serializeWeekCalendar([moved,pull],{{timezone:'UTC',revision:8,generatedAt:'20260821T120000Z'}}); const events=text=>text.split('BEGIN:VEVENT').slice(1).map(value=>({{ uid:value.match(/UID:([^\\r]+)/)[1],sequence:Number(value.match(/SEQUENCE:(\\d+)/)[1]) }})); console.log(JSON.stringify({{first:events(first),second:events(second)}})); """ result = run_node(source) assert result["first"] == [{ "uid": "week-issue-stackchain-dashboard-12@stackchain", "sequence": 7, }] assert result["second"] == [ {"uid": "week-issue-stackchain-dashboard-12@stackchain", "sequence": 8}, {"uid": "week-pull-stackchain-dashboard-12@stackchain", "sequence": 8}, ] def test_week_calendar_places_whole_tasks_in_imported_free_windows_and_reports_unfitted_work(): source = f""" const calendar=require({json.dumps(str(WEEK_CALENDAR))}); const plan={{days:[{{plan_date:'2026-08-21',ids:['one','two','three'],estimates:{{one:45,two:90,three:400}}}}]}}; const items={{one:{{title:'First',repository:'r',number:1}},two:{{title:'Second',repository:'r',number:2}}, three:{{title:'Too large',repository:'r',number:3}}}}; const availability=[{{plan_date:'2026-08-21',free_windows:[{{start_time:'09:00',end_time:'10:00'}},{{start_time:'11:00',end_time:'17:00'}}]}}]; const result=calendar.buildWeekSchedule(plan,{{'2026-08-21':'09:00'}},id=>items[id],null,availability); console.log(JSON.stringify(result)); """ result = run_node(source) assert [(block["id"], block["start_time"], block["end_time"]) for block in result["blocks"]] == [ ("one", "09:00", "09:45"), ("two", "11:00", "12:30"), ] assert result["blockers"] == [{ "id": "three", "title": "Too large", "plan_date": "2026-08-21", "minutes": 400 }] def test_week_calendar_honors_exact_saved_task_times_and_rejects_conflicts(): source = f""" const calendar=require({json.dumps(str(WEEK_CALENDAR))}); const items={{one:{{title:'First'}},two:{{title:'Second'}}}}; const plan={{days:[{{plan_date:'2026-08-21',ids:['one','two'],estimates:{{one:45,two:90}}, free_windows:[{{start_time:'09:00',end_time:'10:00'}},{{start_time:'13:00',end_time:'17:00'}}], start_times:{{one:'09:00',two:'14:00'}}}}]}}; const saved=calendar.buildWeekSchedule(plan,{{}},id=>items[id]); const conflict=calendar.buildWeekSchedule(plan,{{one:'14:30',two:'14:00'}},id=>items[id]); console.log(JSON.stringify({{saved,conflict}})); """ result = run_node(source) assert [(block["id"], block["start_time"], block["end_time"]) for block in result["saved"]["blocks"]] == [ ("one", "09:00", "09:45"), ("two", "14:00", "15:30") ] assert result["conflict"]["blocks"] == [] assert {blocker["id"] for blocker in result["conflict"]["blockers"]} == {"one", "two"} def test_week_calendar_excludes_unselected_items_and_downloads_when_native_share_is_unavailable(): source = f""" const calendar=require({json.dumps(str(WEEK_CALENDAR))}); const plan={{timezone:'UTC',revision:2,days:[{{plan_date:'2026-08-21',ids:['one','two'],capacity_minutes:60,estimates:{{one:20,two:25}}}}]}}; const items={{one:{{title:'Private',repository:'r',number:1,url:'https://forge/r/1'}},two:{{title:'Public',repository:'r',number:2,url:'https://forge/r/2'}}}}; const blocks=calendar.buildWeekBlocks(plan,{{'2026-08-21':'13:00'}},id=>items[id],new Set(['two'])); class FakeFile {{constructor(parts,name,options){{this.parts=parts;this.name=name;this.type=options.type;}}}} const clicks=[],revoked=[]; (async()=>{{ const result=await calendar.deliverWeekCalendar({{text:'calendar',filename:'week.ics',navigator:{{}},FileCtor:FakeFile, document:{{createElement:()=>({{click(){{clicks.push([this.download,this.href]);}}}})}}, urlApi:{{createObjectURL:()=> 'blob:week',revokeObjectURL:value=>revoked.push(value)}}}}); console.log(JSON.stringify({{blocks,result,clicks,revoked}})); }})(); """ result = run_node(source) assert [block["id"] for block in result["blocks"]] == ["two"] assert result["blocks"][0]["start_time"] == "13:00" assert result["result"] == "downloaded" assert result["clicks"] == [["week.ics", "blob:week"]] assert result["revoked"] == ["blob:week"] def test_week_calendar_handoff_is_wired_into_confirmation_with_mobile_privacy_controls(): index = INDEX.read_text() dashboard = DASHBOARD.read_text() bundle = BUNDLE.read_text() assert '' in index assert 'id="week-calendar-handoff"' in index assert 'id="week-calendar-days"' in index assert 'id="share-week-calendar"' in index assert 'id="save-week-calendar-times"' in index assert 'data-week-calendar-exact' in WEEK_CALENDAR.read_text() assert "onSave" in WEEK_CALENDAR.read_text() assert 'min-height:44px' in CSS.read_text()[CSS.read_text().index('.week-calendar-time'):] assert 'id="back-to-week-review-from-calendar"' in index assert 'id="export-saved-week-calendar"' in index assert "Add saved week to calendar" in index assert "Titles, references, dates, and times are included" in index assert "Imported busy time is used only until this page reloads" in index assert "mountWeekCalendarHandoff" in WEEK_CALENDAR.read_text() assert "wc.open(weekPlan.state())" in dashboard assert "getItem:weekFlow.i" in dashboard assert "i:id=>getItem(id)||controller.item?.(id)" in (WEEK_CALENDAR.parent / "week-plan.js").read_text() assert "#export-saved-week-calendar" in WEEK_CALENDAR.read_text() assert "focusReturn=event.currentTarget" in WEEK_CALENDAR.read_text() assert "wc.close()" in dashboard assert "StackchainWeekCalendar.setAvailabilityProvider(workflow.availability)" in (WEEK_CALENDAR.parent / "week-calendar-import.js").read_text() assert "Planning around imported busy time" in WEEK_CALENDAR.read_text() assert '"static/week-calendar.js", "static/week-calendar-import.js", "static/week-plan.js"' in bundle