import json import subprocess from pathlib import Path import pytest from tests.dashboard_bundle import dashboard DOCK = Path(__file__).resolve().parents[1] / "frontend" / "mobile-task-dock.js" ENTRY = Path(__file__).resolve().parents[1] / "frontend" / "mobile-work-entry.js" QUEUE_LAUNCHER = Path(__file__).resolve().parents[1] / "frontend" / "mobile-queue-launcher.js" QUEUE_PRIORITY = Path(__file__).resolve().parents[1] / "frontend" / "mobile-queue-priority.js" TIMER = Path(__file__).resolve().parents[1] / "frontend" / "today-timer.js" def test_mobile_work_entry_prioritizes_continue_resume_start_plan_then_find(): script = f""" const createEntry = require({json.dumps(str(ENTRY))}); const state = {{active:true, resumable:true, today:2, eligible:3}}; const calls = []; const entry = createEntry({{ isTodayActive: () => state.active, isTodayResumable: () => state.resumable, getTodayCount: () => state.today, getEligibleCount: () => state.eligible, continueToday: () => calls.push('continue'), resumeToday: () => calls.push('resume'), startToday: () => calls.push('start'), planToday: () => calls.push('plan'), findWork: () => calls.push('find'), }}); const modes = []; modes.push(entry.open()); state.active = false; modes.push(entry.open()); state.resumable = false; modes.push(entry.open()); state.today = 0; modes.push(entry.open()); state.eligible = 0; modes.push(entry.open()); process.stdout.write(JSON.stringify({{modes, calls}})); """ result = subprocess.run( ["node", "-e", script], capture_output=True, text=True ) assert result.returncode == 0, result.stderr assert json.loads(result.stdout) == { "modes": ["continue", "resume", "start", "plan", "find"], "calls": ["continue", "resume", "start", "plan", "find"], } def test_mobile_queue_priority_persists_complete_account_scoped_routine_order(): script = f""" const createPriority = require({json.dumps(str(QUEUE_PRIORITY))}); const values = new Map(); const storage = {{ getItem:key => values.has(key) ? values.get(key) : null, setItem:(key, value) => values.set(key, value), removeItem:key => values.delete(key), }}; let login = 'alice'; const priority = createPriority({{storage, getLogin:() => login}}); const original = priority.getOrder(); priority.move('following', -1); priority.move('following', -1); priority.move('following', -1); const alice = priority.getOrder(); login = 'bob'; const bob = priority.getOrder(); login = ''; const anonymous = priority.getOrder(); login = 'alice'; priority.reset(); process.stdout.write(JSON.stringify({{ original, alice, bob, anonymous, reset:priority.getOrder(), keys:Array.from(values.keys()), }})); """ result = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert result.returncode == 0, result.stderr default = ["attention", "today", "update", "agenda", "following", "authored", "filed", "later", "draft"] assert json.loads(result.stdout) == { "original": default, "alice": ["attention", "following", "today", "update", "agenda", "authored", "filed", "later", "draft"], "bob": default, "anonymous": default, "reset": default, "keys": [], } def test_mobile_queue_priority_hydrates_syncs_and_preserves_local_order_on_conflict(): script = f""" const createPriority = require({json.dumps(str(QUEUE_PRIORITY))}); const values = new Map(); const defaults = ['attention','today','update','agenda','following','authored','filed','later','draft']; const remote = defaults.slice(); remote.splice(remote.indexOf('following'), 1); remote.splice(1, 0, 'following'); let server = {{revision:2, order:remote.slice()}}; let conflict = false; const calls = []; const fetchJson = async (url, init={{}}) => {{ calls.push([url, init.method || 'GET']); if (!init.method) return JSON.parse(JSON.stringify(server)); const payload = JSON.parse(init.body); if (conflict) {{ const error = new Error('conflict'); error.status=409; error.payload={{detail:{{snapshot:JSON.parse(JSON.stringify(server))}}}}; throw error; }} server={{revision:payload.revision + 1, order:payload.order.slice()}}; return JSON.parse(JSON.stringify(server)); }}; (async () => {{ const priority=createPriority({{ storage:{{getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)}}, getLogin:()=>'alice', fetchJson, }}); await priority.load(); const hydrated=priority.getOrder(); priority.move('authored', -1); const local=priority.getOrder(); conflict=true; server={{revision:3, order:defaults.slice()}}; await priority.sync(); const conflicted=priority.state(); conflict=false; await priority.useLocal(); process.stdout.write(JSON.stringify({{hydrated,local,conflicted,settled:priority.state(),server,calls}})); }})().catch(error => {{ console.error(error); process.exit(1); }}); """ result = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert result.returncode == 0, result.stderr payload = json.loads(result.stdout) assert payload["hydrated"][1] == "following" assert payload["local"] == payload["conflicted"]["order"] assert payload["conflicted"]["status"] == "conflict" assert payload["conflicted"]["remote"] == { "revision": 3, "order": ["attention", "today", "update", "agenda", "following", "authored", "filed", "later", "draft"], } assert payload["settled"]["status"] == "ready" assert payload["server"]["revision"] == 4 assert payload["server"]["order"] == payload["local"] assert payload["calls"] == [ ["api/v1/queue-priority", "GET"], ["api/v1/queue-priority", "PUT"], ["api/v1/queue-priority", "PUT"], ] def test_mobile_queue_priority_serializes_rapid_edits_and_publishes_latest_order(): script = f""" const createPriority = require({json.dumps(str(QUEUE_PRIORITY))}); const values = new Map(); const pending=[]; const payloads=[]; const fetchJson = async (_url, init) => new Promise(resolve => {{ const payload=JSON.parse(init.body); payloads.push(payload); pending.push(() => resolve({{revision:payload.revision+1,order:payload.order}})); }}); (async () => {{ let priority; priority=createPriority({{ storage:{{getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)}}, getLogin:()=>'alice', fetchJson, onChange:()=>{{ void priority.sync(); }}, }}); priority.move('following', -1); priority.move('following', -1); await new Promise(resolve => setImmediate(resolve)); const firstPending=pending.length; pending.shift()(); await new Promise(resolve => setImmediate(resolve)); const secondPending=pending.length; pending.shift()(); await new Promise(resolve => setImmediate(resolve)); process.stdout.write(JSON.stringify({{firstPending,secondPending,payloads,state:priority.state()}})); }})().catch(error=>{{console.error(error);process.exit(1);}}); """ result = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert result.returncode == 0, result.stderr payload = json.loads(result.stdout) assert payload["firstPending"] == 1 assert payload["secondPending"] == 1 assert len(payload["payloads"]) == 2 assert payload["payloads"][0]["revision"] == 0 assert payload["payloads"][1]["revision"] == 1 assert payload["payloads"][1]["order"] == payload["state"]["order"] assert payload["state"]["revision"] == 2 assert payload["state"]["status"] == "ready" def test_mobile_queue_priority_discards_in_flight_results_after_account_switch(): script = f""" const createPriority = require({json.dumps(str(QUEUE_PRIORITY))}); const values = new Map(); let login='alice'; let rejectLoad; const fetchJson = () => new Promise((_resolve, reject) => {{ rejectLoad=reject; }}); (async () => {{ const priority=createPriority({{ storage:{{getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)}}, getLogin:()=>login, fetchJson, }}); const loading=priority.load(); login='bob'; rejectLoad(new Error('alice offline')); await loading; process.stdout.write(JSON.stringify({{state:priority.state(),keys:Array.from(values.keys())}})); }})().catch(error=>{{console.error(error);process.exit(1);}}); """ result = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert result.returncode == 0, result.stderr payload = json.loads(result.stdout) assert payload["state"]["status"] == "ready" assert payload["state"]["order"] == [ "attention", "today", "update", "agenda", "following", "authored", "filed", "later", "draft" ] assert payload["keys"] == [] def test_mobile_queue_priority_renders_keyboard_controls_and_updates_immediately(): script = f""" const createPriority = require({json.dumps(str(QUEUE_PRIORITY))}); class Element {{ constructor(tag='div') {{ this.tag=tag; this.children=[]; this.listeners={{}}; this.attributes={{}}; this.disabled=false; this.textContent=''; }} append(...items) {{ this.children.push(...items); }} replaceChildren(...items) {{ this.children=[...items]; }} addEventListener(name, callback) {{ this.listeners[name]=callback; }} setAttribute(name, value) {{ this.attributes[name]=value; }} click() {{ this.listeners.click?.(); }} }} const values = new Map(); const list = new Element(); const resetButton = new Element('button'); const status = new Element(); const priority = createPriority({{ storage:{{getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)}}, getLogin:()=>'alice', document:{{createElement:tag=>new Element(tag)}}, list, resetButton, status, labels:{{attention:'Attention',today:'Today',update:'Updates',agenda:'Agenda',following:'Following',authored:'My PRs',filed:'Filed',later:'Later',draft:'Drafts'}}, }}); priority.start(); for (let index=0; index<3; index += 1) {{ const row = list.children.find(item => item.attributes['data-queue-priority'] === 'following'); row.children[1].children[0].click(); }} const following = list.children.find(item => item.attributes['data-queue-priority'] === 'following'); process.stdout.write(JSON.stringify({{ order:list.children.map(item => item.attributes['data-queue-priority']), earlierLabel:following.children[1].children[0].attributes['aria-label'], laterLabel:following.children[1].children[1].attributes['aria-label'], status:status.textContent, }})); """ result = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert result.returncode == 0, result.stderr assert json.loads(result.stdout) == { "order": ["attention", "following", "today", "update", "agenda", "authored", "filed", "later", "draft"], "earlierLabel": "Move Following earlier", "laterLabel": "Move Following later", "status": "Sync pending.", } def test_mobile_work_entry_preserves_active_today_then_launches_highest_priority_queue(): script = f""" const createEntry = require({json.dumps(str(ENTRY))}); const calls = []; const state = {{active:true, today:2, resumable:true, eligible:3, next:{{name:'attention'}}}}; const entry = createEntry({{ isTodayActive: () => state.active, isTodayResumable: () => state.resumable, getTodayCount: () => state.today, getEligibleCount: () => state.eligible, queueLauncher: {{recommend: () => state.next, open: name => calls.push('queue:' + name)}}, continueToday: () => calls.push('continue'), resumeToday: () => calls.push('resume'), startToday: () => calls.push('start'), planToday: () => calls.push('plan'), findWork: () => calls.push('find'), }}); const modes = [entry.open()]; state.active = false; modes.push(entry.open()); state.next = {{name:'today'}}; modes.push(entry.open()); state.today = 0; state.eligible = 0; state.next = {{name:'update'}}; modes.push(entry.open()); state.next = {{name:'agenda'}}; modes.push(entry.open()); state.next = {{name:'later'}}; modes.push(entry.open()); state.next = {{name:'draft'}}; modes.push(entry.open()); state.next = {{name:'find'}}; modes.push(entry.open()); process.stdout.write(JSON.stringify({{modes, calls}})); """ result = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert result.returncode == 0, result.stderr assert json.loads(result.stdout) == { "modes": ["continue", "attention", "resume", "update", "agenda", "later", "draft", "find"], "calls": ["continue", "queue:attention", "resume", "queue:update", "queue:agenda", "queue:later", "queue:draft", "find"], } def test_mobile_work_entry_opens_prepare_today_before_direct_queue_work(): script = f""" const createEntry = require({json.dumps(str(ENTRY))}); const calls = []; const state = {{active:true, preparation:{{active:false, next:'attention'}}}}; const entry = createEntry({{ isTodayActive: () => state.active, isTodayResumable: () => false, getTodayCount: () => 0, getEligibleCount: () => 3, getPreparationState: () => state.preparation, queueLauncher: {{recommend: () => ({{name:'attention'}}), open: name => calls.push('queue:' + name)}}, continueToday: () => calls.push('continue'), prepareToday: () => calls.push('prepare'), planToday: () => calls.push('plan'), findWork: () => calls.push('find'), }}); const modes = [entry.open()]; state.active = false; modes.push(entry.open()); state.preparation.active = true; modes.push(entry.open()); process.stdout.write(JSON.stringify({{modes, calls}})); """ result = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert result.returncode == 0, result.stderr assert json.loads(result.stdout) == { "modes": ["continue", "prepare", "prepare-resume"], "calls": ["continue", "prepare", "prepare"], } def test_mobile_work_entry_plans_eligible_work_before_find_fallback(): script = f""" const createEntry = require({json.dumps(str(ENTRY))}); const calls = []; const entry = createEntry({{ isTodayActive: () => false, isTodayResumable: () => false, getTodayCount: () => 0, getEligibleCount: () => 2, queueLauncher: {{recommend: () => ({{name:'find'}}), open: () => {{}}}}, continueToday: () => {{}}, resumeToday: () => {{}}, startToday: () => {{}}, planToday: () => calls.push('plan'), findWork: () => calls.push('find'), }}); process.stdout.write(JSON.stringify({{mode:entry.open(), calls}})); """ result = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert result.returncode == 0, result.stderr assert json.loads(result.stdout) == {"mode": "plan", "calls": ["plan"]} def test_mobile_work_entry_guides_an_empty_first_workspace_before_find(): script = f""" const createEntry = require({json.dumps(str(ENTRY))}); const calls = []; let activationRequired = true; const entry = createEntry({{ isTodayActive: () => false, isTodayResumable: () => false, getTodayCount: () => 0, getEligibleCount: () => 0, shouldActivate: () => activationRequired, queueLauncher: {{recommend: () => ({{name:'find'}}), open: () => {{}}}}, openActivation: () => calls.push('activate'), findWork: () => calls.push('find'), }}); const modes = [entry.open()]; activationRequired = false; modes.push(entry.open()); process.stdout.write(JSON.stringify({{modes, calls}})); """ result = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert result.returncode == 0, result.stderr assert json.loads(result.stdout) == { "modes": ["activate", "find"], "calls": ["activate", "find"], } def test_mobile_task_dock_routes_actions_hides_for_overlays_and_restores_focus(): script = f""" const createDock = require({json.dumps(str(DOCK))}); class FakeElement {{ constructor() {{ this.listeners = {{}}; this.attributes = {{}}; this.classList = {{ values:new Set(), contains:value => this.classList.values.has(value) }}; this.hidden = false; this.focuses = 0; }} addEventListener(name, callback) {{ this.listeners[name] = callback; }} click() {{ this.listeners.click({{currentTarget:this}}); }} setAttribute(name, value) {{ this.attributes[name] = value; }} removeAttribute(name) {{ delete this.attributes[name]; }} focus() {{ this.focuses += 1; }} }} const nav = new FakeElement(); const sessionHud = new FakeElement(); const buttons = Object.fromEntries(['work','attention','find','new','search','drafts'].map(name => [name, new FakeElement()])); buttons.attention.hidden = true; const overlay = new FakeElement(); const calls = []; let observerCallback; const dock = createDock({{ nav, sessionHud, buttons, overlays:[overlay], actions: Object.fromEntries(Object.keys(buttons).map(name => [name, () => calls.push(name)])), observe(callback) {{ observerCallback = callback; return {{disconnect() {{}}}}; }}, }}); dock.start(); buttons.work.click(); buttons.find.click(); overlay.classList.values.add('open'); observerCallback(); const hiddenWhileOpen = nav.hidden; const hudHiddenWhileOpen = sessionHud.attributes['data-overlay-hidden']; overlay.classList.values.delete('open'); observerCallback(); buttons.drafts.click(); process.stdout.write(JSON.stringify({{ calls, hiddenWhileOpen, hiddenAfterClose:nav.hidden, hudHiddenWhileOpen, hudHiddenAfterClose:sessionHud.attributes['data-overlay-hidden'] || null, attentionHidden:buttons.attention.hidden, findFocuses:buttons.find.focuses, current:Object.fromEntries(Object.entries(buttons).map(([name, button]) => [name, button.attributes['aria-current'] || null])), }})); """ result = subprocess.run( ["node", "-e", script], capture_output=True, text=True ) assert result.returncode == 0, result.stderr assert json.loads(result.stdout) == { "calls": ["work", "find", "drafts"], "hiddenWhileOpen": True, "hiddenAfterClose": False, "hudHiddenWhileOpen": "true", "hudHiddenAfterClose": None, "attentionHidden": True, "findFocuses": 1, "current": { "work": None, "attention": None, "find": None, "new": None, "search": None, "drafts": "page", }, } def test_mobile_task_dock_pauses_before_queues_and_distinguishes_close_from_selection(): script = f""" const createDock = require({json.dumps(str(DOCK))}); class FakeElement {{ constructor() {{ this.listeners = {{}}; this.open = false; this.attributes = {{}}; }} addEventListener(name, callback) {{ this.listeners[name] = callback; }} setAttribute(name, value) {{ this.attributes[name] = value; }} removeAttribute(name) {{ delete this.attributes[name]; }} showModal() {{ this.open = true; }} close() {{ this.open = false; this.listeners.close?.(); }} focus() {{}} }} const nav = new FakeElement(); nav.hidden = false; const queues = new FakeElement(); const close = new FakeElement(); const row = new FakeElement(); const button = new FakeElement(); const calls = []; const dock = createDock({{ nav, buttons:{{queues:button}}, queueSheet:queues, queueClose:close, queueRows:{{update:row}}, overlays:[], detour:()=>({{beginDetour:reason=>calls.push('pause:' + reason), finishDetour:()=>calls.push('return')}}), onSelectQueue:name=>calls.push('select:' + name), observe:()=>({{disconnect(){{}}}}), }}); dock.start(); button.listeners.click({{currentTarget:button}}); close.listeners.click(); button.listeners.click({{currentTarget:button}}); row.listeners.click(); process.stdout.write(JSON.stringify({{calls, open:queues.open}})); """ result = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert result.returncode == 0, result.stderr assert json.loads(result.stdout) == { "calls": ["pause:queues", "return", "pause:queues", "select:update"], "open": False, } @pytest.mark.anyio async def test_mobile_queue_sheet_prioritizes_next_active_and_planning_without_duplicate_rows(): html = await dashboard() assert 'aria-labelledby="mobile-queue-next-heading"' in html assert 'id="mobile-queue-next-action"' in html assert 'aria-labelledby="mobile-queue-active-heading"' in html assert 'id="mobile-queue-active-list"' in html assert 'aria-labelledby="mobile-queue-planning-heading"' in html assert 'id="mobile-queue-planning-list"' in html assert '
' in html assert "getRoutineOrder: () => mobileQueuePriority?.getOrder()" in html assert "createMobileQueuePriority({" in html assert "getLogin: () => confirmedOwnerLogin" in html assert "mobileQueuePriority.render();\n renderMobileQueuePresentation();" in html assert ".mobile-queue-priority-controls button { min-height:44px;" in html @pytest.mark.anyio async def test_mobile_queue_priority_wires_cross_device_hydration_and_explicit_conflict_actions(): html = await dashboard() assert 'id="mobile-queue-priority-conflict"' in html assert 'id="keep-local-mobile-queue-priority"' in html assert 'id="use-remote-mobile-queue-priority"' in html assert "fetchJson: fetchReviewJson" in html assert "void mobileQueuePriority.load();" in html assert "void mobileQueuePriority.sync();" in html assert "keepLocalButton: qs('#keep-local-mobile-queue-priority')" in html assert "useRemoteButton: qs('#use-remote-mobile-queue-priority')" in html assert "error.payload = payload;" in html @pytest.mark.anyio async def test_adaptive_mobile_queue_wiring_recommends_again_when_connectivity_changes(): html = await dashboard() assert "let offlineWorkMode = false;" in html assert "isOnline: () => !offlineWorkMode" in html assert ( "offlineWorkMode = value;\n" " renderMobileQueuePresentation();" ) in html @pytest.mark.anyio async def test_following_hydration_refreshes_the_persistent_mobile_queue_census(): html = await dashboard() assert ( "preparationItems.following = items.filter(item => item.has_unseen_change === true);\n" " renderMobileQueuePresentation();\n" " mobileTaskDock.updateQueues(queueCounts);\n" " mobileStartDay.render();" ) in html assert ( "queueCounts.followingUnavailable = status === 'error';\n" " renderMobileQueuePresentation();\n" " mobileTaskDock.updateQueues(queueCounts);\n" " mobileStartDay.render();" ) in html @pytest.mark.anyio async def test_my_work_refresh_preserves_the_hydrated_following_queue(): html = await dashboard() assert "counts.following = queueCounts.following;" in html assert "counts.followingUnavailable = queueCounts.followingUnavailable;" in html assert "following:preparationItems.following || []," in html @pytest.mark.anyio async def test_find_and_queue_detours_are_visible_and_wired_to_today_timing(): html = await dashboard() assert html.count('data-today-detour role="status"') == 6 assert html.count('data-return-from-detour type="button"') == 6 assert "detour:() => timerView" in html timer_source = TIMER.read_text() assert "createTodayDetourInterruption" in timer_source assert "queryAll('#find-work-sheet')" in timer_source assert '.today-detour-interruption button { min-height:44px;' in html def test_mobile_task_dock_programmatically_selects_destination_without_running_action(): script = f""" const createDock = require({json.dumps(str(DOCK))}); const calls = []; const makeButton = () => ({{ attributes: {{}}, setAttribute(name, value) {{ this.attributes[name] = value; }}, removeAttribute(name) {{ delete this.attributes[name]; }}, }}); const buttons = {{work:makeButton(), drafts:makeButton()}}; const dock = createDock({{ nav:{{}}, buttons, actions:{{work:()=>calls.push('work'), drafts:()=>calls.push('drafts')}}, }}); const selected = dock.select('drafts'); process.stdout.write(JSON.stringify({{ selected, calls, work:buttons.work.attributes['aria-current'] || null, drafts:buttons.drafts.attributes['aria-current'] || null, }})); """ result = subprocess.run( ["node", "-e", script], capture_output=True, text=True ) assert result.returncode == 0, result.stderr assert json.loads(result.stdout) == { "selected": True, "calls": [], "work": None, "drafts": "page", } def test_mobile_task_dock_keeps_today_label_separate_from_attention_count(): script = f""" const createDock = require({json.dumps(str(DOCK))}); const work = {{ attributes: {{}}, setAttribute(name, value) {{ this.attributes[name] = value; }} }}; const workLabel = {{ textContent:'' }}; const dock = createDock({{ nav:{{}}, buttons:{{work}}, workLabel }}); dock.updateWork('continue', 3); const continuing = {{ text:workLabel.textContent, label:work.attributes['aria-label'] }}; dock.updateWork('resume', 0); const resuming = {{ text:workLabel.textContent, label:work.attributes['aria-label'] }}; dock.updateWork('start', 0); const starting = {{ text:workLabel.textContent, label:work.attributes['aria-label'] }}; dock.updateWork('plan', 0); const planning = {{ text:workLabel.textContent, label:work.attributes['aria-label'] }}; dock.updateWork('find', 0); process.stdout.write(JSON.stringify({{ continuing, resuming, starting, planning, finding:{{ text:workLabel.textContent, label:work.attributes['aria-label'] }}, }})); """ result = subprocess.run( ["node", "-e", script], capture_output=True, text=True ) assert result.returncode == 0, result.stderr assert json.loads(result.stdout) == { "continuing": { "text": "Continue", "label": "Continue Today", }, "resuming": {"text": "Resume", "label": "Resume Today"}, "starting": {"text": "Start", "label": "Start Today"}, "planning": {"text": "Plan", "label": "Plan Today"}, "finding": {"text": "Find", "label": "Find work"}, } def test_mobile_today_hud_completes_current_item_and_labels_final_item_for_recap(): script = f""" const createView = require({json.dumps(str(TIMER))}).createView; class FakeElement {{ constructor() {{ this.listeners = {{}}; this.hidden = true; this.textContent = ''; this.attributes = {{}}; }} addEventListener(name, callback) {{ this.listeners[name] = callback; }} click() {{ return this.listeners.click?.(); }} setAttribute(name, value) {{ this.attributes[name] = value; }} }} const hud = new FakeElement(); const open = new FakeElement(); const toggle = new FakeElement(); const complete = new FakeElement(); const progress = new FakeElement(); const elements = {{ '[data-mobile-today-hud]':[hud], '[data-mobile-today-open]':[open], '[data-mobile-today-toggle]':[toggle], '[data-mobile-today-complete]':[complete], '[data-work-session-progress]':[progress], '[data-work-session-adjust-plan]':[], '[data-work-session-timer-toggle]':[toggle], }}; let active = true; const snapshot = {{identity:'issue:r:1:', elapsed_ms:0, running:true}}; const completed = []; const view = createView({{ timer:{{snapshot:()=>snapshot,totalElapsed:()=>0,pause:()=>true,resume:()=>true}}, isActive:()=>active, queryAll:selector=>elements[selector] || [], formatEstimate:value=>value + 'm', getItem:()=>({{title:'Ship mobile flow'}}), onComplete:identity=>{{ completed.push(identity); return false; }}, }}); view.update({{index:1,total:2}}, null); const next = {{hidden:complete.hidden,label:complete.textContent,aria:complete.attributes['aria-label']}}; complete.click(); const preservedAfterFailure = {{hudHidden:hud.hidden,identity:snapshot.identity}}; view.update({{index:2,total:2}}, null); const recap = {{hidden:complete.hidden,label:complete.textContent}}; active = false; view.render(); process.stdout.write(JSON.stringify({{next,recap,completed,preservedAfterFailure,hiddenWhenInactive:complete.hidden}})); """ result = subprocess.run( ["node", "-e", script], capture_output=True, text=True ) assert result.returncode == 0, result.stderr assert json.loads(result.stdout) == { "next": { "hidden": False, "label": "Done & next", "aria": "Complete Ship mobile flow and open next Today item", }, "recap": {"hidden": False, "label": "Done & recap"}, "completed": ["issue:r:1:"], "preservedAfterFailure": {"hudHidden": False, "identity": "issue:r:1:"}, "hiddenWhenInactive": True, } def test_mobile_task_dock_exposes_independent_attention_action_and_six_column_state(): script = f""" const createDock = require({json.dumps(str(DOCK))}); const nav = {{ attributes: {{}}, setAttribute(name, value) {{ this.attributes[name] = value; }}, removeAttribute(name) {{ delete this.attributes[name]; }} }}; const attention = {{ attributes: {{}}, hidden:true, setAttribute(name, value) {{ this.attributes[name] = value; }} }}; const badge = {{ textContent:'', hidden:true }}; const dock = createDock({{ nav, buttons:{{attention}}, attentionBadge:badge }}); dock.updateAttention(3); const pending = {{ count:badge.textContent, badgeHidden:badge.hidden, actionHidden:attention.hidden, label:attention.attributes['aria-label'], layout:nav.attributes['data-attention'] }}; dock.updateAttention(0); process.stdout.write(JSON.stringify({{ pending, empty:{{ count:badge.textContent, badgeHidden:badge.hidden, actionHidden:attention.hidden, label:attention.attributes['aria-label'], layout:nav.attributes['data-attention'] || null }}, }})); """ result = subprocess.run( ["node", "-e", script], capture_output=True, text=True ) assert result.returncode == 0, result.stderr assert json.loads(result.stdout) == { "pending": {"count": "3", "badgeHidden": False, "actionHidden": False, "label": "Attention, 3 items", "layout": "true"}, "empty": {"count": "0", "badgeHidden": True, "actionHidden": True, "label": "Attention, 0 items", "layout": None}, } def test_mobile_task_dock_opens_stable_queue_switcher_and_routes_each_queue(): script = f""" const createDock = require({json.dumps(str(DOCK))}); class FakeElement {{ constructor() {{ this.listeners = {{}}; this.attributes = {{}}; this.hidden = false; this.textContent = ''; this.focuses = 0; this.classList = {{contains:()=>false}}; }} addEventListener(name, callback) {{ this.listeners[name] = callback; }} click() {{ return this.listeners.click?.({{currentTarget:this}}); }} setAttribute(name, value) {{ this.attributes[name] = value; }} removeAttribute(name) {{ delete this.attributes[name]; }} focus() {{ this.focuses += 1; }} }} const nav = new FakeElement(); const queues = new FakeElement(); const sheet = new FakeElement(); sheet.open = false; sheet.showModal = function () {{ this.open = true; }}; sheet.close = function () {{ this.open = false; this.listeners.close?.(); }}; const close = new FakeElement(); const badge = new FakeElement(); const deadline = new FakeElement(); const rows = Object.fromEntries(['today','agenda','delivery','gate','attention','update','filed','later','draft','recaps'].map(name => [name, new FakeElement()])); const counts = Object.fromEntries(Object.keys(rows).map(name => [name, new FakeElement()])); const selected = []; const utilities = []; const dock = createDock({{ nav, buttons:{{queues}}, actions:{{queues:()=>{{}}}}, overlays:[sheet], queueSheet:sheet, queueClose:close, queueRows:rows, queueCounts:counts, queueBadge:badge, deadlineBadge:deadline, onSelectQueue:(name,row)=>name === 'recaps' ? utilities.push(row === rows.recaps ? 'recaps:trigger' : 'recaps:missing-trigger') : selected.push(name), observe() {{}}, }}); dock.start(); dock.updateQueues({{today:2, agenda:5, delivery:1, gate:2, attention:1, update:5, filed:2, later:3, draft:4}}); queues.click(); const opened = sheet.open; rows.update.click(); const closedAfterSelect = !sheet.open; queues.click(); rows.recaps.click(); const closedAfterUtility = !sheet.open; queues.click(); close.click(); const populated = {{ badge:badge.textContent, badgeHidden:badge.hidden, deadline:deadline.textContent, deadlineHidden:deadline.hidden, agendaDue:rows.agenda.attributes['data-deadlines'] || null, badgeLabel:queues.attributes['aria-label'], }}; dock.updateQueues({{today:0, agenda:0, attention:0, update:7, later:0, draft:0}}); const updatesOnly = {{ badge:badge.textContent, badgeHidden:badge.hidden, deadlineHidden:deadline.hidden, badgeLabel:queues.attributes['aria-label'], }}; dock.updateQueues({{today:0, agenda:0, attention:0, update:0, later:0, draft:0}}); process.stdout.write(JSON.stringify({{ opened, closedAfterSelect, closedAfterUtility, selected, utilities, badge:badge.textContent, populated, updatesOnly, clearedBadgeHidden:badge.hidden, clearedDeadlineHidden:deadline.hidden, clearedAgendaDue:rows.agenda.attributes['data-deadlines'] || null, clearedBadgeLabel:queues.attributes['aria-label'], counts:Object.fromEntries(Object.entries(counts).map(([name, node]) => [name, node.textContent])), updateLabel:rows.update.attributes['aria-label'], queueFocuses:queues.focuses, columnState:nav.attributes['data-attention'] || null, }})); """ result = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert result.returncode == 0, result.stderr assert json.loads(result.stdout) == { "opened": True, "closedAfterSelect": True, "closedAfterUtility": True, "selected": ["update"], "utilities": ["recaps:trigger"], "badge": "0 active", "populated": { "badge": "8 active", "badgeHidden": False, "badgeLabel": "Queues: Today 2, Agenda 5 due, Delivery 1, Human Gates 2, Attention 1, Updates 5, Following 0, Filed 2, My PRs 0, Later 3, Drafts 4; 8 active queues", "deadline": "5 due", "deadlineHidden": False, "agendaDue": "true", }, "updatesOnly": { "badge": "1 active", "badgeHidden": False, "deadlineHidden": True, "badgeLabel": "Queues: Today 0, Agenda 0 due, Delivery 0, Human Gates 0, Attention 0, Updates 7, Following 0, Filed 0, My PRs 0, Later 0, Drafts 0; 1 active queue", }, "clearedBadgeHidden": True, "clearedDeadlineHidden": True, "clearedAgendaDue": None, "clearedBadgeLabel": "Queues: no active queues; no upcoming deadlines", "counts": {"today": "0", "agenda": "0", "delivery": "0", "gate": "0", "attention": "0", "update": "0", "filed": "0", "later": "0", "draft": "0", "recaps": "0"}, "updateLabel": "Updates, 0 unread conversations", "queueFocuses": 3, "columnState": None, } def test_mobile_task_dock_counts_following_and_authored_work_truthfully(): script = f""" const createDock = require({json.dumps(str(DOCK))}); const node = () => ({{ textContent:'', hidden:false, attributes:{{}}, setAttribute(name, value) {{ this.attributes[name] = value; }}, removeAttribute(name) {{ delete this.attributes[name]; }}, }}); const queues = node(); const badge = node(); const deadline = node(); const following = node(); const authored = node(); const dock = createDock({{ nav:node(), buttons:{{queues}}, queueBadge:badge, deadlineBadge:deadline, queueRows:{{following, authored}}, queueCounts:{{following:node(), authored:node()}}, }}); dock.updateQueues({{following:3, authored:2}}); const active = {{ badge:badge.textContent, badgeHidden:badge.hidden, summary:queues.attributes['aria-label'], followingCount:dock ? following.attributes['aria-label'] : null, }}; dock.updateQueues({{following:0, authored:0}}); process.stdout.write(JSON.stringify({{ active, cleared:{{ badgeHidden:badge.hidden, summary:queues.attributes['aria-label'], followingLabel:following.attributes['aria-label'], }}, }})); """ result = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert result.returncode == 0, result.stderr assert json.loads(result.stdout) == { "active": { "badge": "2 active", "badgeHidden": False, "summary": "Queues: Today 0, Agenda 0 due, Delivery 0, Human Gates 0, Attention 0, Updates 0, Following 3, Filed 0, My PRs 2, Later 0, Drafts 0; 2 active queues", "followingCount": "Following, 3 unseen changes", }, "cleared": { "badgeHidden": True, "summary": "Queues: no active queues; no upcoming deadlines", "followingLabel": "Following, 0 unseen changes", }, } def test_mobile_queue_launcher_opens_first_actionable_item_after_selecting_queue(): script = f""" const createLauncher = require({json.dumps(str(QUEUE_LAUNCHER))}); const calls = []; const launcher = createLauncher({{ selectFilter: name => calls.push('filter:' + name), firstAction: name => name === 'attention' ? {{click() {{ calls.push('open:first'); }}}} : null, announce: message => calls.push('announce:' + message), }}); const opened = launcher.open('attention'); const empty = launcher.open('later'); process.stdout.write(JSON.stringify({{opened, empty, calls}})); """ result = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert result.returncode == 0, result.stderr assert json.loads(result.stdout) == { "opened": "opened", "empty": "empty", "calls": [ "filter:attention", "open:first", "filter:later", "announce:No deferred work is ready to open.", ], } def test_mobile_queue_launcher_uses_dedicated_filed_follow_up_flow(): script = f""" const createLauncher = require({json.dumps(str(QUEUE_LAUNCHER))}); const calls = []; const launcher = createLauncher({{ openFiled: () => {{ calls.push('filed-follow-up'); return 'opened-update'; }}, selectFilter: name => calls.push('generic-filter:' + name), firstAction: () => {{ throw new Error('generic card launch must not run'); }}, announce: message => calls.push('announce:' + message), }}); const result = launcher.open('filed'); process.stdout.write(JSON.stringify({{result, calls}})); """ result = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert result.returncode == 0, result.stderr assert json.loads(result.stdout) == { "result": "opened-update", "calls": ["filed-follow-up"], } def test_mobile_queue_launcher_uses_existing_human_gate_review_flow(): script = f""" const createLauncher = require({json.dumps(str(QUEUE_LAUNCHER))}); const calls = []; const launcher = createLauncher({{ openHumanGates: () => {{ calls.push('human-gates'); return 'opened-gates'; }}, selectFilter: name => calls.push('generic-filter:' + name), firstAction: () => {{ throw new Error('generic card launch must not run'); }}, announce: message => calls.push('announce:' + message), }}); process.stdout.write(JSON.stringify({{result:launcher.open('gate'), calls}})); """ result = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert result.returncode == 0, result.stderr assert json.loads(result.stdout) == { "result": "opened-gates", "calls": ["human-gates"], } def test_mobile_queue_launcher_uses_dedicated_delivery_recovery_flow(): script = f""" const createLauncher = require({json.dumps(str(QUEUE_LAUNCHER))}); const calls = []; const launcher = createLauncher({{ openDelivery: () => {{ calls.push('delivery-recovery'); return 'opened'; }}, selectFilter: name => calls.push('generic-filter:' + name), firstAction: () => {{ throw new Error('generic card launch must not run'); }}, }}); const result = launcher.open('delivery'); process.stdout.write(JSON.stringify({{result, calls}})); """ result = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert result.returncode == 0, result.stderr assert json.loads(result.stdout) == {"result": "opened", "calls": ["delivery-recovery"]} def test_mobile_work_prioritizes_and_revalidates_actionable_delivery_recovery(): script = f""" const createLauncher = require({json.dumps(str(QUEUE_LAUNCHER))}); const calls = []; let counts = {{delivery:2, gate:1, attention:3, today:1, update:4}}; const launcher = createLauncher({{ getCounts: () => counts, openDelivery: () => {{ calls.push('delivery-recovery'); return 'opened-delivery'; }}, openUpdates: () => {{ calls.push('updates'); return 'opened-updates'; }}, selectFilter: name => calls.push('filter:' + name), firstAction: () => null, announce: () => {{}}, openFindWork: () => calls.push('find'), }}); const delivery = launcher.recommend(); const opened = launcher.continueWork(); counts = {{delivery:0, gate:1, attention:0, today:0, update:4}}; const afterRecovery = launcher.recommend(); process.stdout.write(JSON.stringify({{delivery, opened, afterRecovery, calls}})); """ result = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert result.returncode == 0, result.stderr assert json.loads(result.stdout) == { "delivery": {"name": "delivery", "count": 2, "label": "Recover Delivery (2)"}, "opened": "opened-delivery", "afterRecovery": {"name": "gate", "count": 1, "label": "Review Human Gates (1)"}, "calls": ["delivery-recovery"], } def test_adaptive_mobile_work_skips_online_only_queues_offline_and_restores_them_online(): script = f""" const createLauncher = require({json.dumps(str(QUEUE_LAUNCHER))}); const calls = []; let online = false; const launcher = createLauncher({{ getCounts: () => ({{delivery:2, gate:1, today:3, update:4}}), isOnline: () => online, openToday: () => {{ calls.push('today'); return 'opened-today'; }}, selectFilter: name => calls.push('filter:' + name), firstAction: () => null, announce: () => {{}}, }}); const offline = launcher.recommend(); const opened = launcher.continueWork(); online = true; const reconnected = launcher.recommend(); process.stdout.write(JSON.stringify({{offline, opened, reconnected, calls}})); """ result = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert result.returncode == 0, result.stderr assert json.loads(result.stdout) == { "offline": {"name": "today", "count": 3, "label": "Continue Today (3)"}, "opened": "opened-today", "reconnected": {"name": "delivery", "count": 2, "label": "Recover Delivery (2)"}, "calls": ["today"], } def test_mobile_work_continues_into_filed_follow_up_before_later_work(): script = f""" const createLauncher = require({json.dumps(str(QUEUE_LAUNCHER))}); const calls = []; let counts = {{attention:0, today:0, update:0, agenda:0, filed:2, later:4, draft:3}}; const launcher = createLauncher({{ getCounts: () => counts, openFiled: () => {{ calls.push('filed-follow-up'); return 'opened-filed'; }}, selectFilter: name => calls.push('filter:' + name), firstAction: () => null, announce: () => {{}}, openFindWork: () => calls.push('find'), }}); const recommendation = launcher.recommend(); const opened = launcher.continueWork(); process.stdout.write(JSON.stringify({{recommendation, opened, calls}})); """ result = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert result.returncode == 0, result.stderr assert json.loads(result.stdout) == { "recommendation": {"name": "filed", "count": 2, "label": "Review Filed (2)"}, "opened": "opened-filed", "calls": ["filed-follow-up"], } def test_mobile_work_continues_following_then_authored_before_claiming_new_work(): script = f""" const createLauncher = require({json.dumps(str(QUEUE_LAUNCHER))}); const calls = []; let counts = {{following:3, authored:2, filed:1, later:4}}; const launcher = createLauncher({{ getCounts: () => counts, openFollowing: () => {{ calls.push('following'); return 'opened-following'; }}, selectFilter: name => calls.push('filter:' + name), firstAction: name => name === 'authored' ? {{click() {{ calls.push('open:authored'); }}}} : null, announce: message => calls.push('announce:' + message), openFindWork: () => calls.push('find'), }}); const following = launcher.recommend(); const openedFollowing = launcher.continueWork(); counts = {{following:0, authored:2, filed:1, later:4}}; const authored = launcher.recommend(); const openedAuthored = launcher.continueWork(); counts = {{following:0, authored:0, filed:0, later:0}}; const fallback = launcher.recommend(); launcher.continueWork(); process.stdout.write(JSON.stringify({{following, openedFollowing, authored, openedAuthored, fallback, calls}})); """ result = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert result.returncode == 0, result.stderr assert json.loads(result.stdout) == { "following": {"name": "following", "count": 3, "label": "Review Following (3)"}, "openedFollowing": "opened-following", "authored": {"name": "authored", "count": 2, "label": "Open My PRs (2)"}, "openedAuthored": "opened", "fallback": {"name": "find", "count": 0, "label": "Find Work"}, "calls": ["following", "filter:authored", "open:authored", "find"], } def test_mobile_queue_launcher_recommends_and_revalidates_cross_queue_continuation(): script = f""" const createLauncher = require({json.dumps(str(QUEUE_LAUNCHER))}); const calls = []; let counts = {{attention: 2, today: 1, agenda: 5, later: 4, draft: 3}}; const launcher = createLauncher({{ openToday: () => calls.push('today'), openAgenda: () => {{ calls.push('agenda'); return 'opened'; }}, selectFilter: name => calls.push('filter:' + name), firstAction: name => (counts[name] || 0) ? {{click() {{ calls.push('open:' + name); }}}} : null, announce: message => calls.push('announce:' + message), getCounts: () => counts, openFindWork: () => calls.push('find'), }}); const first = launcher.recommend(); counts = {{attention: 0, today: 0, agenda: 5, later: 4, draft: 3}}; const agenda = launcher.recommend(); const opened = launcher.continueWork(); counts = {{attention: 0, today: 0, agenda: 0, later: 0, draft: 0}}; const fallback = launcher.recommend(); launcher.continueWork(); process.stdout.write(JSON.stringify({{first, agenda, opened, fallback, calls}})); """ result = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert result.returncode == 0, result.stderr assert json.loads(result.stdout) == { "first": {"name": "attention", "count": 2, "label": "Start Attention (2)"}, "agenda": {"name": "agenda", "count": 5, "label": "Open Agenda (5)"}, "opened": "opened", "fallback": {"name": "find", "count": 0, "label": "Find Work"}, "calls": ["agenda", "find"], } def test_mobile_queue_launcher_applies_routine_priority_without_demoting_safety_queues(): script = f""" const createLauncher = require({json.dumps(str(QUEUE_LAUNCHER))}); let online = true; const launcher = createLauncher({{ getCounts:() => ({{delivery:1, gate:2, attention:3, following:4, authored:5}}), isOnline:() => online, getRoutineOrder:() => ['following','authored','attention','today','update','agenda','filed','later','draft'], }}); const onlineView = launcher.presentation(); online = false; const offlineView = launcher.presentation(); process.stdout.write(JSON.stringify({{ onlineNext:onlineView.nextUp.name, onlineActive:onlineView.active.map(item => item.name), offlineNext:offlineView.nextUp.name, offlineActive:offlineView.active.map(item => item.name), }})); """ result = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert result.returncode == 0, result.stderr assert json.loads(result.stdout) == { "onlineNext": "delivery", "onlineActive": ["delivery", "gate", "following", "authored", "attention"], "offlineNext": "following", "offlineActive": ["delivery", "gate", "following", "authored", "attention"], } def test_mobile_queue_launcher_uses_one_adaptive_action_for_daily_preparation(): script = f""" const createLauncher = require({json.dumps(str(QUEUE_LAUNCHER))}); const calls = []; let preparation = {{active:true, total:2, label:'Review Human Gates'}}; const nextAction = {{textContent:'', dataset:{{}}, attributes:{{}}, setAttribute(name, value) {{ this.attributes[name]=value; }}}}; const launcher = createLauncher({{ getCounts: () => ({{delivery:4}}), getPreparation: () => preparation, openPreparation: () => calls.push('prepare'), nextAction, }}); launcher.renderPresentation(); const resumed = [nextAction.textContent, nextAction.dataset.queue, nextAction.attributes['aria-label']]; launcher.continueWork(); preparation = {{active:false, total:2, label:'Review Agenda'}}; launcher.renderPresentation(); const started = [nextAction.textContent, nextAction.dataset.queue, nextAction.attributes['aria-label']]; launcher.continueWork(); process.stdout.write(JSON.stringify({{resumed, started, calls}})); """ result = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert result.returncode == 0, result.stderr assert json.loads(result.stdout) == { "resumed": [ "Resume preparation · Review Human Gates", "prepare", "Start or continue: Resume preparation · Review Human Gates", ], "started": [ "Start day · Review Agenda", "prepare", "Start or continue: Start day · Review Agenda", ], "calls": ["prepare", "prepare"], } def test_mobile_queue_launcher_builds_truthful_next_up_and_active_sections(): script = f""" const createLauncher = require({json.dumps(str(QUEUE_LAUNCHER))}); const counts = {{delivery:1, gate:2, today:3, authored:4, update:0, attentionUnavailable:true, followingUnavailable:true}}; const launcher = createLauncher({{getCounts:() => counts}}); process.stdout.write(JSON.stringify(launcher.presentation())); """ result = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert result.returncode == 0, result.stderr assert json.loads(result.stdout) == { "nextUp": {"name": "delivery", "count": 1, "label": "Recover Delivery (1)"}, "active": [ {"name": "delivery", "count": 1}, {"name": "gate", "count": 2}, {"name": "today", "count": 3}, {"name": "authored", "count": 4}, {"name": "attention", "unavailable": True}, {"name": "following", "unavailable": True}, ], "planning": ["today", "tomorrow", "week"], "all": [ "today", "tomorrow", "week", "agenda", "delivery", "gate", "attention", "update", "following", "filed", "authored", "later", "draft", "find", "recaps", ], } def test_mobile_queue_launcher_renders_one_set_of_rows_into_adaptive_groups(): script = f""" const createLauncher = require({json.dumps(str(QUEUE_LAUNCHER))}); class Box {{ constructor(name) {{ this.name=name; this.children=[]; this.hidden=false; this.attributes={{}}; this.textContent=''; this.dataset={{}}; }} append(row) {{ if (row.parent) row.parent.children=row.parent.children.filter(item => item !== row); this.children.push(row); row.parent=this; }} setAttribute(name, value) {{ this.attributes[name]=value; }} removeAttribute(name) {{ delete this.attributes[name]; }} }} const names=['today','tomorrow','week','agenda','delivery','gate','attention','update','following','filed','authored','later','draft','find','recaps']; const rows=Object.fromEntries(names.map(name => [name,new Box(name)])); const nextAction=new Box('next'); const activeList=new Box('active'); const planningList=new Box('planning'); const allList=new Box('all'); const activeSection=new Box('active-section'); const launcher=createLauncher({{ getCounts:()=>({{delivery:1,gate:2,today:3,attentionUnavailable:true}}), rows, nextAction, activeList, planningList, allList, activeSection, }}); launcher.renderPresentation(); process.stdout.write(JSON.stringify({{ next:[nextAction.textContent,nextAction.dataset.queue,nextAction.attributes['aria-label']], active:activeList.children.map(row => [row.name,row.attributes['data-unavailable'] || null]), planning:planningList.children.map(row => row.name), all:allList.children.map(row => row.name), activeHidden:activeSection.hidden, }})); """ result = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert result.returncode == 0, result.stderr assert json.loads(result.stdout) == { "next": ["Recover Delivery (1)", "delivery", "Next up: Recover Delivery (1)"], "active": [["delivery", None], ["gate", None], ["attention", "true"]], "planning": ["today", "tomorrow", "week"], "all": ["agenda", "update", "following", "filed", "authored", "later", "draft", "find", "recaps"], "activeHidden": False, } def test_mobile_queue_launcher_prioritizes_updates_before_agenda_and_resumes_launcher(): script = f""" const createLauncher = require({json.dumps(str(QUEUE_LAUNCHER))}); const calls = []; let counts = {{attention:0, today:0, update:6, agenda:2, later:1, draft:1}}; const launcher = createLauncher({{ getCounts: () => counts, openUpdates: () => {{ calls.push('updates'); return 'resumed'; }}, openAgenda: () => calls.push('agenda'), selectFilter: name => calls.push('filter:' + name), firstAction: () => null, announce: () => {{}}, openFindWork: () => calls.push('find'), }}); const recommendation = launcher.recommend(); const opened = launcher.continueWork(); process.stdout.write(JSON.stringify({{recommendation, opened, calls}})); """ result = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert result.returncode == 0, result.stderr assert json.loads(result.stdout) == { "recommendation": {"name": "update", "count": 6, "label": "Resume Updates (6)"}, "opened": "resumed", "calls": ["updates"], } def test_mobile_task_dock_labels_each_recommended_work_destination_accessibly(): script = f""" const createDock = require({json.dumps(str(DOCK))}); const work = {{attributes: {{}}, setAttribute(name, value) {{ this.attributes[name] = value; }}}}; const workLabel = {{textContent:''}}; const dock = createDock({{nav:{{}}, buttons:{{work}}, workLabel}}); const labels = {{}}; for (const mode of ['delivery','attention','update','agenda','following','authored','filed','later','draft']) {{ dock.updateWork(mode); labels[mode] = [workLabel.textContent, work.attributes['aria-label']]; }} process.stdout.write(JSON.stringify(labels)); """ result = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert result.returncode == 0, result.stderr assert json.loads(result.stdout) == { "delivery": ["Delivery", "Open Delivery"], "attention": ["Attention", "Open Attention"], "update": ["Updates", "Resume Updates"], "agenda": ["Agenda", "Open Agenda"], "following": ["Following", "Open Following"], "authored": ["My PRs", "Open My PRs"], "filed": ["Filed", "Open Filed"], "later": ["Later", "Open Later"], "draft": ["Drafts", "Open Drafts"], } def test_mobile_task_dock_labels_prepare_today_as_the_primary_action(): script = f""" const createDock = require({json.dumps(str(DOCK))}); const work = {{attributes: {{}}, setAttribute(name, value) {{ this.attributes[name] = value; }}}}; const workLabel = {{textContent:''}}; const dock = createDock({{nav:{{}}, buttons:{{work}}, workLabel}}); const labels = {{}}; for (const mode of ['prepare','prepare-resume']) {{ dock.updateWork(mode); labels[mode] = [workLabel.textContent, work.attributes['aria-label']]; }} process.stdout.write(JSON.stringify(labels)); """ result = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert result.returncode == 0, result.stderr assert json.loads(result.stdout) == { "prepare": ["Prepare", "Prepare Today"], "prepare-resume": ["Resume prep", "Resume preparation"], } @pytest.mark.anyio async def test_dashboard_renders_accessible_mobile_queue_completion_handoff(): html = await dashboard() assert 'id="mobile-queue-heading"' in html assert 'data-mobile-queue="find"' in html assert "mobileQueueLauncher.recommend()" in html assert "showMobileQueueCompletion('Updates')" in html assert "row.setAttribute('data-recommended', 'true')" in html assert "row.focus()" in html @pytest.mark.anyio async def test_dashboard_renders_and_wires_mobile_queue_switcher(): html = await dashboard() assert 'data-mobile-task="queues"' in html assert 'id="mobile-queue-sheet"' in html assert 'aria-labelledby="mobile-queue-heading"' in html assert 'data-mobile-queue="today"' in html assert 'data-mobile-queue="agenda"' in html assert 'AgendaUpcoming deadlines' in html assert 'data-work-filter="agenda"' in html assert "const agendaItems = agendaMyWork(activeMyWork);" in html assert "counts.agenda = agendaItems.length;" in html assert 'data-agenda-group' in html assert 'data-mobile-queue="attention"' in html assert 'data-mobile-queue="delivery"' in html assert 'DeliveryNeeds recovery' in html assert 'data-mobile-queue-count="delivery"' in html assert 'data-mobile-queue="update"' in html assert 'UpdatesUnread conversations' in html assert 'data-mobile-queue="filed"' in html assert 'FiledIssues you delegated' in html assert 'data-mobile-queue-count="filed"' in html assert "openFiled: openFiledFollowUp" in html assert "filedFollowUpTarget(completedFiledReview.visible(lastMyWork))" in html assert 'data-mobile-queue="later"' in html assert 'data-mobile-queue="draft"' in html assert 'data-mobile-queue="recaps"' in html assert 'RecapsHistory' in html assert 'id="mobile-queue-count"' in html assert 'id="mobile-deadline-count"' in html assert 'mobileTaskDock.updateQueues(counts)' in html assert '' in html assert "const mobileQueueLauncher = createMobileQueueLauncher({" in html assert "firstAction: name => qs('#my-work-list .my-work-card-main, #my-work-list .draft-resume, #my-work-list .draft-continue, #my-work-list .draft-edit')" in html assert "name === 'find' ? qs('#find-work').click() :" in html assert "queueCounts.followingUnavailable ? followingQueue.open()" in html assert '.mobile-queue-sheet' in html assert '.my-work-actions { display:none;' in html assert 'padding-bottom:calc(16px + env(safe-area-inset-bottom))' in html assert '.mobile-task-deadline' in html assert '[data-deadlines="true"]' in html assert '.mobile-task-dock[data-attention="true"]' not in html @pytest.mark.anyio async def test_dashboard_wires_mobile_work_dock_into_today_session_lifecycle(): html = await dashboard() assert 'id="mobile-work-label">Work' in html assert '' in html assert "const mobileWorkEntry = createMobileWorkEntry({" in html assert "isTodayActive: () => workSession.checkpointed()" in html assert "isTodayResumable: () => workSession.resumable()" in html assert "getTodayCount: () => todayMyWork.length" in html assert "getEligibleCount: () => activeMyWork.length" in html assert "continueToday: continueTodaySession" in html assert "resumeToday: resumeTodaySession" in html assert "startToday: startTodaySession" in html assert "planToday: () => openPlanToday(mobileTaskButtons.work)" in html assert "work: () => mobileWorkEntry.open()" in html assert "mobileTaskDock.updateWork(mobileWorkEntry.mode())" in html assert "mobileTaskDock.updateAttention(countMyWork(activeMyWork).attention)" in html assert "workSession.reopen(item)" in html assert "runTodayTransition('continue')" in html @pytest.mark.anyio async def test_dashboard_wires_work_dock_and_app_shortcut_to_live_queue_recommendation(): html = await dashboard() assert "queueLauncher: mobileQueueLauncher" in html assert "work: () => mobileWorkEntry.open()" in html assert "continueWork: () => mobileWorkEntry.open()" in html assert "queueCounts = counts;" in html assert "mobileTaskDock.updateWork(mobileWorkEntry.mode())" in html @pytest.mark.anyio async def test_dashboard_renders_and_wires_phone_safe_task_dock(): html = await dashboard() assert '