import json import subprocess from pathlib import Path import pytest from tests.dashboard_bundle import dashboard START_DAY = Path(__file__).resolve().parents[1] / "frontend" / "mobile-start-day.js" def run_node(script: str) -> dict: result = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert result.returncode == 0, result.stderr return json.loads(result.stdout) def test_start_day_briefing_guides_urgent_review_before_today(): script = f""" const createStartDay = require({json.dumps(str(START_DAY))}); let counts = {{agenda:2, attention:3, update:4, filed:1, today:5}}; const opened = []; const controller = createStartDay({{ getCounts: () => counts, openQueue: name => opened.push(name), }}); const first = controller.briefing(); controller.startNext(); counts = {{agenda:0, attention:3, update:4, filed:1, today:5}}; const second = controller.briefing(); controller.startNext(); counts = {{agenda:0, attention:0, update:0, filed:0, today:5}}; const ready = controller.briefing(); controller.startNext(); process.stdout.write(JSON.stringify({{first, second, ready, opened}})); """ result = run_node(script) assert result == { "first": { "total": 10, "next": "agenda", "label": "Review Agenda", "summary": "10 items before Today · 5 planned", "phases": [ {"name": "agenda", "label": "Agenda", "count": 2}, {"name": "attention", "label": "Attention", "count": 3}, {"name": "update", "label": "Updates", "count": 4}, {"name": "filed", "label": "Filed", "count": 1}, ], }, "second": { "total": 8, "next": "attention", "label": "Review Attention", "summary": "8 items before Today · 5 planned", "phases": [ {"name": "attention", "label": "Attention", "count": 3}, {"name": "update", "label": "Updates", "count": 4}, {"name": "filed", "label": "Filed", "count": 1}, ], }, "ready": { "total": 0, "next": "today", "label": "Continue Today", "summary": "Review clear · 5 planned", "phases": [], }, "opened": ["agenda", "attention", "today"], } def test_start_day_view_renders_refreshed_phases_and_launches_primary_action(): script = f""" const createStartDay = require({json.dumps(str(START_DAY))}); const elements = {{ summary: {{textContent:''}}, phases: {{textContent:''}}, action: {{textContent:'', listeners:{{}}, addEventListener(name, fn) {{ this.listeners[name] = fn; }}}}, }}; const opened = []; let counts = {{agenda:0, attention:2, update:1, filed:0, today:3}}; const controller = createStartDay({{ getCounts: () => counts, openQueue: name => opened.push(name), elements, }}); controller.start(); const first = {{summary:elements.summary.textContent, phases:elements.phases.textContent, action:elements.action.textContent}}; elements.action.listeners.click(); counts = {{agenda:0, attention:0, update:0, filed:0, today:3}}; controller.render(); const ready = {{summary:elements.summary.textContent, phases:elements.phases.textContent, action:elements.action.textContent}}; process.stdout.write(JSON.stringify({{first, ready, opened}})); """ assert run_node(script) == { "first": { "summary": "3 items before Today · 3 planned", "phases": "Attention 2 · Updates 1", "action": "Review Attention", }, "ready": { "summary": "Review clear · 3 planned", "phases": "All urgent queues reviewed", "action": "Continue Today", }, "opened": ["attention"], } def test_prepare_today_pass_persists_per_account_and_hands_off_using_live_counts(): script = f""" const createStartDay = require({json.dumps(str(START_DAY))}); const saved = new Map(); const storage = {{ getItem: key => saved.has(key) ? saved.get(key) : null, setItem: (key, value) => saved.set(key, value), removeItem: key => saved.delete(key), }}; let login = 'timmy'; let day = '2026-08-15'; let counts = {{agenda:2, attention:1, update:1, filed:1, today:3}}; const opened = []; const handoffs = []; const controller = createStartDay({{ storage, getLogin: () => login, getDay: () => day, getCounts: () => counts, openQueue: name => opened.push(name), onHandoff: state => handoffs.push({{next:state.next, label:state.label}}), }}); const started = controller.startNext(); counts = {{agenda:0, attention:0, update:1, filed:1, today:3}}; const handed = controller.completePhase('agenda'); const resumed = createStartDay({{ storage, getLogin: () => login, getDay: () => day, getCounts: () => counts, openQueue: name => opened.push(name), }}).state(); login = 'alexander'; const isolated = createStartDay({{ storage, getLogin: () => login, getDay: () => day, getCounts: () => counts, openQueue: name => opened.push(name), }}).state(); login = 'timmy'; day = '2026-08-16'; const expired = createStartDay({{ storage, getLogin: () => login, getDay: () => day, getCounts: () => counts, openQueue: name => opened.push(name), }}).state(); day = '2026-08-15'; const finished = controller.finish(); const cleared = controller.state(); process.stdout.write(JSON.stringify({{started, handed, resumed, isolated, expired, finished, cleared, opened, handoffs}})); """ assert run_node(script) == { "started": "agenda", "handed": True, "resumed": {"active": True, "next": "update", "label": "Review Updates"}, "isolated": {"active": False, "next": "update", "label": "Review Updates"}, "expired": {"active": False, "next": "update", "label": "Review Updates"}, "finished": True, "cleared": {"active": False, "next": "update", "label": "Review Updates"}, "opened": ["agenda"], "handoffs": [{"next": "update", "label": "Review Updates"}], } def test_prepare_today_attention_hands_off_once_only_after_authoritative_refresh(): script = f""" const createStartDay = require({json.dumps(str(START_DAY))}); const saved = new Map(); const storage = {{ getItem: key => saved.has(key) ? saved.get(key) : null, setItem: (key, value) => saved.set(key, value), removeItem: key => saved.delete(key), }}; let counts = {{agenda:0, attention:1, update:2, filed:0, today:3}}; const opened = []; const handoffs = []; const controller = createStartDay({{ storage, getLogin: () => 'timmy', getDay: () => '2026-08-15', getCounts: () => counts, openQueue: name => opened.push(name), onHandoff: current => handoffs.push({{next:current.next, label:current.label}}), }}); const launchedAttention = controller.startNext(); counts = {{agenda:0, attention:0, update:2, filed:0, today:3}}; const ignoredPartial = controller.reconcile({{authoritative:false}}); const handedToUpdates = controller.reconcile({{authoritative:true}}); const duplicateRefresh = controller.reconcile({{authoritative:true}}); const launchedUpdates = controller.startNext(); counts = {{agenda:0, attention:0, update:0, filed:0, today:3}}; const handedToToday = controller.reconcile({{authoritative:true}}); const duplicateReadyRefresh = controller.reconcile({{authoritative:true}}); process.stdout.write(JSON.stringify({{ launchedAttention, ignoredPartial, handedToUpdates, duplicateRefresh, launchedUpdates, handedToToday, duplicateReadyRefresh, opened, handoffs, }})); """ assert run_node(script) == { "launchedAttention": "attention", "ignoredPartial": False, "handedToUpdates": True, "duplicateRefresh": False, "launchedUpdates": "update", "handedToToday": True, "duplicateReadyRefresh": False, "opened": ["attention", "update"], "handoffs": [ {"next": "update", "label": "Review Updates"}, {"next": "today", "label": "Continue Today"}, ], } def test_prepare_today_checkpoint_uses_the_device_local_day(): script = f""" process.env.TZ = 'Pacific/Honolulu'; const RealDate = Date; global.Date = class extends RealDate {{ constructor(...args) {{ super(...(args.length ? args : ['2026-08-16T01:00:00Z'])); }} }}; const createStartDay = require({json.dumps(str(START_DAY))}); const saved = new Map(); const storage = {{ getItem: key => saved.has(key) ? saved.get(key) : null, setItem: (key, value) => saved.set(key, value), removeItem: key => saved.delete(key), }}; createStartDay({{ storage, getLogin: () => 'timmy', getCounts: () => ({{agenda:1}}), openQueue: () => {{}}, }}).startNext(); process.stdout.write(JSON.stringify(JSON.parse(saved.get('stackchain.mobile-start-day.v1')))); """ assert run_node(script) == {"login": "timmy", "day": "2026-08-15", "phase": "agenda"} @pytest.mark.anyio async def test_dashboard_wires_thumb_safe_start_day_briefing_into_offline_mobile_bundle(): html = await dashboard() service_worker = (START_DAY.parent / "service-worker.js").read_text() assert 'class="mobile-start-day"' in html assert 'id="mobile-start-day-summary"' in html assert 'id="mobile-start-day-phases"' in html assert 'id="mobile-start-day-action"' in html assert 'id="finish-mobile-start-day"' in html assert '' in html assert "const mobileStartDay = createMobileStartDay({" in html assert "getLogin: () => confirmedOwnerLogin" in html assert "onHandoff: current =>" in html assert "mobileStartDay.completePhase('agenda')" in html assert "mobileStartDay.completePhase('update')" in html assert "mobileStartDay.completePhase('filed')" in html assert "mobileStartDay.reconcile({authoritative:authoritativeMyWorkRefresh})" in html assert "mobileStartDay.finish()" in html assert "openQueue: name =>" in html assert "if (sheet.open) sheet.close();" in html assert "name === 'find' ? qs('#find-work').click() : mobileQueueLauncher.open(name)" in html assert "mobileStartDay.render();" in html assert ".mobile-start-day-action { width:100%; min-height:48px;" in html assert ".mobile-start-day-finish { min-height:44px;" in html assert "max-width:100%; overflow-wrap:anywhere;" in html assert "BASE + 'static/mobile-start-day.js'" in service_worker assert "stackchain-dashboard-shell-v114" in service_worker