import json import os import subprocess from pathlib import Path import pytest from tests.dashboard_bundle import dashboard @pytest.mark.anyio async def test_empty_all_work_queue_offers_find_and_create_actions(): markup = (Path(__file__).resolve().parents[1] / "frontend" / "index.html").read_text() source = await dashboard() assert 'id="empty-work-start"' in markup assert 'Ready for something new?' in markup assert 'id="empty-work-find"' in markup assert 'id="empty-work-create"' in markup assert "qs('#empty-work-find').addEventListener('click'" in source assert "qs('#find-work').click()" in source assert "qs('#empty-work-create').addEventListener('click'" in source assert "qs('#new-issue').click()" in source assert "emptyWorkStart.hidden = !showEmptyStart" in source MY_WORK = Path(__file__).parents[1] / "frontend" / "my-work.js" NOTIFICATION_UNDO = Path(__file__).parents[1] / "frontend" / "notification-undo.js" TODAY_TIMER = Path(__file__).parents[1] / "frontend" / "today-timer.js" LATER_WORK = Path(__file__).parents[1] / "frontend" / "later-work.js" DETAIL_DEFER = Path(__file__).parents[1] / "frontend" / "detail-defer.js" LATER_PICKER = Path(__file__).parents[1] / "frontend" / "later-picker.js" LATER_AND_START = Path(__file__).parents[1] / "frontend" / "later-and-start.js" REVIEW_SHEET = Path(__file__).parents[1] / "frontend" / "review-sheet.js" ISSUE_SHEET = Path(__file__).parents[1] / "frontend" / "issue-sheet.js" FRONTEND_BUNDLE = Path(__file__).parents[1] / "src" / "frontend_bundle.py" CREATE_ISSUE_SHEET = Path(__file__).parents[1] / "frontend" / "create-issue-sheet.js" PULL_SHEET = Path(__file__).parents[1] / "frontend" / "pull-sheet.js" CONVERSATION = Path(__file__).parents[1] / "frontend" / "conversation.js" COMMENT_ACTIONS = Path(__file__).parents[1] / "frontend" / "comment-actions.js" PICK_WORK = Path(__file__).parents[1] / "frontend" / "pick-work.js" WORK_ROUTE = Path(__file__).parents[1] / "frontend" / "work-route.js" UPDATE_OWNERSHIP = Path(__file__).parents[1] / "frontend" / "update-ownership.js" AGENDA_SESSION_LAUNCHER = Path(__file__).parents[1] / "frontend" / "agenda-session-launcher.js" UPDATE_TRIAGE_LAUNCHER = Path(__file__).parents[1] / "frontend" / "update-triage-launcher.js" def test_filed_follow_up_target_preserves_queue_order_and_selects_update_reader(): script = f""" const buildMyWork = require({json.dumps(str(MY_WORK))}); const choose = buildMyWork.filedFollowUpTarget; const available = typeof choose === 'function'; const quiet = {{key:'repo#1', is_filed:true, has_update:false}}; const unread = {{key:'repo#2', is_filed:true, has_update:true, notification_id:22}}; const unrelated = {{key:'repo#3', is_filed:false, has_update:true}}; const results = available ? {{ first:choose([unrelated, unread, quiet]), fallback:choose([unrelated, quiet, unread]), empty:choose([unrelated]), }} : null; process.stdout.write(JSON.stringify({{available, results}})); """ result = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert result.returncode == 0, result.stderr assert json.loads(result.stdout) == { "available": True, "results": { "first": {"kind": "update", "item": {"key": "repo#2", "is_filed": True, "has_update": True, "notification_id": 22}}, "fallback": {"kind": "issue", "item": {"key": "repo#1", "is_filed": True, "has_update": False}}, "empty": None, }, } def test_completed_filed_outcomes_are_distinct_and_rank_before_open_filings(): payload = { "user": {"login": "timmy"}, "issues": [ {"number": 10, "title": "Still delegated", "repository": "stackchain/api", "state": "open", "assignees": ["alex"], "work_reasons": ["created_by_me"], "updated_at": "2026-08-15T12:00:00Z"}, {"number": 9, "title": "Outcome ready", "repository": "stackchain/api", "state": "closed", "assignees": ["timmy"], "work_reasons": ["created_by_me"], "updated_at": "2026-08-14T12:00:00Z"}, ], } script = f""" const buildMyWork = require({json.dumps(str(MY_WORK))}); process.stdout.write(JSON.stringify(buildMyWork({json.dumps(payload)}))); """ completed = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert completed.returncode == 0, completed.stderr result = json.loads(completed.stdout) assert [item["number"] for item in result] == [9, 10] assert result[0]["is_completed"] is True assert result[0]["reason"] == "Completed · review outcome" assert result[1]["is_completed"] is False assert result[1]["reason"] == "Filed by you" def test_completed_filed_acknowledgement_survives_refresh_until_upstream_update(): script = f""" const buildMyWork = require({json.dumps(str(MY_WORK))}); const values = new Map(); const storage = {{ getItem:key => values.has(key) ? values.get(key) : null, setItem:(key, value) => values.set(key, value), }}; const create = buildMyWork.createCompletedFiledReview; if (typeof create !== 'function') {{ process.stdout.write(JSON.stringify({{available:false}})); }} else {{ const items = [ {{kind:'issue', key:'stackchain/api#9', repository:'stackchain/api', number:9, is_filed:true, is_completed:true, updated_at:'2026-08-14T12:00:00Z'}}, {{kind:'issue', key:'stackchain/api#10', repository:'stackchain/api', number:10, is_filed:true, is_completed:false, updated_at:'2026-08-15T12:00:00Z'}}, ]; const first = create({{storage, getLogin:() => 'timmy'}}); const before = first.visible(items).map(item => item.number); const acknowledged = first.acknowledge(items[0]); const after = first.visible(items).map(item => item.number); const reloaded = create({{storage, getLogin:() => 'timmy'}}); const afterRefresh = reloaded.visible(items).map(item => item.number); const updated = reloaded.visible([{{...items[0], updated_at:'2026-08-16T12:00:00Z'}}, items[1]]) .map(item => item.number); process.stdout.write(JSON.stringify({{available:true, before, acknowledged, after, afterRefresh, updated}})); }} """ completed = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert completed.returncode == 0, completed.stderr assert json.loads(completed.stdout) == { "available": True, "before": [9, 10], "acknowledged": True, "after": [10], "afterRefresh": [10], "updated": [9, 10], } def test_completed_filed_review_partitions_acknowledged_history_until_a_new_revision(): script = f""" const buildMyWork = require({json.dumps(str(MY_WORK))}); const values = new Map(); const storage = {{ getItem:key => values.has(key) ? values.get(key) : null, setItem:(key, value) => values.set(key, value), }}; const create = buildMyWork.createCompletedFiledReview; const open = {{kind:'issue', key:'stackchain/api#10', repository:'stackchain/api', number:10, is_filed:true, is_completed:false, updated_at:'2026-08-15T12:00:00Z'}}; const completed = {{kind:'issue', key:'stackchain/api#9', repository:'stackchain/api', number:9, is_filed:true, is_completed:true, updated_at:'2026-08-14T12:00:00Z'}}; const review = create({{storage, getLogin:() => 'timmy'}}); const before = review.partition([open, completed]); review.acknowledge(completed); const after = review.partition([open, completed]); const revised = review.partition([open, {{...completed, updated_at:'2026-08-16T12:00:00Z'}}]); const shape = value => ({{ needsReview:value.needsReview.map(item => item.number), reviewed:value.reviewed.map(item => item.number), }}); process.stdout.write(JSON.stringify({{before:shape(before), after:shape(after), revised:shape(revised)}})); """ completed = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert completed.returncode == 0, completed.stderr assert json.loads(completed.stdout) == { "before": {"needsReview": [10, 9], "reviewed": []}, "after": {"needsReview": [10], "reviewed": [9]}, "revised": {"needsReview": [10, 9], "reviewed": []}, } def test_completed_filed_review_merges_remote_receipts_and_keeps_local_work_pending(): script = f""" const buildMyWork = require({json.dumps(str(MY_WORK))}); const values = new Map(); const storage = {{ getItem:key => values.has(key) ? values.get(key) : null, setItem:(key, value) => values.set(key, value), }}; const item = (repository, number, updated_at) => ({{ kind:'issue', key:repository + '#' + number, repository, number, is_filed:true, is_completed:true, updated_at, }}); const local = item('stackchain/api', 9, '2026-08-15T12:00:00Z'); const remote = item('stackchain/web', 4, '2026-08-14T12:00:00Z'); const review = buildMyWork.createCompletedFiledReview({{storage, getLogin:() => 'timmy'}}); review.acknowledge(local); const pendingBefore = review.pending(); const changed = review.adopt({{receipts:[remote]}}); const visible = review.visible([local, remote]).map(value => value.key); const pendingAfterRemote = review.pending(); review.adopt({{receipts:[ {{repository:local.repository, number:local.number, updated_at:local.updated_at}}, {{repository:remote.repository, number:remote.number, updated_at:remote.updated_at}}, ]}}); process.stdout.write(JSON.stringify({{pendingBefore, changed, visible, pendingAfterRemote, pendingFinal:review.pending()}})); """ completed = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert completed.returncode == 0, completed.stderr assert json.loads(completed.stdout) == { "pendingBefore": [receipt := { "repository": "stackchain/api", "number": 9, "updated_at": "2026-08-15T12:00:00Z" }], "changed": True, "visible": [], "pendingAfterRemote": [receipt], "pendingFinal": [], } @pytest.mark.anyio async def test_mobile_filed_exposes_actionable_and_reviewed_views_without_inflating_badge(): markup = (Path(__file__).parents[1] / "frontend" / "index.html").read_text() css = (Path(__file__).parents[1] / "frontend" / "dashboard.css").read_text() source = await dashboard() assert 'id="filed-history-tabs"' in markup assert 'data-filed-view="needs-review"' in markup assert 'data-filed-view="reviewed"' in markup assert 'No reviewed outcomes yet' in source assert "const actionableMyWork = filedHistoryTabs.prepare(lastMyWork)" in source assert "const counts = countMyWork(activeMyWork)" in source assert "todayWork.reconcile(actionableMyWork" in source assert "selectedFiledView === 'reviewed'" in source assert "filedFollowUpTarget(completedFiledReview.visible(lastMyWork))" in source.split("function openFiledFollowUp()", 1)[1].split("}", 1)[0] assert ".filed-history-tabs" in css filed_tabs_rule = css.split(".filed-history-tabs button", 1)[1].split("}", 1)[0] assert "min-height:44px" in filed_tabs_rule assert "grid-template-columns:repeat(2,minmax(0,1fr))" in css @pytest.mark.anyio async def test_completed_filed_sheet_exposes_mobile_acknowledge_and_next_flow(): markup = (Path(__file__).parents[1] / "frontend" / "index.html").read_text() css = (Path(__file__).parents[1] / "frontend" / "dashboard.css").read_text() source = await dashboard() assert 'id="completed-filed-actions"' in markup assert 'id="acknowledge-completed-filed"' in markup assert 'Acknowledge & next' in markup assert "createCompletedFiledReview({" in source assert "lastMyWork = buildMyWork(data)" in source assert "filed: 'filed issues'" in source handler = source.split("qs('#acknowledge-completed-filed').addEventListener('click'", 1)[1] assert "completedFiledReview.acknowledge(selectedIssue)" in handler assert "syncCompletedFiledReviews()" in source assert "api/v1/completed-filed-reviews" in source assert "sync pending" in source assert "filedFollowUpTarget(completedFiledReview.visible(lastMyWork))" in handler assert "openRoutedWork" in handler assert ".completed-filed-actions" in css action_rule = css.split(".completed-filed-actions", 1)[1].split("}", 1)[0] assert "position:fixed" in action_rule assert "env(safe-area-inset-bottom)" in action_rule assert "min-height:44px" in css.split(".completed-filed-actions", 1)[1] def test_updates_rank_actionable_work_before_newer_routine_activity(): payload = { "user": {"login": "timmy"}, "issues": [ {"number": 1, "title": "Critical incident", "repository": "stackchain/api", "labels": ["P0"], "assignees": [], "updated_at": "2026-08-01T10:00:00Z"}, {"number": 2, "title": "Overdue assignment", "repository": "stackchain/api", "labels": [], "assignees": ["timmy"], "due_date": "2026-08-06T23:59:59Z", "updated_at": "2026-08-05T10:00:00Z"}, {"number": 3, "title": "Assigned follow-up", "repository": "stackchain/api", "labels": [], "assignees": ["timmy"], "updated_at": "2026-08-06T10:00:00Z"}, ], "pull_requests": [ {"number": 4, "title": "Requested review", "repository": "stackchain/web", "labels": [], "assignees": [], "work_reasons": ["review_requested"], "updated_at": "2026-08-02T10:00:00Z"}, ], "notifications": [ {"id": 11, "number": 1, "repository": "stackchain/api", "unread": True, "subject_type": "Issue", "url": "https://forge.example/api/issues/1"}, {"id": 12, "number": 2, "repository": "stackchain/api", "unread": True, "subject_type": "Issue", "url": "https://forge.example/api/issues/2"}, {"id": 13, "number": 3, "repository": "stackchain/api", "unread": True, "subject_type": "Issue", "url": "https://forge.example/api/issues/3"}, {"id": 14, "number": 4, "repository": "stackchain/web", "unread": True, "subject_type": "PullRequest", "url": "https://forge.example/web/pulls/4"}, {"id": 15, "number": 5, "repository": "stackchain/web", "unread": True, "subject_type": "Issue", "title": "Newest routine update", "updated_at": "2026-08-07T11:00:00Z", "url": "https://forge.example/web/issues/5"}, ], } script = f""" const buildMyWork = require({json.dumps(str(MY_WORK))}); const updates = buildMyWork({json.dumps(payload)}, new Date('2026-08-07T12:00:00Z')) .filter(item => item.has_update) .map(item => ({{title:item.title, update_reason:item.update_reason}})); process.stdout.write(JSON.stringify(updates)); """ result = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert result.returncode == 0, result.stderr assert json.loads(result.stdout) == [ {"title": "Critical incident", "update_reason": "Critical"}, {"title": "Requested review", "update_reason": "Review requested"}, {"title": "Overdue assignment", "update_reason": "Overdue"}, {"title": "Assigned follow-up", "update_reason": "Assigned to you"}, {"title": "Newest routine update", "update_reason": ""}, ] def test_update_triage_launch_waits_for_complete_single_flight_discovery(): script = f""" const createLauncher = require({json.dumps(str(UPDATE_TRIAGE_LAUNCHER))}); let release; let discoveries = 0; let opens = 0; const messages = []; const launcher = createLauncher({{ selectUpdates: () => {{}}, discover: () => {{ discoveries += 1; return new Promise(resolve => {{ release = resolve; }}); }}, isUpdatesSelected: () => true, hasMore: () => false, hasCheckpoint: () => false, resume: () => {{ throw new Error('must start'); }}, start: () => {{ opens += 1; return true; }}, announce: message => messages.push(message), }}); const first = launcher.open(); const second = launcher.open(); Promise.resolve().then(() => {{ const before = {{discoveries, opens, same:first === second}}; release(true); return Promise.all([first, second]).then(results => process.stdout.write(JSON.stringify({{before, discoveries, opens, results, messages}})) ); }}); """ result = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert result.returncode == 0, result.stderr assert json.loads(result.stdout) == { "before": {"discoveries": 1, "opens": 0, "same": True}, "discoveries": 1, "opens": 1, "results": ["opened", "opened"], "messages": ["Checking all unread updates…"], } def test_update_triage_launch_preserves_checkpoint_when_discovery_fails(): script = f""" const createLauncher = require({json.dumps(str(UPDATE_TRIAGE_LAUNCHER))}); const messages = []; let resumed = 0; const launcher = createLauncher({{ selectUpdates: () => {{}}, discover: async () => false, isUpdatesSelected: () => true, hasMore: () => true, hasCheckpoint: () => true, resume: () => {{ resumed += 1; return true; }}, start: () => true, announce: message => messages.push(message), }}); launcher.open().then(result => process.stdout.write(JSON.stringify({{result, resumed, messages}}))); """ result = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert result.returncode == 0, result.stderr assert json.loads(result.stdout) == { "result": "incomplete", "resumed": 0, "messages": [ "Checking all unread updates…", "Updates check paused. Retry to check older unread updates.", ], } def test_update_triage_launch_applies_atomic_snapshot_before_starting(): script = f""" const createLauncher = require({json.dumps(str(UPDATE_TRIAGE_LAUNCHER))}); const events = []; const snapshot = {{items:[{{id:1}},{{id:51}}],total:2,complete:true}}; const launcher = createLauncher({{ selectUpdates: () => events.push('selected'), discover: async () => snapshot, applySnapshot: value => events.push(['applied', value.items.map(item => item.id)]), isUpdatesSelected: () => true, hasMore: () => false, hasCheckpoint: () => false, resume: () => {{ throw new Error('must start'); }}, start: () => {{ events.push('started'); return true; }}, announce: () => {{}}, }}); launcher.open().then(result => process.stdout.write(JSON.stringify({{result, events}}))); """ result = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert result.returncode == 0, result.stderr assert json.loads(result.stdout) == { "result": "opened", "events": ["selected", ["applied", [1, 51]], "started"], } def test_notification_pager_load_all_retries_from_failed_page_without_duplicates(): script = f""" const buildMyWork = require({json.dumps(str(MY_WORK))}); let items = [{{id:1}}]; let fail = true; const requested = []; const pager = buildMyWork.createNotificationPager({{ load: async page => {{ requested.push(page); if (page === 2 && fail) {{ fail = false; throw new Error('offline'); }} return page === 2 ? {{page:2,total:101,has_more:true,items:[{{id:1}},{{id:51}}]}} : {{page:3,total:101,has_more:false,items:[{{id:101}}]}}; }}, onNotifications: value => {{ items = value; }}, onPagination: () => {{}}, onStatus: () => {{}}, }}); pager.reset({{page:1,total:101,has_more:true}}); pager.loadAll(() => items).then(firstResult => pager.loadAll(() => items).then(secondResult => process.stdout.write(JSON.stringify({{firstResult, secondResult, requested, ids:items.map(item => item.id)}})) )); """ result = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert result.returncode == 0, result.stderr assert json.loads(result.stdout) == { "firstResult": False, "secondResult": True, "requested": [2, 2, 3], "ids": [1, 51, 101], } def test_agenda_session_launch_waits_for_complete_single_flight_discovery(): script = f""" const createLauncher = require({json.dumps(str(AGENDA_SESSION_LAUNCHER))}); let finishDiscovery; let discoveries = 0; let opens = 0; let queue = 'agenda'; const launcher = createLauncher({{ selectAgenda: () => {{ queue = 'agenda'; }}, discover: () => {{ discoveries += 1; return new Promise(resolve => {{ finishDiscovery = resolve; }}); }}, isAgendaSelected: () => queue === 'agenda', hasMore: () => false, hasCheckpoint: () => true, resume: () => {{ opens += 1; return true; }}, start: () => {{ throw new Error('must resume'); }}, announce: () => {{}}, }}); const first = launcher.open(); const second = launcher.open(); Promise.resolve().then(() => {{ const before = {{discoveries, opens, same:first === second}}; finishDiscovery(true); return Promise.all([first, second]).then(results => {{ process.stdout.write(JSON.stringify({{before, results, discoveries, opens}})); }}); }}); """ result = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert result.returncode == 0, result.stderr assert json.loads(result.stdout) == { "before": {"discoveries": 1, "opens": 0, "same": True}, "results": ["opened", "opened"], "discoveries": 1, "opens": 1, } def test_agenda_session_launch_preserves_progress_when_discovery_fails_or_queue_changes(): script = f""" const createLauncher = require({json.dumps(str(AGENDA_SESSION_LAUNCHER))}); async function scenario(mode) {{ let queue = 'agenda'; let opens = 0; const messages = []; const launcher = createLauncher({{ selectAgenda: () => {{ queue = 'agenda'; }}, discover: async () => {{ if (mode === 'leave') queue = 'today'; if (mode === 'throw') throw new Error('offline'); return mode !== 'incomplete'; }}, isAgendaSelected: () => queue === 'agenda', hasMore: () => mode === 'incomplete', hasCheckpoint: () => true, resume: () => {{ opens += 1; return true; }}, start: () => {{ opens += 1; return true; }}, announce: message => messages.push(message), }}); return {{result:await launcher.open(), opens, messages}}; }} Promise.all(['incomplete', 'throw', 'leave'].map(scenario)).then(results => process.stdout.write(JSON.stringify(results)) ); """ result = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert result.returncode == 0, result.stderr assert json.loads(result.stdout) == [ {"result": "incomplete", "opens": 0, "messages": [ "Agenda check paused. Retry to check older assigned deadlines." ]}, {"result": "incomplete", "opens": 0, "messages": [ "Agenda check paused. Retry to check older assigned deadlines." ]}, {"result": "cancelled", "opens": 0, "messages": []}, ] def test_protect_today_route_is_an_explicit_agenda_action(): script = f""" const routes = require({json.dumps(str(WORK_ROUTE))}); process.stdout.write(JSON.stringify(routes.parse('#/my-work/agenda/protect-today'))); """ result = subprocess.run( ["node", "-e", script], check=True, capture_output=True, text=True ) assert json.loads(result.stdout) == { "kind": "queue", "filter": "agenda", "action": "protect-today" } CARD_PLANNING = Path(__file__).parents[1] / "frontend" / "card-planning.js" TODAY_WORK = Path(__file__).parents[1] / "frontend" / "today-work.js" WORK_SELECTION = Path(__file__).parents[1] / "frontend" / "work-selection.js" def test_work_selection_selects_matching_items_in_order_with_an_explicit_cap(): script = f""" const createWorkSelection = require({json.dumps(str(WORK_SELECTION))}); const selection = createWorkSelection({{limit:3}}); selection.start(); selection.select({{kind:'issue', repository:'stackchain/api', number:1}}); const result = selection.selectMany([ {{kind:'issue', repository:'stackchain/api', number:1}}, {{kind:'pull', repository:'stackchain/web', number:2}}, {{kind:'issue', repository:'stackchain/app', number:3}}, {{kind:'issue', repository:'stackchain/overflow', number:4}}, ]); process.stdout.write(JSON.stringify({{result, snapshot:selection.snapshot()}})); """ result = subprocess.run( ["node", "-e", script], check=True, capture_output=True, text=True ) output = json.loads(result.stdout) assert output == { "result": {"status": "limit", "added": 2, "count": 3, "limit": 3}, "snapshot": { "active": True, "count": 3, "ids": [ "issue:stackchain/api:1:", "pull:stackchain/web:2:", "issue:stackchain/app:3:", ], }, } def test_work_selection_clears_matches_without_leaving_selection_mode(): script = f""" const createWorkSelection = require({json.dumps(str(WORK_SELECTION))}); const selection = createWorkSelection(); selection.start(); selection.selectMany([ {{kind:'issue', repository:'stackchain/api', number:1}}, {{kind:'pull', repository:'stackchain/web', number:2}}, ]); process.stdout.write(JSON.stringify(selection.clear())); """ result = subprocess.run( ["node", "-e", script], check=True, capture_output=True, text=True ) assert json.loads(result.stdout) == {"active": True, "count": 0, "ids": []} @pytest.mark.anyio async def test_mobile_batch_planning_can_select_and_clear_active_queue_matches(): html = await dashboard() assert 'id="select-matching-work"' in html assert 'id="clear-work-selection"' in html assert "workSelection.selectMany(visible)" in html assert "workSelection.clear()" in html assert "matches selected" in html assert ".selection-scope-actions button { min-height:44px;" in html assert "max-width:100%" in html @pytest.mark.anyio async def test_issue_sheet_formats_due_date_as_a_calendar_day_without_local_timestamp_conversion(): source = await dashboard() assert "formatCalendarDueDate(detail.due_date)" in source assert "new Date(detail.due_date).toLocaleDateString()" not in source def test_queue_finder_matches_repository_number_and_title_without_reordering(): script = f""" const work = require({json.dumps(str(MY_WORK))}); const items = [ {{repository:'stackchain/api', key:'stackchain/api#42', number:42, title:'Retry failed deploy'}}, {{repository:'stackchain/web', key:'stackchain/web#7', number:7, title:'Polish mobile queue'}}, {{repository:'other/repo', key:'other/repo#42', number:42, title:'Unrelated task'}}, ]; process.stdout.write(JSON.stringify({{ repo:work.findQueueItems(items, 'STACKCHAIN/API').map(item => item.key), number:work.findQueueItems(items, '#42').map(item => item.key), title:work.findQueueItems(items, 'mobile queue').map(item => item.key), clear:work.findQueueItems(items, ' ').map(item => item.key), }})); """ result = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert result.returncode == 0, result.stderr assert json.loads(result.stdout) == { "repo": ["stackchain/api#42"], "number": ["stackchain/api#42", "other/repo#42"], "title": ["stackchain/web#7"], "clear": ["stackchain/api#42", "stackchain/web#7", "other/repo#42"], } def test_mobile_agenda_groups_assigned_deadlines_in_local_seven_day_horizon(): script = f""" const work = require({json.dumps(str(MY_WORK))}); const now = new Date('2026-08-12T12:00:00'); const items = [ {{kind:'issue', key:'o/r#5', repository:'o/r', number:5, is_assigned:true, due_date:'2026-08-11T18:00:00'}}, {{kind:'issue', key:'o/r#3', repository:'o/r', number:3, is_assigned:true, due_date:'2026-08-12T17:00:00'}}, {{kind:'issue', key:'a/r#9', repository:'a/r', number:9, is_assigned:true, due_date:'2026-08-13T09:00:00'}}, {{kind:'issue', key:'a/r#2', repository:'a/r', number:2, is_assigned:true, due_date:'2026-08-18T09:00:00'}}, {{kind:'issue', key:'a/r#1', repository:'a/r', number:1, is_assigned:true, due_date:'2026-08-20T09:00:00'}}, {{kind:'issue', key:'a/r#7', repository:'a/r', number:7, is_assigned:false, due_date:'2026-08-13T08:00:00'}}, {{kind:'pull', key:'a/r#8', repository:'a/r', number:8, is_assigned:true, due_date:'2026-08-13T08:00:00'}}, {{kind:'issue', key:'a/r#6', repository:'a/r', number:6, is_assigned:true}}, ]; process.stdout.write(JSON.stringify(work.agendaMyWork(items, now).map(item => [item.key, item.agenda_group]))); """ result = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert result.returncode == 0, result.stderr assert json.loads(result.stdout) == [ ["o/r#5", "Overdue"], ["o/r#3", "Today"], ["a/r#9", "Tomorrow"], ["a/r#2", "Next 7 days"], ] def test_mobile_agenda_sorts_equal_deadlines_by_repository_and_number(): script = f""" const work = require({json.dumps(str(MY_WORK))}); const items = [ {{kind:'issue', key:'z/r#1', repository:'z/r', number:1, is_assigned:true, due_date:'2026-08-14T09:00:00'}}, {{kind:'issue', key:'a/r#10', repository:'a/r', number:10, is_assigned:true, due_date:'2026-08-14T09:00:00'}}, {{kind:'issue', key:'a/r#2', repository:'a/r', number:2, is_assigned:true, due_date:'2026-08-14T09:00:00'}}, ]; process.stdout.write(JSON.stringify(work.agendaMyWork(items, new Date('2026-08-12T12:00:00')).map(item => item.key))); """ result = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert result.returncode == 0, result.stderr assert json.loads(result.stdout) == ["a/r#2", "a/r#10", "z/r#1"] def test_mobile_agenda_runs_as_an_ordered_non_durable_work_session(): script = f""" const buildMyWork = require({json.dumps(str(MY_WORK))}); const values = new Map([['stackchain.today-session.v1', 'preserve-today']]); const checkpoint = {{ read:() => null, save:() => {{ throw new Error('Agenda must not save Today'); }}, clear:() => {{ throw new Error('Agenda must not clear Today'); }}, }}; const active = [ {{kind:'issue',repository:'o/r',number:3,title:'Tomorrow',is_assigned:true,due_date:'2026-08-14'}}, {{kind:'issue',repository:'o/r',number:1,title:'Overdue',is_assigned:true,due_date:'2026-08-12'}}, {{kind:'issue',repository:'o/r',number:2,title:'Today',is_assigned:true,due_date:'2026-08-13'}}, {{kind:'issue',repository:'o/r',number:4,title:'No deadline',is_assigned:true}}, ]; const opened = []; let finished = 0; const session = buildMyWork.createWorkSession({{ getItems:() => buildMyWork.agendaMyWork(active, new Date('2026-08-13T12:00:00')), getFilter:() => 'all', checkpoint, checkpointEnabled:() => false, onOpen:item => opened.push(item.title), onProgress:() => {{}}, onFinish:() => {{ finished += 1; }}, }}); const started = session.start(); session.next(); session.next(); const completed = session.next(); process.stdout.write(JSON.stringify({{ started, completed, opened, finished, active:session.active(), today:values.get('stackchain.today-session.v1'), }})); """ result = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert result.returncode == 0, result.stderr assert json.loads(result.stdout) == { "started": True, "completed": False, "opened": ["Overdue", "Today", "Tomorrow"], "finished": 1, "active": False, "today": "preserve-today", } def test_mobile_agenda_checkpoint_is_account_bound_resumable_and_separate_from_today(): script = f""" const buildMyWork = require({json.dumps(str(MY_WORK))}); const values = new Map([['stackchain.today-session.v1', 'preserve-today']]); const storage = {{ getItem:key => values.get(key) || null, setItem:(key, value) => values.set(key, value), removeItem:key => values.delete(key), }}; let login = 'timmy'; const agendaCheckpoint = buildMyWork.createWorkSessionCheckpoint({{ storage, getLogin:() => login, key:'stackchain.agenda-session.v1', }}); const todayCheckpoint = buildMyWork.createWorkSessionCheckpoint({{ storage, getLogin:() => login, key:'stackchain.today-session.v1', }}); let mode = 'agenda'; let items = [ {{kind:'issue',repository:'o/r',number:1,title:'Overdue'}}, {{kind:'issue',repository:'o/r',number:2,title:'Today'}}, {{kind:'issue',repository:'o/r',number:3,title:'Tomorrow'}}, ]; const opened = []; const makeSession = () => buildMyWork.createWorkSession({{ getItems:() => items, getFilter:() => 'all', checkpoint:() => mode === 'agenda' ? agendaCheckpoint : todayCheckpoint, checkpointEnabled:() => true, checkpointedEnabled:() => mode === 'today', onOpen:item => opened.push(item.title), onProgress:() => {{}}, onFinish:() => {{}}, }}); const first = makeSession(); first.start(); first.next(); const saved = JSON.parse(values.get('stackchain.agenda-session.v1')); const todayWhileRunning = first.checkpointed(); login = 'alexander'; const hiddenFromOtherAccount = makeSession().resumable(); login = 'timmy'; items = items.filter(item => item.number !== 2); const resumedSession = makeSession(); const resumed = resumedSession.resume(); resumedSession.end(); process.stdout.write(JSON.stringify({{ saved, todayWhileRunning, hiddenFromOtherAccount, resumed, opened, agendaCleared:!values.has('stackchain.agenda-session.v1'), today:values.get('stackchain.today-session.v1'), }})); """ result = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert result.returncode == 0, result.stderr state = json.loads(result.stdout) assert state["saved"] == { "version": 1, "login": "timmy", "identity": "issue:o/r:2:", "index": 1, } assert state["todayWhileRunning"] is False assert state["hiddenFromOtherAccount"] is False assert state["resumed"] is True assert state["opened"] == ["Overdue", "Today", "Tomorrow"] assert state["agendaCleared"] is True assert state["today"] == "preserve-today" @pytest.mark.anyio async def test_mobile_agenda_launcher_starts_or_resumes_the_durable_agenda_session(): html = await dashboard() assert '' in html assert "openAgenda: openAgendaSession" in html assert "const agendaSessionLauncher = createAgendaSessionLauncher({" in html assert "discover: completeAgendaIssues" in html assert "hasMore: () => Boolean(workPagination.issue?.has_more)" in html assert "key: 'stackchain.agenda-session.v1'" in html assert "checkpoint: () => selectedWorkFilter === 'agenda' ? agendaSessionCheckpoint : sessionCheckpoint" in html assert "checkpointEnabled: () => ['today', 'agenda'].includes(selectedWorkFilter)" in html assert "checkpointedEnabled: () => selectedWorkFilter === 'today'" in html assert "agendaSessionCheckpoint.read() ? 'Resume Agenda' : 'Start Agenda'" in html assert "window.location.hash = '#/my-work/agenda'" in html @pytest.mark.anyio async def test_mobile_agenda_launcher_starts_the_rendered_queue_and_finishes_in_agenda(): html = await dashboard() assert "selectedWorkFilter === 'agenda' ? agendaMyWork(activeMyWork)" in html assert "selectedWorkFilter === 'agenda' ? 'all'" in html assert "agendaSessionCheckpoint.read() ? 'Resume Agenda' : 'Start Agenda'" in html assert "const sessionItems = workSession.items()" in html assert "qs('#my-work-action-status').textContent = 'Agenda complete.'" in html assert "checkpointEnabled: () => ['today', 'agenda'].includes(selectedWorkFilter)" in html @pytest.mark.parametrize("timezone_name", ["Asia/Tokyo", "America/Los_Angeles"]) def test_mobile_agenda_preserves_the_gitea_calendar_day_in_every_timezone(timezone_name): script = f""" const work = require({json.dumps(str(MY_WORK))}); const now = new Date(2026, 7, 13, 12, 0, 0); const built = work({{ user:{{login:'timmy'}}, notifications:[], pull_requests:[], issues:[{{number:1,title:'Ship',repository:'o/r',labels:[],assignees:['timmy'],due_date:'2026-08-13T23:59:59Z'}}], }}, now); process.stdout.write(JSON.stringify({{ dueLabel:built[0].due_label, agenda:work.agendaMyWork(built, now).map(item => [item.key, item.agenda_group]), }})); """ environment = {**os.environ, "TZ": timezone_name} result = subprocess.run( ["node", "-e", script], capture_output=True, text=True, env=environment ) assert result.returncode == 0, result.stderr assert json.loads(result.stdout) == { "dueLabel": "Due today", "agenda": [["o/r#1", "Today"]], } def test_agenda_pager_loads_every_issue_page_single_flight_and_retries_failed_page(): script = f""" const work = require({json.dumps(str(MY_WORK))}); const calls = []; let failPage = 3; let items = [{{id:1, title:'first'}}]; let pagination = {{issue:{{page:1,total:4,has_more:true}}}}; const pager = work.createWorkPager({{ load: async (stream, page) => {{ calls.push(page); await new Promise(resolve => setTimeout(resolve, 5)); if (page === failPage) throw new Error('offline'); return {{page,total:4,has_more:page < 4,items:[{{id:page,title:'page '+page}}]}}; }}, onItems: (_stream, next) => {{ items = next; }}, onPagination: next => {{ pagination = next; }}, onStatus: () => {{}}, }}); pager.reset(pagination); async function run() {{ const first = pager.loadAll('issue', () => items); const duplicate = pager.loadAll('issue', () => items); const failed = await first; const samePromise = first === duplicate; failPage = 0; const retried = await pager.loadAll('issue', () => items); process.stdout.write(JSON.stringify({{ failed, samePromise, retried, calls, ids:items.map(item => item.id), pagination, }})); }} run(); """ result = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert result.returncode == 0, result.stderr assert json.loads(result.stdout) == { "failed": False, "samePromise": True, "retried": True, "calls": [2, 3, 3, 4], "ids": [1, 2, 3, 4], "pagination": {"issue": {"page": 4, "total": 4, "has_more": False}}, } @pytest.mark.anyio async def test_agenda_activation_checks_all_issue_pages_before_showing_empty_state(): html = await dashboard() assert "workPager.loadAll('issue'" in html assert "Checking all assigned deadlines…" in html assert "Agenda check paused. Retry to check older assigned deadlines." in html assert "selectedWorkFilter === 'agenda' && workPagination.issue?.has_more" in html @pytest.mark.anyio async def test_mobile_queue_finder_is_labeled_thumb_safe_and_offers_older_search(): html = await dashboard() assert '