import json import subprocess from pathlib import Path import pytest from src.views import dashboard MY_WORK = Path(__file__).parents[1] / "frontend" / "my-work.js" REVIEW_SHEET = Path(__file__).parents[1] / "frontend" / "review-sheet.js" ISSUE_SHEET = Path(__file__).parents[1] / "frontend" / "issue-sheet.js" 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" PICK_WORK = Path(__file__).parents[1] / "frontend" / "pick-work.js" WORK_ROUTE = Path(__file__).parents[1] / "frontend" / "work-route.js" def test_work_routes_round_trip_all_sheet_kinds_and_reject_unsafe_fragments(): script = f""" const routes = require({json.dumps(str(WORK_ROUTE))}); const inputs = [ {{kind:'issue',repository:'stackchain/api',number:17}}, {{kind:'pull',repository:'stackchain/dashboard',number:42}}, {{kind:'review',repository:'stackchain/dashboard',number:42}}, {{kind:'update',notification_id:913}}, ]; process.stdout.write(JSON.stringify({{ paths: inputs.map(routes.serialize), parsed: inputs.map(item => routes.parse(routes.serialize(item))), invalid: [ '#/my-work/issue/../../etc/1', '#/my-work/issue/stackchain/api/not-a-number', '#/my-work/update/-1', '#/other/issue/stackchain/api/1', ].map(routes.parse), }})); """ result = subprocess.run( ["node", "-e", script], check=True, capture_output=True, text=True ) assert json.loads(result.stdout) == { "paths": [ "#/my-work/issue/stackchain/api/17", "#/my-work/pull/stackchain/dashboard/42", "#/my-work/review/stackchain/dashboard/42", "#/my-work/update/913", ], "parsed": [ {"kind": "issue", "repository": "stackchain/api", "number": 17}, {"kind": "pull", "repository": "stackchain/dashboard", "number": 42}, {"kind": "review", "repository": "stackchain/dashboard", "number": 42}, {"kind": "update", "notification_id": 913}, ], "invalid": [None, None, None, None], } def test_work_route_controller_restores_direct_links_and_uses_history_for_close(): script = f""" const routes = require({json.dumps(str(WORK_ROUTE))}); const listeners = {{}}; const location = {{hash:'#/my-work/issue/stackchain/api/17', href:'https://forge.example/dashboard/#/my-work/issue/stackchain/api/17'}}; const calls = []; const history = {{ state: null, pushState(state, _, hash) {{ this.state = state; location.hash = hash; calls.push(['push', hash]); }}, replaceState(state, _, hash) {{ this.state = state; location.hash = hash; calls.push(['replace', hash]); }}, back() {{ calls.push(['back']); location.hash = ''; listeners.popstate(); }}, }}; const controller = routes.createController({{ location, history, eventTarget: {{addEventListener(name, fn) {{ listeners[name] = fn; }}}}, onOpen(item) {{ calls.push(['open', item.kind, item.repository, item.number || item.notification_id]); }}, onClose() {{ calls.push(['close']); }}, onInvalid() {{ calls.push(['invalid']); }}, }}); controller.start(); controller.setItems([{{kind:'issue',repository:'stackchain/api',number:17}}]); controller.close(); controller.open({{kind:'pull',repository:'stackchain/dashboard',number:9}}); controller.open({{kind:'review',repository:'stackchain/dashboard',number:10}}, {{replace:true}}); process.stdout.write(JSON.stringify({{calls, hash:location.hash}})); """ result = subprocess.run( ["node", "-e", script], check=True, capture_output=True, text=True ) output = json.loads(result.stdout) assert output["calls"] == [ ["open", "issue", "stackchain/api", 17], ["back"], ["close"], ["push", "#/my-work/pull/stackchain/dashboard/9"], ["open", "pull", "stackchain/dashboard", 9], ["replace", "#/my-work/review/stackchain/dashboard/10"], ["open", "review", "stackchain/dashboard", 10], ] assert output["hash"] == "#/my-work/review/stackchain/dashboard/10" def test_work_route_controller_resolves_cold_routes_without_erasing_the_fragment(): script = f""" const routes = require({json.dumps(str(WORK_ROUTE))}); const location = {{hash:'#/my-work/issue/stackchain/api/87'}}; const calls = []; const controller = routes.createController({{ location, history: {{pushState() {{}}, replaceState() {{}}, back() {{}}}}, eventTarget: {{addEventListener() {{}}}}, resolve: async route => {{ calls.push(['resolve', route.kind, route.repository, route.number]); return {{kind:'issue', repository:'stackchain/api', number:87, title:'Older assigned issue'}}; }}, onResolving: route => calls.push(['resolving', route.number]), onOpen: item => calls.push(['open', item.number, item.title]), onClose() {{}}, onInvalid: () => calls.push(['invalid']), onError: () => calls.push(['error']), }}); (async () => {{ controller.start(); controller.setItems([]); await new Promise(resolve => setTimeout(resolve, 0)); process.stdout.write(JSON.stringify({{calls, hash:location.hash}})); }})(); """ result = subprocess.run( ["node", "-e", script], check=True, capture_output=True, text=True ) assert json.loads(result.stdout) == { "calls": [ ["resolving", 87], ["resolve", "issue", "stackchain/api", 87], ["open", 87, "Older assigned issue"], ], "hash": "#/my-work/issue/stackchain/api/87", } def test_work_route_controller_dismisses_only_confirmed_unavailable_routes(): script = f""" const routes = require({json.dumps(str(WORK_ROUTE))}); const location = {{hash:'#/my-work/update/913'}}; const calls = []; const unavailable = new Error('gone'); unavailable.unavailable = true; const controller = routes.createController({{ location, history: {{pushState() {{}}, replaceState() {{}}, back() {{}}}}, eventTarget: {{addEventListener() {{}}}}, resolve: async () => {{ throw unavailable; }}, onResolving() {{}}, onOpen() {{}}, onClose() {{}}, onInvalid: () => calls.push('invalid'), onError: () => calls.push('error'), }}); (async () => {{ controller.start(); controller.setItems([]); await new Promise(resolve => setTimeout(resolve, 0)); process.stdout.write(JSON.stringify(calls)); }})(); """ result = subprocess.run( ["node", "-e", script], check=True, capture_output=True, text=True ) assert json.loads(result.stdout) == ["invalid"] def test_work_route_share_prefers_native_share_and_falls_back_to_clipboard(): script = f""" const routes = require({json.dumps(str(WORK_ROUTE))}); const calls = []; (async () => {{ const native = await routes.share('https://forge.example/dashboard/#/my-work/update/9', {{ share: async payload => calls.push(['native', payload.url]), }}, null); const fallback = await routes.share('https://forge.example/dashboard/#/my-work/update/10', {{}}, {{ writeText: async text => calls.push(['clipboard', text]), }}); process.stdout.write(JSON.stringify({{native, fallback, calls}})); }})(); """ result = subprocess.run( ["node", "-e", script], check=True, capture_output=True, text=True ) assert json.loads(result.stdout) == { "native": "shared", "fallback": "copied", "calls": [ ["native", "https://forge.example/dashboard/#/my-work/update/9"], ["clipboard", "https://forge.example/dashboard/#/my-work/update/10"], ], } @pytest.mark.anyio async def test_dashboard_wires_addressable_work_sheets_back_navigation_and_share(): html = await dashboard() assert '' in html assert 'const workRoute = createWorkRoute.createController({' in html assert 'workRoute.setItems(lastMyWork);' in html assert "api('api/v1/work-route?' + params.toString())" in html assert "Loading shared work item…" in html assert 'id="retry-work-route"' in html assert 'href="' + "' + escAttr(createWorkRoute.serialize(" in html assert '.read-update { min-height:44px; width:100%; display:flex;' in html assert 'workRoute.close();' in html assert html.count('class="share-work-route"') == 4 assert 'createWorkRoute.share(window.location.href, navigator, navigator.clipboard)' in html assert 'Route unavailable · this item is no longer in My Work.' in html def test_my_work_queue_prioritizes_labels_then_reviews_and_keeps_repo_identity(): payload = { "user": {"login": "timmy"}, "issues": [ { "id": 1, "number": 7, "title": "Assigned issue", "state": "open", "repository": "stackchain/mobile", "labels": [], "assignees": ["timmy"], "updated_at": "2026-08-06T12:00:00Z", "url": "https://forge.example/mobile/issues/7", }, { "id": 2, "number": 7, "title": "Priority issue", "state": "open", "repository": "stackchain/api", "labels": ["P0"], "assignees": [], "updated_at": "2026-08-06T11:00:00Z", "url": "https://forge.example/api/issues/7", }, ], "pull_requests": [ { "id": 3, "number": 4, "title": "Review PR", "state": "open", "repository": "stackchain/web", "work_reasons": ["review_requested"], "updated_at": "2026-08-06T13:00:00Z", "url": "https://forge.example/web/pulls/4", } ], } script = f""" const buildMyWork = require({json.dumps(str(MY_WORK))}); const queue = buildMyWork({json.dumps(payload)}); process.stdout.write(JSON.stringify(queue)); """ result = subprocess.run( ["node", "-e", script], check=True, capture_output=True, text=True ) queue = json.loads(result.stdout) assert [item["title"] for item in queue] == [ "Priority issue", "Review PR", "Assigned issue", ] assert queue[0]["key"] == "stackchain/api#7" assert queue[0]["reason"] == "P0 priority" assert queue[1]["reason"] == "Needs your review" assert queue[1]["is_review"] is True assert queue[2]["reason"] == "Assigned to you" def test_my_work_surfaces_and_ranks_due_issues_after_p0_before_ordinary_work(): payload = { "user": {"login": "timmy"}, "issues": [ {"number": 1, "title": "Ordinary", "repository": "stackchain/api", "labels": [], "assignees": ["timmy"], "updated_at": "2026-08-07T12:00:00Z"}, {"number": 2, "title": "Due today", "repository": "stackchain/api", "labels": [], "assignees": ["timmy"], "due_date": "2026-08-07T23:59:59Z"}, {"number": 3, "title": "Overdue", "repository": "stackchain/api", "labels": [], "assignees": ["timmy"], "due_date": "2026-08-06T23:59:59Z"}, {"number": 4, "title": "P0 future", "repository": "stackchain/api", "labels": ["P0"], "assignees": ["timmy"], "due_date": "2026-08-10T23:59:59Z"}, ], "pull_requests": [], } script = f""" const buildMyWork = require({json.dumps(str(MY_WORK))}); const queue = buildMyWork({json.dumps(payload)}, new Date('2026-08-07T12:00:00Z')); process.stdout.write(JSON.stringify(queue.map(item => ({{title:item.title,reason:item.reason,due_label:item.due_label}})))); """ result = subprocess.run( ["node", "-e", script], check=True, capture_output=True, text=True ) assert json.loads(result.stdout) == [ {"title": "P0 future", "reason": "P0 priority", "due_label": "Due Aug 10"}, {"title": "Overdue", "reason": "Overdue", "due_label": "Overdue"}, {"title": "Due today", "reason": "Due today", "due_label": "Due today"}, {"title": "Ordinary", "reason": "Assigned to you"}, ] def test_confirmed_issue_labels_replace_snapshot_and_reprioritize_queue(): payload = { "user": {"login": "timmy"}, "issues": [ {"number": 1, "title": "Older", "repository": "stackchain/api", "labels": [], "assignees": ["timmy"], "updated_at": "2026-08-06T10:00:00Z"}, {"number": 2, "title": "Newer", "repository": "stackchain/api", "labels": [], "assignees": ["timmy"], "updated_at": "2026-08-06T12:00:00Z"}, ], "pull_requests": [], } script = f""" const buildMyWork = require({json.dumps(str(MY_WORK))}); const original = {json.dumps(payload)}; const updated = buildMyWork.replaceIssueLabels(original, 'stackchain/api', 1, ['P0']); process.stdout.write(JSON.stringify({{ titles: buildMyWork(updated).map(item => item.title), labels: updated.issues[0].labels, original: original.issues[0].labels, }})); """ result = subprocess.run( ["node", "-e", script], check=True, capture_output=True, text=True ) assert json.loads(result.stdout) == { "titles": ["Older", "Newer"], "labels": ["P0"], "original": [], } def test_confirmed_issue_content_updates_my_work_without_mutating_snapshot(): payload = { "issues": [{"number": 17, "repository": "stackchain/api", "title": "Old", "body": "Old body"}], "pull_requests": [], } script = f""" const buildMyWork = require({json.dumps(str(MY_WORK))}); const original = {json.dumps(payload)}; const updated = buildMyWork.replaceIssueContent( original, 'stackchain/api', 17, {{title:'Clarified',body:'New body',updated_at:'2026-08-07T10:01:00Z'}} ); process.stdout.write(JSON.stringify({{updated:updated.issues[0],original:original.issues[0]}})); """ result = subprocess.run( ["node", "-e", script], check=True, capture_output=True, text=True ) assert json.loads(result.stdout) == { "updated": { "number": 17, "repository": "stackchain/api", "title": "Clarified", "body": "New body", "updated_at": "2026-08-07T10:01:00Z", }, "original": { "number": 17, "repository": "stackchain/api", "title": "Old", "body": "Old body", }, } def test_issue_release_is_single_flight_and_requires_confirmed_unassignment(): script = f""" const createIssueSheet = require({json.dumps(str(ISSUE_SHEET))}); let calls = 0; let finish; const controller = createIssueSheet({{ fetchJson: () => {{ calls += 1; return new Promise(resolve => {{ finish = resolve; }}); }}, storage: null, }}); const item = {{repository:'stackchain/api', number:17}}; const first = controller.release(item, 'timmy'); const duplicate = controller.release(item, 'timmy'); finish({{number:17, repository:'stackchain/api', assignees:[], available:true}}); Promise.all([first, duplicate]).then(results => process.stdout.write(JSON.stringify({{ calls, same: first === duplicate, results }}))); """ result = subprocess.run( ["node", "-e", script], check=True, capture_output=True, text=True ) output = json.loads(result.stdout) assert output["calls"] == 1 assert output["same"] is True assert output["results"][0]["available"] is True def test_issue_release_rejects_response_that_still_assigns_current_user(): script = f""" const createIssueSheet = require({json.dumps(str(ISSUE_SHEET))}); const controller = createIssueSheet({{ fetchJson: async () => ({{number:17, assignees:['timmy']}}), storage: null, }}); controller.release({{repository:'stackchain/api', number:17}}, 'timmy') .then(() => process.stdout.write('unexpected')) .catch(error => process.stdout.write(error.message)); """ result = subprocess.run( ["node", "-e", script], check=True, capture_output=True, text=True ) assert result.stdout == "Issue release was not confirmed." def test_issue_content_edit_is_single_flight_and_keeps_scoped_draft_until_confirmed(): script = f""" const createIssueSheet = require({json.dumps(str(ISSUE_SHEET))}); const values = new Map(); const storage = {{ getItem:key => values.get(key) || null, setItem:(key,value) => values.set(key,value), removeItem:key => values.delete(key), }}; let calls = []; let finish; const controller = createIssueSheet({{ storage, fetchJson:(url, options) => {{ calls.push({{url, options}}); return new Promise(resolve => {{ finish = resolve; }}); }}, }}); const item = {{repository:'stackchain/api', number:17}}; const draft = {{title:'Clarified scope', body:'Updated body', expectedUpdatedAt:'2026-08-07T10:00:00Z'}}; controller.saveEditDraft(item, draft); const first = controller.updateContent(item, draft); const duplicate = controller.updateContent(item, draft); const during = controller.loadEditDraft(item); finish({{repository:'stackchain/api',number:17,title:'Clarified scope',body:'Updated body',updated_at:'2026-08-07T10:01:00Z'}}); Promise.all([first, duplicate]).then(results => process.stdout.write(JSON.stringify({{ calls:calls.map(call => ({{url:call.url,method:call.options.method,body:JSON.parse(call.options.body)}})), same:first === duplicate, during, after:controller.loadEditDraft(item), results }}))); """ result = subprocess.run( ["node", "-e", script], check=True, capture_output=True, text=True ) output = json.loads(result.stdout) assert output["calls"] == [{ "url": "api/v1/repos/stackchain/api/issues/17/content", "method": "PATCH", "body": { "title": "Clarified scope", "body": "Updated body", "expected_updated_at": "2026-08-07T10:00:00Z", }, }] assert output["same"] is True assert output["during"] == { "title": "Clarified scope", "body": "Updated body", "expectedUpdatedAt": "2026-08-07T10:00:00Z", } assert output["after"] is None assert output["results"][0]["updated_at"] == "2026-08-07T10:01:00Z" def test_issue_due_date_update_is_single_flight_and_keeps_draft_until_confirmed(): script = f""" const createIssueSheet = require({json.dumps(str(ISSUE_SHEET))}); const values = new Map(); const storage = {{ getItem:key => values.get(key) || null, setItem:(key,value) => values.set(key,value), removeItem:key => values.delete(key), }}; let calls = []; let finish; const controller = createIssueSheet({{ storage, fetchJson:(url, options) => {{ calls.push({{url, options}}); return new Promise(resolve => {{ finish = resolve; }}); }}, }}); const item = {{repository:'stackchain/api', number:17}}; const first = controller.updateDueDate(item, '2026-08-09T23:59:59Z'); const duplicate = controller.updateDueDate(item, '2026-08-09T23:59:59Z'); const during = controller.loadDueDateDraft(item); finish({{repository:'stackchain/api',number:17,state:'open',due_date:'2026-08-09T23:59:59Z'}}); Promise.all([first, duplicate]).then(results => process.stdout.write(JSON.stringify({{ calls:calls.map(call => ({{url:call.url,method:call.options.method,body:JSON.parse(call.options.body)}})), same:first === duplicate, during, after:controller.loadDueDateDraft(item), results }}))); """ result = subprocess.run( ["node", "-e", script], check=True, capture_output=True, text=True ) output = json.loads(result.stdout) assert output["calls"] == [{ "url": "api/v1/repos/stackchain/api/issues/17/due-date", "method": "PATCH", "body": {"due_date": "2026-08-09T23:59:59Z"}, }] assert output["same"] is True assert output["during"] == "2026-08-09T23:59:59Z" assert output["after"] is None assert output["results"][0]["due_date"] == "2026-08-09T23:59:59Z" @pytest.mark.anyio async def test_mobile_issue_sheet_exposes_touch_sized_due_date_editor_and_card_badge(): html = await dashboard() assert 'id="issue-due-date" type="date"' in html assert 'id="save-issue-due-date"' in html assert 'id="clear-issue-due-date"' in html assert 'class="pill due-badge"' in html assert '.issue-due-editor input, .issue-due-editor button { min-height:44px;' in html assert 'padding-bottom:calc(10px + env(safe-area-inset-bottom))' in html @pytest.mark.anyio async def test_new_issue_sheet_exposes_touch_safe_release_planning_controls(): html = await dashboard() assert 'id="create-issue-milestone"' in html assert 'id="create-issue-due-date" type="date"' in html assert 'id="create-issue-milestone-status" aria-live="polite"' in html assert '.create-issue-form select, .create-issue-form input[type="date"] { min-height:44px;' in html assert 'issueCapture.loadMilestones(repository)' in html assert "milestoneId: Number(qs('#create-issue-milestone').value) || null" in html assert "dueDate: qs('#create-issue-due-date').value" in html @pytest.mark.anyio async def test_dashboard_launches_share_capture_with_draft_conflict_choices(): html = await dashboard() assert 'Unplanned' in html assert 'id="issue-milestone"' in html assert 'id="save-issue-milestone"' in html assert 'class="pill milestone-badge"' in html assert '.work-milestone-filter, .issue-milestone-editor select, .issue-milestone-editor button { min-height:44px;' in html assert 'getMilestone: () => selectedWorkMilestone' in html assert 'buildMyWork.replaceIssueMilestone(' in html @pytest.mark.anyio async def test_mobile_issue_sheet_puts_reading_before_collapsed_planning_controls(): html = await dashboard() body = html.index('id="issue-sheet-body"') conversation = html.index('

Full conversation

', body) planning = html.index('id="issue-planning"', conversation) labels = html.index('id="issue-label-editor"', planning) milestone = html.index('id="issue-milestone"', planning) assert 'Plan & edit' in html[planning:labels] assert '
' in html assert body < conversation < planning < labels < milestone def test_issue_planning_metadata_is_lazy_cached_and_retryable_after_failure(): script = f""" const createIssueSheet = require({json.dumps(str(ISSUE_SHEET))}); const calls = []; let fail = true; const planning = createIssueSheet.createPlanningLoader({{ loadLabels: async item => {{ calls.push('labels:' + item.number); return [{{id:1,name:'P0'}}]; }}, loadMilestones: async item => {{ calls.push('milestones:' + item.number); if (fail) throw new Error('offline'); return [{{id:9,title:'RC'}}]; }}, }}); const item = {{repository:'stackchain/api', number:17}}; (async () => {{ const before = calls.slice(); let failed = false; try {{ await planning.open(item); }} catch (_error) {{ failed = true; }} fail = false; const first = await planning.open(item); const second = await planning.open(item); process.stdout.write(JSON.stringify({{before, failed, calls, same:first === second, first}})); }})(); """ result = subprocess.run( ["node", "-e", script], check=True, capture_output=True, text=True ) assert json.loads(result.stdout) == { "before": [], "failed": True, "calls": ["labels:17", "milestones:17", "labels:17", "milestones:17"], "same": True, "first": {"labels": [{"id": 1, "name": "P0"}], "milestones": [{"id": 9, "title": "RC"}]}, } @pytest.mark.anyio async def test_opening_issue_defers_planning_requests_until_disclosure_expands(): html = await dashboard() open_handler = html[html.index('async function openIssueSheet'):html.index('function closeIssueSheet')] assert 'loadIssueLabelEditor(' not in open_handler assert 'loadIssueMilestoneEditor(' not in open_handler assert "if (qs('#issue-planning').open) loadIssuePlanning();" in open_handler assert "qs('#issue-planning').addEventListener('toggle'" in html assert 'planningLoader.open(selectedIssue)' in html assert 'id="retry-issue-planning"' in html def test_my_work_reviews_filter_and_summary_are_actionable(): items = [ {"title": "Issue", "kind": "issue", "is_review": False, "is_assigned": True}, {"title": "Assigned PR", "kind": "pull", "is_review": False, "is_assigned": True}, {"title": "Review PR", "kind": "pull", "is_review": True, "is_assigned": False}, ] script = f""" const buildMyWork = require({json.dumps(str(MY_WORK))}); const items = {json.dumps(items)}; process.stdout.write(JSON.stringify({{ reviews: buildMyWork.filterMyWork(items, 'review'), summary: buildMyWork.summarizeMyWork(items), }})); """ result = subprocess.run( ["node", "-e", script], check=True, capture_output=True, text=True ) output = json.loads(result.stdout) assert [item["title"] for item in output["reviews"]] == ["Review PR"] assert output["summary"] == "1 review · 2 assigned" def test_milestone_lane_composes_with_type_filter_and_updates_confirmed_snapshot(): payload = { "issues": [ {"number": 1, "repository": "stackchain/api", "title": "RC issue", "milestone": {"id": 9, "title": "August RC"}}, {"number": 2, "repository": "stackchain/api", "title": "Unplanned", "milestone": None}, ], "pull_requests": [ {"number": 3, "repository": "stackchain/api", "title": "PR"}, ], } script = f""" const buildMyWork = require({json.dumps(str(MY_WORK))}); const original = {json.dumps(payload)}; const queue = buildMyWork(original); const updated = buildMyWork.replaceIssueMilestone( original, 'stackchain/api', 2, {{id:9,title:'August RC'}} ); process.stdout.write(JSON.stringify({{ rc: buildMyWork.filterMyWork(queue, 'issue', '9').map(item => item.title), unplanned: buildMyWork.filterMyWork(queue, 'all', 'unplanned').map(item => item.title), options: buildMyWork.milestoneLanes(queue), updated: updated.issues[1].milestone, original: original.issues[1].milestone, }})); """ result = subprocess.run( ["node", "-e", script], check=True, capture_output=True, text=True ) assert json.loads(result.stdout) == { "rc": ["RC issue"], "unplanned": ["Unplanned"], "options": [{"id": 9, "title": "August RC"}], "updated": {"id": 9, "title": "August RC"}, "original": None, } def test_issue_milestone_editor_is_single_flight_and_keeps_scoped_draft_until_confirmed(): script = f""" const createIssueSheet = require({json.dumps(str(ISSUE_SHEET))}); 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 calls = []; let finish; const controller = createIssueSheet({{ storage, fetchJson:(url, options) => {{ calls.push({{url, options}}); return new Promise(resolve => {{ finish = resolve; }}); }}, }}); const item = {{repository:'stackchain/api', number:17}}; const first = controller.updateMilestone(item, 9); const duplicate = controller.updateMilestone(item, 9); const during = controller.loadMilestoneDraft(item); finish({{repository:'stackchain/api',number:17,state:'open',milestone:{{id:9,title:'August RC'}}}}); Promise.all([first, duplicate]).then(results => process.stdout.write(JSON.stringify({{ calls:calls.map(call => ({{url:call.url,method:call.options.method,body:JSON.parse(call.options.body)}})), same:first === duplicate, during, after:controller.loadMilestoneDraft(item), results }}))); """ result = subprocess.run( ["node", "-e", script], check=True, capture_output=True, text=True ) output = json.loads(result.stdout) assert output["calls"] == [{ "url": "api/v1/repos/stackchain/api/issues/17/milestone", "method": "PATCH", "body": {"milestone_id": 9}, }] assert output["same"] is True assert output["during"] == 9 assert output["after"] is None assert output["results"][0]["milestone"] == {"id": 9, "title": "August RC"} def test_mobile_work_session_follows_filter_and_reconciles_by_identity(): items = [ {"kind": "issue", "repository": "stackchain/api", "number": 1, "title": "First"}, {"kind": "pull", "repository": "stackchain/web", "number": 2, "title": "Second"}, {"kind": "issue", "repository": "stackchain/api", "number": 3, "title": "Third"}, ] script = f""" const buildMyWork = require({json.dumps(str(MY_WORK))}); let items = {json.dumps(items)}; let filter = 'issue'; const opened = []; const progress = []; let finished = 0; const session = buildMyWork.createWorkSession({{ getItems: () => items, getFilter: () => filter, onOpen: item => opened.push(item.title), onProgress: state => progress.push(state), onFinish: () => {{ finished += 1; }}, }}); session.start(); items = [items[2], items[1], items[0]]; session.reconcile(); session.previous(); items = items.filter(item => item.number !== 3); session.complete(); items = []; session.complete(); process.stdout.write(JSON.stringify({{opened, progress, finished, active:session.active()}})); """ result = subprocess.run( ["node", "-e", script], check=True, capture_output=True, text=True ) output = json.loads(result.stdout) assert output["opened"] == ["First", "Third", "First"] assert output["progress"] == [ {"index": 1, "total": 2, "can_previous": False, "can_next": True}, {"index": 2, "total": 2, "can_previous": True, "can_next": False}, {"index": 1, "total": 2, "can_previous": False, "can_next": True}, {"index": 1, "total": 1, "can_previous": False, "can_next": False}, ] assert output["finished"] == 1 assert output["active"] is False def test_mobile_work_session_completion_finishes_when_current_item_is_last(): script = f""" const buildMyWork = require({json.dumps(str(MY_WORK))}); const items = [{{kind:'pull',repository:'stackchain/api',number:9,is_review:true}}]; let finished = 0; const session = buildMyWork.createWorkSession({{ getItems: () => items, getFilter: () => 'review', onOpen: () => {{}}, onProgress: () => {{}}, onFinish: () => {{ finished += 1; }}, }}); session.start(); session.complete(); process.stdout.write(JSON.stringify({{finished,active:session.active()}})); """ result = subprocess.run( ["node", "-e", script], check=True, capture_output=True, text=True ) assert json.loads(result.stdout) == {"finished": 1, "active": False} @pytest.mark.anyio async def test_mobile_work_session_renders_touch_safe_controls_for_every_work_sheet(): html = await dashboard() assert 'id="start-work-session"' in html assert html.count('class="work-session-nav"') == 4 assert html.count('') == 4 assert html.count('' in html assert 'id="issue-edit-status" class="small" aria-live="assertive"' in html assert '.issue-edit-form input, .issue-edit-form textarea, .issue-edit-form button { min-height:44px;' in html assert 'buildMyWork.replaceIssueContent(' in html @pytest.mark.anyio async def test_mobile_find_work_sheet_is_accessible_touch_sized_and_subpath_safe(): html = await dashboard() assert 'id="find-work"' in html assert 'id="find-work-sheet" role="dialog" aria-modal="true"' in html assert 'id="find-work-list"' in html assert 'id="find-work-status" class="small" aria-live="assertive"' in html assert 'id="load-more-available"' in html assert 'static/pick-work.js' in html assert '.find-work-action { min-height:44px;' in html assert 'padding-bottom:calc(18px + env(safe-area-inset-bottom))' in html assert '@media(max-width:320px)' in html assert 'const retainedItems = findWorkController.items();' in html assert 'if (retainedItems.length) renderAvailableIssues(retainedItems);' in html assert 'Refreshing available issues…' in html @pytest.mark.anyio async def test_mobile_find_work_cards_preview_escaped_context_without_extra_requests(): html = await dashboard() assert 'data-preview-index=' in html assert 'aria-expanded="' in html assert 'aria-controls="' in html assert "const detailId = 'find-work-detail-' + index" in html assert 'class="find-work-detail"' in html assert "escapeHtml(item.body || 'No description provided.')" in html assert '>Open in Gitea' in html assert '.find-work-card a, .find-work-more { min-height:44px;' in html assert '.find-work-detail { min-width:0;' in html def test_issue_capture_is_single_flight_and_keeps_draft_until_confirmed_success(): script = f""" const createIssueCapture = require({json.dumps(str(CREATE_ISSUE_SHEET))}); const values = new Map(); const storage = {{ getItem:key => values.get(key) || null, setItem:(key,value) => values.set(key,value), removeItem:key => values.delete(key), }}; let calls = []; let release; const capture = createIssueCapture({{ storage, fetchJson: (url, options) => {{ calls.push({{url, options}}); return new Promise(resolve => {{ release = () => resolve({{ id:81, number:17, title:'Capture work', state:'open', repository:'stackchain/api', labels:[], assignees:['timmy'], url:'https://forge.example/issues/17' }}); }}); }}, }}); const draft = {{repository:'stackchain/api', title:'Capture work', body:'Context', labelIds:[3]}}; capture.saveDraft(draft); const first = capture.submit(draft); const duplicate = capture.submit(draft); const during = capture.loadDraft(); release(); Promise.all([first, duplicate]).then(results => process.stdout.write(JSON.stringify({{ calls:calls.map(call => ({{url:call.url, method:call.options.method, body:JSON.parse(call.options.body)}})), during, after:capture.loadDraft(), results }}))); """ result = subprocess.run( ["node", "-e", script], check=True, capture_output=True, text=True ) output = json.loads(result.stdout) assert output["calls"] == [{ "url": "api/v1/repos/stackchain/api/issues", "method": "POST", "body": {"title": "Capture work", "body": "Context", "label_ids": [3]}, }] assert output["during"] == { "repository": "stackchain/api", "title": "Capture work", "body": "Context", "labelIds": [3], } assert output["after"] == { "repository": "", "title": "", "body": "", "labelIds": [] } assert output["results"][0]["number"] == 17 assert output["results"][1]["number"] == 17 def test_issue_capture_reuses_its_persisted_idempotency_key_after_reload(): script = f""" const createIssueCapture = require({json.dumps(str(CREATE_ISSUE_SHEET))}); const values = new Map(); const storage = {{ getItem:key => values.get(key) || null, setItem:(key,value) => values.set(key,value), removeItem:key => values.delete(key), }}; const calls = []; const draft = {{repository:'stackchain/api', title:'Capture work', body:'Context', labelIds:[3]}}; const first = createIssueCapture({{ storage, createOperationId: () => 'operation-177', fetchJson: (_url, options) => {{ calls.push(options.headers['Idempotency-Key']); return Promise.reject(new Error('timeout')); }}, }}); first.submit(draft).catch(() => {{ const restored = createIssueCapture({{ storage, createOperationId: () => 'must-not-replace-operation-177', fetchJson: (_url, options) => {{ calls.push(options.headers['Idempotency-Key']); return Promise.resolve({{number:17, title:'Capture work'}}); }}, }}); const before = restored.loadDraft(); restored.submit(before).then(issue => process.stdout.write(JSON.stringify({{ calls, before, after:restored.loadDraft(), number:issue.number }}))); }}); """ result = subprocess.run( ["node", "-e", script], check=True, capture_output=True, text=True ) output = json.loads(result.stdout) assert output["calls"] == ["operation-177", "operation-177"] assert output["before"] == { "repository": "stackchain/api", "title": "Capture work", "body": "Context", "labelIds": [3], } assert output["after"] == { "repository": "", "title": "", "body": "", "labelIds": [] } assert output["number"] == 17 def test_issue_capture_normalizes_shared_mobile_content_without_repeating_source_url(): script = f""" const createIssueCapture = require({json.dumps(str(CREATE_ISSUE_SHEET))}); const normalized = createIssueCapture.normalizeSharedContent({{ title: ' Production alert ', text: 'Latency crossed the threshold https://status.example/incidents/42', url: 'https://status.example/incidents/42', }}); process.stdout.write(JSON.stringify(normalized)); """ result = subprocess.run( ["node", "-e", script], check=True, capture_output=True, text=True ) assert json.loads(result.stdout) == { "title": "Production alert", "body": "Latency crossed the threshold https://status.example/incidents/42", } def test_issue_capture_suggests_a_title_when_mobile_share_only_contains_text(): script = f""" const createIssueCapture = require({json.dumps(str(CREATE_ISSUE_SHEET))}); process.stdout.write(JSON.stringify(createIssueCapture.normalizeSharedContent({{ text: 'Investigate checkout latency before the release window opens. More diagnostic context follows.' }}))); """ result = subprocess.run( ["node", "-e", script], check=True, capture_output=True, text=True ) assert json.loads(result.stdout) == { "title": "Investigate checkout latency before the release window opens.", "body": "Investigate checkout latency before the release window opens. More diagnostic context follows.", } def test_issue_capture_keeps_existing_draft_until_shared_content_is_accepted(): script = f""" const createIssueCapture = require({json.dumps(str(CREATE_ISSUE_SHEET))}); const values = new Map(); const storage = {{ getItem:key => values.get(key) || null, setItem:(key,value) => values.set(key,value), removeItem:key => values.delete(key), }}; let capture = createIssueCapture({{storage, fetchJson:()=>Promise.resolve({{}})}}); capture.saveDraft({{ repository:'stackchain/api', title:'Existing draft', body:'Keep me', labelIds:[3], milestoneId:9, }}); const staged = capture.stageSharedContent({{title:'Shared alert', text:'Investigate', url:'https://status.example/42'}}); const before = capture.loadDraft(); capture = createIssueCapture({{storage, fetchJson:()=>Promise.resolve({{}})}}); const pendingAfterReload = capture.pendingSharedContent(); const accepted = capture.acceptSharedContent(); process.stdout.write(JSON.stringify({{staged, before, pendingAfterReload, accepted, pending:capture.pendingSharedContent()}})); """ result = subprocess.run( ["node", "-e", script], check=True, capture_output=True, text=True ) assert json.loads(result.stdout) == { "staged": {"status": "conflict"}, "before": { "repository": "stackchain/api", "title": "Existing draft", "body": "Keep me", "labelIds": [3], "milestoneId": 9, }, "pendingAfterReload": { "title": "Shared alert", "body": "Investigate\n\nhttps://status.example/42", }, "accepted": { "repository": "stackchain/api", "title": "Shared alert", "body": "Investigate\n\nhttps://status.example/42", "labelIds": [3], "milestoneId": 9, }, "pending": None, } def test_issue_capture_can_resume_existing_draft_and_discard_staged_share(): script = f""" const createIssueCapture = require({json.dumps(str(CREATE_ISSUE_SHEET))}); const values = new Map(); const storage = {{getItem:key=>values.get(key)||null, setItem:(key,value)=>values.set(key,value), removeItem:key=>values.delete(key)}}; const capture = createIssueCapture({{storage, fetchJson:()=>Promise.resolve({{}})}}); capture.saveDraft({{repository:'stackchain/api', title:'Existing', body:'Keep', labelIds:[]}}); capture.stageSharedContent({{title:'Incoming', text:'Replace'}}); capture.discardSharedContent(); process.stdout.write(JSON.stringify({{draft:capture.loadDraft(), pending:capture.pendingSharedContent()}})); """ result = subprocess.run( ["node", "-e", script], check=True, capture_output=True, text=True ) assert json.loads(result.stdout) == { "draft": {"repository": "stackchain/api", "title": "Existing", "body": "Keep", "labelIds": []}, "pending": None, } def test_issue_capture_loads_repository_labels_with_priorities_first(): script = f""" const createIssueCapture = require({json.dumps(str(CREATE_ISSUE_SHEET))}); const calls = []; const capture = createIssueCapture({{ storage: {{getItem:()=>null, setItem:()=>{{}}, removeItem:()=>{{}}}}, fetchJson: url => {{ calls.push(url); return Promise.resolve([ {{id:8,name:'frontend',color:'1d76db'}}, {{id:3,name:'P0',color:'d73a4a'}}, {{id:5,name:'critical',color:'b60205'}} ]); }}, }}); capture.loadLabels('stackchain/api').then(labels => process.stdout.write(JSON.stringify({{calls,labels}}))); """ result = subprocess.run( ["node", "-e", script], check=True, capture_output=True, text=True ) output = json.loads(result.stdout) assert output["calls"] == ["api/v1/repos/stackchain/api/labels"] assert [label["name"] for label in output["labels"]] == ["P0", "critical", "frontend"] def test_issue_capture_persists_and_submits_milestone_and_due_date(): script = f""" const createIssueCapture = require({json.dumps(str(CREATE_ISSUE_SHEET))}); const values = new Map(); const calls = []; const storage = {{ getItem:key => values.get(key) || null, setItem:(key,value) => values.set(key,value), removeItem:key => values.delete(key), }}; const capture = createIssueCapture({{ storage, createOperationId: () => 'planned-operation', fetchJson: (url, options) => {{ calls.push({{url, body:options?.body ? JSON.parse(options.body) : null}}); if (url.endsWith('/milestones')) return Promise.resolve([{{id:9,title:'August RC'}}]); return Promise.resolve({{number:221, milestone:{{id:9,title:'August RC'}}, due_date:'2026-08-31T23:59:59Z'}}); }}, }}); const draft = {{repository:'stackchain/api', title:'Ship plan', body:'', labelIds:[], milestoneId:9, dueDate:'2026-08-31'}}; capture.saveDraft(draft); Promise.all([capture.loadMilestones('stackchain/api'), capture.submit(capture.loadDraft())]).then(results => process.stdout.write(JSON.stringify({{calls, stored:results[1], milestones:results[0]}})) ); """ result = subprocess.run( ["node", "-e", script], check=True, capture_output=True, text=True ) output = json.loads(result.stdout) assert output["calls"] == [ {"url": "api/v1/repos/stackchain/api/milestones", "body": None}, { "url": "api/v1/repos/stackchain/api/issues", "body": { "title": "Ship plan", "body": "", "label_ids": [], "milestone_id": 9, "due_date": "2026-08-31T23:59:59Z", }, }, ] assert output["milestones"] == [{"id": 9, "title": "August RC"}] assert output["stored"]["number"] == 221 def test_unread_updates_enrich_matching_work_and_keep_unassigned_mentions_actionable(): payload = { "user": {"login": "timmy"}, "issues": [{ "id": 1, "number": 7, "title": "Assigned issue", "repository": "stackchain/api", "labels": [], "assignees": ["timmy"], "updated_at": "2026-08-06T10:00:00Z", "url": "https://forge.example/stackchain/api/issues/7", }], "pull_requests": [], "notifications": [ { "id": 42, "number": 7, "title": "Assigned issue", "repository": "stackchain/api", "subject_type": "Issue", "unread": True, "updated_at": "2026-08-06T12:00:00Z", "url": "https://forge.example/stackchain/api/issues/7#issuecomment-9", }, { "id": 43, "number": 8, "title": "Mention only", "repository": "stackchain/web", "subject_type": "Issue", "unread": True, "updated_at": "2026-08-06T13:00:00Z", "url": "https://forge.example/stackchain/web/issues/8#issuecomment-2", }, ], } script = f""" const buildMyWork = require({json.dumps(str(MY_WORK))}); const queue = buildMyWork({json.dumps(payload)}); process.stdout.write(JSON.stringify({{ queue, updates: buildMyWork.filterMyWork(queue, 'update'), counts: buildMyWork.countMyWork(queue), summary: buildMyWork.summarizeMyWork(queue), }})); """ result = subprocess.run( ["node", "-e", script], check=True, capture_output=True, text=True ) output = json.loads(result.stdout) assert len(output["queue"]) == 2 assert [item["key"] for item in output["updates"]] == ["stackchain/web#8", "stackchain/api#7"] assert output["updates"][0]["kind"] == "update" assert output["updates"][1]["kind"] == "issue" assert output["updates"][1]["url"].endswith("#issuecomment-9") assert output["counts"] == {"all": 2, "issue": 1, "pull": 0, "review": 0, "update": 2} assert output["summary"] == "2 unread updates · 0 reviews · 1 assigned" def test_unread_update_correlation_distinguishes_issue_and_pull_with_same_number(): payload = { "user": {"login": "timmy"}, "issues": [{"number": 7, "title": "Issue seven", "repository": "stackchain/api", "url": "https://forge.example/issues/7"}], "pull_requests": [{"number": 7, "title": "Pull seven", "repository": "stackchain/api", "url": "https://forge.example/pulls/7"}], "notifications": [{ "id": 42, "number": 7, "title": "Issue seven", "repository": "stackchain/api", "subject_type": "Issue", "unread": True, "url": "https://forge.example/issues/7#comment-1", }], } script = f""" const buildMyWork = require({json.dumps(str(MY_WORK))}); process.stdout.write(JSON.stringify(buildMyWork({json.dumps(payload)}))); """ result = subprocess.run( ["node", "-e", script], check=True, capture_output=True, text=True ) queue = json.loads(result.stdout) issue = next(item for item in queue if item["kind"] == "issue") pull = next(item for item in queue if item["kind"] == "pull") assert issue["has_update"] is True assert issue["url"].endswith("#comment-1") assert pull["has_update"] is False def test_notification_identity_survives_merge_and_acknowledgement_preserves_assigned_work(): payload = { "user": {"login": "timmy"}, "issues": [{ "number": 7, "title": "Assigned issue", "repository": "stackchain/api", "assignees": ["timmy"], "url": "https://forge.example/issues/7", }], "notifications": [ {"id": 42, "number": 7, "title": "Assigned issue", "repository": "stackchain/api", "subject_type": "Issue", "unread": True, "url": "https://forge.example/issues/7#comment"}, {"id": 43, "number": 8, "title": "Mention", "repository": "stackchain/web", "subject_type": "Issue", "unread": True, "url": "https://forge.example/issues/8"}, ], } script = f""" const buildMyWork = require({json.dumps(str(MY_WORK))}); const before = buildMyWork({json.dumps(payload)}); const afterMerged = buildMyWork.acknowledgeNotification(before, 42); const afterStandalone = buildMyWork.acknowledgeNotification(before, 43); process.stdout.write(JSON.stringify({{before, afterMerged, afterStandalone}})); """ result = subprocess.run( ["node", "-e", script], check=True, capture_output=True, text=True ) output = json.loads(result.stdout) assigned = next(item for item in output["before"] if item["kind"] == "issue") assert assigned["notification_id"] == 42 assert assigned["has_update"] is True assert len(output["afterMerged"]) == 2 acknowledged = next(item for item in output["afterMerged"] if item["kind"] == "issue") assert acknowledged["has_update"] is False assert "notification_id" not in acknowledged assert [item["notification_id"] for item in output["afterStandalone"] if item["has_update"]] == [42] def test_notification_acknowledger_is_single_flight_and_rolls_back_on_failure(): script = f""" const buildMyWork = require({json.dumps(str(MY_WORK))}); const original = [{{kind:'update', notification_id:42, has_update:true}}]; let calls = 0; let release; const states = []; const statuses = []; const controller = buildMyWork.createNotificationAcknowledger({{ markRead: () => {{ calls += 1; return new Promise((resolve, reject) => {{ release = reject; }}); }}, onItems: items => states.push(items), onStatus: status => statuses.push(status), }}); const first = controller.acknowledge(original, 42); const duplicate = controller.acknowledge(original, 42); release(new Error('offline')); Promise.all([first, duplicate]).then(results => process.stdout.write(JSON.stringify({{ calls, states, statuses, results }}))); """ result = subprocess.run( ["node", "-e", script], check=True, capture_output=True, text=True ) output = json.loads(result.stdout) assert output["calls"] == 1 assert output["states"] == [[], [{"kind": "update", "notification_id": 42, "has_update": True}]] assert output["statuses"] == ["Marking update read…", "Could not mark update read. Retry."] assert output["results"] == [False, False] def test_bulk_notification_acknowledger_deduplicates_and_keeps_partial_failures_retryable(): script = f""" const buildMyWork = require({json.dumps(str(MY_WORK))}); const original = [ {{kind:'issue', key:'repo#1', notification_id:42, has_update:true}}, {{kind:'update', key:'repo#2', notification_id:43, has_update:true}}, {{kind:'update', key:'repo#3', notification_id:44, has_update:true}}, ]; let calls = 0; let release; const states = []; const statuses = []; const controller = buildMyWork.createBulkNotificationAcknowledger({{ markRead: ids => {{ calls += 1; return new Promise(resolve => {{ release = () => resolve({{marked:[42,43], failed:[44]}}); }}); }}, onItems: items => states.push(items), onStatus: status => statuses.push(status), }}); const first = controller.acknowledge(original, [42, 43, 42, 44]); const duplicate = controller.acknowledge(original, [42, 43, 44]); release(); Promise.all([first, duplicate]).then(results => process.stdout.write(JSON.stringify({{ calls, states, statuses, results, retryIds: buildMyWork.notificationIds(states.at(-1)), }}))); """ result = subprocess.run( ["node", "-e", script], check=True, capture_output=True, text=True ) output = json.loads(result.stdout) assert output["calls"] == 1 assert output["results"] == [ {"marked": [42, 43], "failed": [44]}, False, ] assert output["states"] == [[ {"kind": "issue", "key": "repo#1", "has_update": False}, {"kind": "update", "key": "repo#3", "notification_id": 44, "has_update": True}, ]] assert output["retryIds"] == [44] assert output["statuses"] == [ "Marking 3 updates read…", "2 marked read · 1 could not be updated — retry.", ] def test_notification_pager_is_single_flight_and_merges_unique_updates(): script = f""" const buildMyWork = require({json.dumps(str(MY_WORK))}); let calls = 0; let release; const pages = []; const notifications = []; const statuses = []; const pager = buildMyWork.createNotificationPager({{ load: page => {{ calls += 1; return new Promise(resolve => {{ release = () => resolve({{ items: [{{id:50, title:'duplicate'}}, {{id:51, title:'older'}}], page, total: 75, has_more: false, }}); }}); }}, onNotifications: items => notifications.push(items), onPagination: page => pages.push(page), onStatus: status => statuses.push(status), }}); pager.reset({{page:1, total:75, has_more:true}}); const existing = [{{id:50, title:'newer'}}]; const first = pager.loadMore(existing); const duplicate = pager.loadMore(existing); release(); Promise.all([first, duplicate]).then(results => process.stdout.write(JSON.stringify({{ calls, pages, notifications, statuses, results }}))); """ result = subprocess.run( ["node", "-e", script], check=True, capture_output=True, text=True ) output = json.loads(result.stdout) assert output["calls"] == 1 assert output["notifications"] == [[ {"id": 50, "title": "newer"}, {"id": 51, "title": "older"}, ]] assert output["pages"][-1] == {"page": 2, "total": 75, "has_more": False} assert output["statuses"] == ["Loading older updates…", "75 of 75 unread updates loaded."] assert output["results"] == [True, False] def test_notification_pager_keeps_loaded_updates_and_retries_the_same_page_after_failure(): script = f""" const buildMyWork = require({json.dumps(str(MY_WORK))}); const states = []; const statuses = []; const requested = []; const pager = buildMyWork.createNotificationPager({{ load: async page => {{ requested.push(page); throw new Error('offline'); }}, onNotifications: items => states.push(items), onPagination: () => {{}}, onStatus: status => statuses.push(status), }}); pager.reset({{page:2, total:125, has_more:true}}); pager.loadMore([{{id:1}}]).then(result => pager.loadMore([{{id:1}}]).then(retry => process.stdout.write(JSON.stringify({{requested, states, statuses, result, retry}})) ) ); """ result = subprocess.run( ["node", "-e", script], check=True, capture_output=True, text=True ) assert json.loads(result.stdout) == { "requested": [3, 3], "states": [], "statuses": [ "Loading older updates…", "Could not load older updates. Retry.", "Loading older updates…", "Could not load older updates. Retry.", ], "result": False, "retry": False, } def test_notification_reader_marks_current_read_and_opens_next_update(): script = f""" const buildMyWork = require({json.dumps(str(MY_WORK))}); const original = [ {{kind:'update', key:'repo#1', notification_id:42, has_update:true, title:'First'}}, {{kind:'issue', key:'repo#2', notification_id:43, has_update:true, title:'Second'}}, ]; const loaded = []; const opened = []; const details = []; const states = []; const statuses = []; const closed = []; const reader = buildMyWork.createNotificationReader({{ load: async id => {{ loaded.push(id); return {{id, latest_comment:{{body:'Comment ' + id}}}}; }}, markRead: async id => id, onOpen: item => opened.push(item.notification_id), onDetail: detail => details.push(detail.id), onItems: items => states.push(items), onStatus: status => statuses.push(status), onClose: () => closed.push(true), }}); reader.open(original[0], original).then(() => reader.markReadAndNext(original).then(result => process.stdout.write(JSON.stringify({{loaded, opened, details, states, statuses, closed, result}})) ) ); """ result = subprocess.run( ["node", "-e", script], check=True, capture_output=True, text=True ) output = json.loads(result.stdout) assert output["loaded"] == [42, 43] assert output["opened"] == [42, 43] assert output["details"] == [42, 43] assert output["states"] == [[ {"kind": "issue", "key": "repo#2", "notification_id": 43, "has_update": True, "title": "Second"}, ]] assert output["statuses"] == [ "Loading update…", "Update ready.", "Marking update read…", "Loading update…", "Update ready.", ] assert output["closed"] == [] assert output["result"]["next"]["notification_id"] == 43 def test_notification_reader_keeps_current_update_retryable_when_detail_load_fails(): script = f""" const buildMyWork = require({json.dumps(str(MY_WORK))}); const item = {{kind:'update', notification_id:42, has_update:true}}; const events = []; const reader = buildMyWork.createNotificationReader({{ load: async () => {{ throw new Error('offline'); }}, markRead: async () => {{}}, onOpen: () => events.push('open'), onDetail: () => events.push('detail'), onItems: () => events.push('items'), onStatus: status => events.push(status), onClose: () => events.push('close'), }}); reader.open(item, [item]).then(result => process.stdout.write(JSON.stringify({{result, events}})) ); """ result = subprocess.run( ["node", "-e", script], check=True, capture_output=True, text=True ) assert json.loads(result.stdout) == { "result": False, "events": [ "open", "Loading update…", "Could not load update. Retry or open it in Gitea.", ], } def test_notification_reader_does_not_advance_or_remove_item_when_mark_read_fails(): script = f""" const buildMyWork = require({json.dumps(str(MY_WORK))}); const item = {{kind:'update', notification_id:42, has_update:true}}; const events = []; const reader = buildMyWork.createNotificationReader({{ load: async id => ({{id}}), markRead: async () => {{ throw new Error('offline'); }}, onOpen: () => events.push('open'), onDetail: () => events.push('detail'), onItems: () => events.push('items'), onStatus: status => events.push(status), onClose: () => events.push('close'), }}); reader.open(item, [item]).then(() => reader.markReadAndNext([item]).then(result => process.stdout.write(JSON.stringify({{result, events}})) ) ); """ result = subprocess.run( ["node", "-e", script], check=True, capture_output=True, text=True ) assert json.loads(result.stdout) == { "result": False, "events": [ "open", "Loading update…", "detail", "Update ready.", "Marking update read…", "Could not mark update read. Retry.", ], } def test_notification_reader_pages_conversation_single_flight_and_ignores_stale_update(): script = f""" const build = require({json.dumps(str(MY_WORK))}); const createPager = require({json.dumps(str(CONVERSATION))}); const details = {{ 42: {{id:42, conversation:{{comments:[{{id:41,created_at:'2026-08-07T12:41:00Z'}}],page:3,older_page:2,total:47}}}}, 43: {{id:43, conversation:{{comments:[{{id:90,created_at:'2026-08-07T13:00:00Z'}}],page:1,older_page:null,total:1}}}}, }}; let release; const requested = []; const states = []; const reader = build.createNotificationReader({{ load: async id => details[id], createPager, loadConversation: (id, page) => {{ requested.push([id, page]); return new Promise(resolve => {{ release = () => resolve({{comments:[{{id:21,created_at:'2026-08-07T12:21:00Z'}}],page:2,older_page:1,total:47}}); }}); }}, markRead: async () => {{}}, onOpen: () => {{}}, onDetail: () => {{}}, onConversation: state => states.push(state.comments.map(item => item.id)), onItems: () => {{}}, onStatus: () => {{}}, onClose: () => {{}}, }}); const firstItem = {{notification_id:42}}; const secondItem = {{notification_id:43}}; reader.open(firstItem).then(async () => {{ const first = reader.loadOlder(); const duplicate = reader.loadOlder(); await reader.open(secondItem); release(); await Promise.all([first, duplicate]); process.stdout.write(JSON.stringify({{requested, states}})); }}); """ result = subprocess.run( ["node", "-e", script], check=True, capture_output=True, text=True ) output = json.loads(result.stdout) assert output["requested"] == [[42, 2]] assert output["states"] == [[41], [90]] def test_notification_reader_appends_a_confirmed_reply_exactly_once(): script = f""" const build = require({json.dumps(str(MY_WORK))}); const createPager = require({json.dumps(str(CONVERSATION))}); const states = []; const reader = build.createNotificationReader({{ load: async () => ({{conversation:{{comments:[{{id:41}}],page:1,older_page:null,total:1}}}}), loadConversation: async () => ({{}}), createPager, markRead: async () => {{}}, onOpen: () => {{}}, onDetail: () => {{}}, onConversation: state => states.push(state.comments.map(item => item.id)), onItems: () => {{}}, onStatus: () => {{}}, onClose: () => {{}}, }}); reader.open({{notification_id:42}}).then(() => {{ reader.appendReply({{id:91, author:'timmy', body:'Ship it'}}); reader.appendReply({{id:91, author:'timmy', body:'Ship it'}}); process.stdout.write(JSON.stringify(states)); }}); """ result = subprocess.run( ["node", "-e", script], check=True, capture_output=True, text=True ) assert json.loads(result.stdout) == [[41], [41, 91], [41, 91]] def test_notification_reader_wraps_to_an_earlier_visible_unread_update(): script = f""" const buildMyWork = require({json.dumps(str(MY_WORK))}); const items = [ {{kind:'update', notification_id:42, has_update:true}}, {{kind:'update', notification_id:43, has_update:true}}, ]; const opened = []; const reader = buildMyWork.createNotificationReader({{ load: async id => ({{id}}), markRead: async () => {{}}, onOpen: item => opened.push(item.notification_id), onDetail: () => {{}}, onItems: () => {{}}, onStatus: () => {{}}, onClose: () => {{}}, }}); reader.open(items[1], items).then(() => reader.markReadAndNext(items)).then(result => process.stdout.write(JSON.stringify({{opened, next:result.next.notification_id}})) ); """ result = subprocess.run( ["node", "-e", script], check=True, capture_output=True, text=True ) assert json.loads(result.stdout) == {"opened": [43, 42], "next": 42} def test_notification_reader_closes_and_announces_when_final_update_is_cleared(): script = f""" const buildMyWork = require({json.dumps(str(MY_WORK))}); const item = {{kind:'update', notification_id:42, has_update:true}}; const events = []; const reader = buildMyWork.createNotificationReader({{ load: async id => ({{id}}), markRead: async () => {{}}, onOpen: () => {{}}, onDetail: () => {{}}, onItems: items => events.push(['items', items]), onStatus: status => events.push(['status', status]), onClose: () => events.push(['close']), }}); reader.open(item, [item]).then(() => {{ events.length = 0; return reader.markReadAndNext([item]); }}).then(result => process.stdout.write(JSON.stringify({{result, events}}))); """ result = subprocess.run( ["node", "-e", script], check=True, capture_output=True, text=True ) output = json.loads(result.stdout) assert output["result"] == {"items": [], "next": None} assert output["events"] == [ ["status", "Marking update read…"], ["items", []], ["close"], ["status", "Inbox cleared."], ] def test_conversation_pager_prepends_older_pages_deduplicates_and_appends_once(): script = f""" const createConversationPager = require({json.dumps(str(CONVERSATION))}); let calls = 0; const pager = createConversationPager({{ loadPage: async page => {{ calls += 1; return {{ comments:[{{id:20,body:'duplicate'}},{{id:1,body:'oldest'}}], page, older_page:null, total:3 }}; }} }}); pager.reset({{comments:[{{id:20,body:'middle'}},{{id:21,body:'newest'}}],page:2,older_page:1,total:3}}); const first = pager.loadOlder(); const duplicate = pager.loadOlder(); Promise.all([first, duplicate]).then(() => {{ pager.append({{id:22,body:'posted'}}); pager.append({{id:22,body:'posted'}}); process.stdout.write(JSON.stringify({{calls,same:first===duplicate,state:pager.snapshot()}})); }}); """ result = subprocess.run( ["node", "-e", script], check=True, capture_output=True, text=True ) output = json.loads(result.stdout) assert output["calls"] == 1 assert output["same"] is True assert [comment["id"] for comment in output["state"]["comments"]] == [1, 20, 21, 22] assert output["state"]["older_page"] is None assert output["state"]["total"] == 4 def test_conversation_pager_keeps_loaded_messages_when_older_page_fails(): script = f""" const createConversationPager = require({json.dumps(str(CONVERSATION))}); const pager = createConversationPager({{loadPage: async () => {{ throw new Error('offline'); }}}}); pager.reset({{comments:[{{id:21,body:'newest'}}],page:2,older_page:1,total:21}}); pager.loadOlder().catch(error => process.stdout.write(JSON.stringify({{ error:error.message, state:pager.snapshot() }}))); """ result = subprocess.run( ["node", "-e", script], check=True, capture_output=True, text=True ) output = json.loads(result.stdout) assert output["error"] == "offline" assert output["state"]["comments"] == [{"id": 21, "body": "newest"}] assert output["state"]["older_page"] == 1 def test_issue_sheet_conversation_loads_older_history_through_assigned_boundary(): script = f""" const createIssueSheet = require({json.dumps(str(ISSUE_SHEET))}); const createConversationPager = require({json.dumps(str(CONVERSATION))}); const calls = []; const controller = createIssueSheet({{ createConversationPager, fetchJson: async url => {{ calls.push(url); return {{comments:[{{id:1}}],page:1,older_page:null,total:21}}; }} }}); const pager = controller.conversation( {{repository:'stackchain/api',number:7}}, {{comments:[{{id:21}}],page:2,older_page:1,total:21}} ); pager.loadOlder().then(state => process.stdout.write(JSON.stringify({{calls,state}}))); """ result = subprocess.run( ["node", "-e", script], check=True, capture_output=True, text=True ) output = json.loads(result.stdout) assert output["calls"] == ["api/v1/repos/stackchain/api/issues/7/comments?page=1&limit=20"] assert [comment["id"] for comment in output["state"]["comments"]] == [1, 21] def test_pull_sheet_conversation_loads_older_history_through_assigned_boundary(): script = f""" const createPullSheet = require({json.dumps(str(PULL_SHEET))}); const createConversationPager = require({json.dumps(str(CONVERSATION))}); const calls = []; const controller = createPullSheet({{ createConversationPager, fetchJson: async url => {{ calls.push(url); return {{comments:[{{id:1}}],page:1,older_page:null,total:21}}; }} }}); const pager = controller.conversation( {{repository:'stackchain/api',number:7}}, {{comments:[{{id:21}}],page:2,older_page:1,total:21}} ); pager.loadOlder().then(state => process.stdout.write(JSON.stringify({{calls,state}}))); """ result = subprocess.run( ["node", "-e", script], check=True, capture_output=True, text=True ) output = json.loads(result.stdout) assert output["calls"] == ["api/v1/repos/stackchain/api/pulls/7/comments?page=1&limit=20"] assert [comment["id"] for comment in output["state"]["comments"]] == [1, 21] def test_issue_sheet_loads_encoded_assigned_issue_detail_path(): script = f""" const createIssueSheet = require({json.dumps(str(ISSUE_SHEET))}); let request; const controller = createIssueSheet({{ fetchJson: async (url, options) => {{ request = {{url, accept:options.headers.Accept}}; return {{title:'Fix mobile flow'}}; }} }}); controller.load({{repository:'stackchain/api', number:7}}).then(detail => process.stdout.write(JSON.stringify({{request, title:detail.title}})) ); """ result = subprocess.run( ["node", "-e", script], check=True, capture_output=True, text=True ) assert json.loads(result.stdout) == { "request": { "url": "api/v1/repos/stackchain/api/issues/7/detail", "accept": "application/json", }, "title": "Fix mobile flow", } def test_issue_sheet_loads_labels_through_assigned_issue_boundary(): script = f""" const createIssueSheet = require({json.dumps(str(ISSUE_SHEET))}); let request; const controller = createIssueSheet({{ fetchJson: async (url, options) => {{ request = {{url, accept:options.headers.Accept}}; return [{{id:3, name:'P0'}}]; }} }}); controller.loadLabels({{repository:'stackchain/api', number:7}}).then(labels => process.stdout.write(JSON.stringify({{request, labels}})) ); """ result = subprocess.run( ["node", "-e", script], check=True, capture_output=True, text=True ) assert json.loads(result.stdout) == { "request": { "url": "api/v1/repos/stackchain/api/issues/7/labels", "accept": "application/json", }, "labels": [{"id": 3, "name": "P0"}], } def test_issue_sheet_comment_is_single_flight_and_preserves_draft_until_success(): script = f""" const createIssueSheet = require({json.dumps(str(ISSUE_SHEET))}); const values = new Map(); const storage = {{ getItem:key => values.get(key) || null, setItem:(key,value) => values.set(key,value), removeItem:key => values.delete(key), }}; let calls = 0; let release; const controller = createIssueSheet({{ storage, fetchJson: (url, options) => {{ calls += 1; return new Promise(resolve => {{ release = () => resolve({{id:82, body:'Ready'}}); }}); }}, }}); const item = {{repository:'stackchain/api', number:7}}; controller.saveDraft(item, 'Ready'); const first = controller.comment(item, 'Ready'); const duplicate = controller.comment(item, 'Ready'); const during = controller.loadDraft(item); release(); Promise.all([first, duplicate]).then(results => process.stdout.write(JSON.stringify({{ calls, during, after:controller.loadDraft(item), results }}))); """ result = subprocess.run( ["node", "-e", script], check=True, capture_output=True, text=True ) output = json.loads(result.stdout) assert output["calls"] == 1 assert output["during"] == "Ready" assert output["after"] == "" assert output["results"] == [{"id": 82, "body": "Ready"}, {"id": 82, "body": "Ready"}] def test_issue_sheet_label_save_is_single_flight_and_keeps_selection_until_confirmed(): script = f""" const createIssueSheet = require({json.dumps(str(ISSUE_SHEET))}); const values = new Map(); const storage = {{ getItem:key => values.get(key) || null, setItem:(key,value) => values.set(key,value), removeItem:key => values.delete(key), }}; const calls = []; let release; const controller = createIssueSheet({{ storage, fetchJson: (url, options={{}}) => {{ calls.push({{url, method:options.method || 'GET', body:options.body ? JSON.parse(options.body) : null}}); return new Promise(resolve => {{ release = () => resolve({{number:7, labels:['P0']}}); }}); }}, }}); const item = {{repository:'stackchain/api', number:7}}; const first = controller.updateLabels(item, [3]); const duplicate = controller.updateLabels(item, [3]); const during = controller.loadLabelDraft(item); release(); Promise.all([first, duplicate]).then(results => process.stdout.write(JSON.stringify({{ calls, during, after:controller.loadLabelDraft(item), results }}))); """ result = subprocess.run( ["node", "-e", script], check=True, capture_output=True, text=True ) output = json.loads(result.stdout) assert output["calls"] == [{ "url": "api/v1/repos/stackchain/api/issues/7/labels", "method": "PATCH", "body": {"label_ids": [3]}, }] assert output["during"] == [3] assert output["after"] == [] assert output["results"] == [ {"number": 7, "labels": ["P0"]}, {"number": 7, "labels": ["P0"]}, ] def test_issue_sheet_close_is_single_flight_and_waits_for_confirmed_closed_state(): script = f""" const createIssueSheet = require({json.dumps(str(ISSUE_SHEET))}); let calls = 0; let release; const controller = createIssueSheet({{ fetchJson: (url, options) => {{ calls += 1; return new Promise(resolve => {{ release = () => resolve({{number:7, state:'closed'}}); }}); }}, }}); const item = {{repository:'stackchain/api', number:7}}; const first = controller.close(item); const duplicate = controller.close(item); release(); Promise.all([first, duplicate]).then(results => process.stdout.write(JSON.stringify({{calls, results}})) ); """ result = subprocess.run( ["node", "-e", script], check=True, capture_output=True, text=True ) assert json.loads(result.stdout) == { "calls": 1, "results": [ {"number": 7, "state": "closed"}, {"number": 7, "state": "closed"}, ], } def test_pull_sheet_preserves_comment_draft_and_single_flights_mutations(): script = f""" const createPullSheet = require({json.dumps(str(PULL_SHEET))}); const values = new Map(); const storage = {{getItem:k => values.get(k) || null, setItem:(k,v) => values.set(k,v), removeItem:k => values.delete(k)}}; const calls = []; let releaseComment; const controller = createPullSheet({{ storage, fetchJson: (url, options={{}}) => {{ calls.push({{url, method:options.method || 'GET', body:options.body ? JSON.parse(options.body) : null}}); if (url.endsWith('/comments')) return new Promise(resolve => {{ releaseComment = () => resolve({{id:91, body:'Ship it'}}); }}); return Promise.resolve({{number:7, merged:true, state:'closed'}}); }}, }}); const item = {{repository:'stackchain/api', number:7}}; controller.saveDraft(item, 'Ship it'); const first = controller.comment(item, 'Ship it'); const duplicate = controller.comment(item, 'Ship it'); const during = controller.loadDraft(item); releaseComment(); Promise.all([first, duplicate]).then(async comments => {{ const merges = await Promise.all([controller.merge(item, 'abc123'), controller.merge(item, 'abc123')]); process.stdout.write(JSON.stringify({{calls, during, after:controller.loadDraft(item), comments, merges}})); }}); """ result = subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True) output = json.loads(result.stdout) assert output["during"] == "Ship it" assert output["after"] == "" assert output["calls"] == [ {"url": "api/v1/repos/stackchain/api/pulls/7/comments", "method": "POST", "body": {"body": "Ship it"}}, {"url": "api/v1/repos/stackchain/api/pulls/7/merge", "method": "POST", "body": {"expected_head_sha": "abc123"}}, ] assert len(output["comments"]) == 2 and len(output["merges"]) == 2 def test_pull_sheet_enables_merge_only_for_current_safe_state(): script = f""" const createPullSheet = require({json.dumps(str(PULL_SHEET))}); const states = [ {{state:'open', draft:false, mergeable:true, merged:false, ci_state:'success', head_sha:'abc'}}, {{state:'open', draft:true, mergeable:true, merged:false, ci_state:'success', head_sha:'abc'}}, {{state:'open', draft:false, mergeable:true, merged:false, ci_state:'failure', head_sha:'abc'}}, {{state:'open', draft:false, mergeable:false, merged:false, ci_state:'success', head_sha:'abc'}}, ]; process.stdout.write(JSON.stringify(states.map(createPullSheet.mergeEligibility))); """ result = subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True) output = json.loads(result.stdout) assert output[0] == {"allowed": True, "reason": "Ready to merge"} assert output[1]["allowed"] is False and "draft" in output[1]["reason"].lower() assert output[2]["allowed"] is False and "CI" in output[2]["reason"] assert output[3]["allowed"] is False and "conflict" in output[3]["reason"].lower() def test_pull_sheet_persists_head_scoped_file_review_and_gates_merge(): script = f""" const createPullSheet = require({json.dumps(str(PULL_SHEET))}); const values = new Map(); const storage = {{getItem:k => values.get(k) || null, setItem:(k,v) => values.set(k,v), removeItem:k => values.delete(k)}}; const item = {{repository:'stackchain/api', number:7}}; const detail = {{state:'open', draft:false, mergeable:true, merged:false, ci_state:'success', head_sha:'abc', files:[ {{filename:'src/api.py'}}, {{filename:'frontend/app.js'}} ]}}; const first = createPullSheet({{storage, fetchJson:()=>Promise.resolve()}}); const before = first.reviewState(item, detail); const afterOne = first.toggleReviewed(item, detail, 'src/api.py'); const restored = createPullSheet({{storage, fetchJson:()=>Promise.resolve()}}).reviewState(item, detail); const complete = first.toggleReviewed(item, detail, 'frontend/app.js'); const changedHead = first.reviewState(item, {{...detail, head_sha:'def'}}); process.stdout.write(JSON.stringify({{ before, afterOne, restored, complete, changedHead, blocked:createPullSheet.mergeEligibility(detail, afterOne), allowed:createPullSheet.mergeEligibility(detail, complete), next:first.nextUnreviewed(detail, afterOne), }})); """ result = subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True) output = json.loads(result.stdout) assert output["before"] == {"reviewed": [], "total": 2, "complete": False} assert output["afterOne"]["reviewed"] == ["src/api.py"] assert output["restored"] == output["afterOne"] assert output["complete"]["complete"] is True assert output["changedHead"]["reviewed"] == [] assert output["blocked"] == {"allowed": False, "reason": "Review every changed file before merging"} assert output["allowed"] == {"allowed": True, "reason": "Ready to merge"} assert output["next"] == "frontend/app.js" def test_pull_sheet_lazy_review_is_single_flight_cached_by_head_and_retryable(): script = f""" const createPullSheet = require({json.dumps(str(PULL_SHEET))}); const calls = []; let rejectFirst; const fetchJson = url => {{ calls.push(url); if (calls.length === 1) return new Promise((_resolve, reject) => {{ rejectFirst = reject; }}); return Promise.resolve({{head_sha:'abc123', files:[]}}); }}; const sheet = createPullSheet({{fetchJson, storage:null}}); const item = {{repository:'stackchain/api', number:7}}; const first = sheet.loadReview(item, 'abc123'); const concurrent = sheet.loadReview(item, 'abc123'); rejectFirst(new Error('diff unavailable')); Promise.allSettled([first, concurrent]).then(async failed => {{ const retried = await sheet.loadReview(item, 'abc123'); const cached = await sheet.loadReview(item, 'abc123'); process.stdout.write(JSON.stringify({{ same:first === concurrent, failed:failed.map(result => result.status), retried, cached, calls, }})); }}); """ result = subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True) output = json.loads(result.stdout) assert output["same"] is True assert output["failed"] == ["rejected", "rejected"] assert output["retried"]["head_sha"] == "abc123" assert output["cached"] == output["retried"] assert output["calls"] == [ "api/v1/repos/stackchain/api/pulls/7/review-data", "api/v1/repos/stackchain/api/pulls/7/review-data", ] @pytest.mark.anyio async def test_mobile_pull_sheet_puts_reading_before_collapsed_review_controls(): html = await dashboard() body = html.index('id="pull-sheet-body"') conversation = html.index('

Full conversation

', body) composer = html.index('id="pull-comment-title"', conversation) review = html.index('id="pull-review"', composer) files = html.index('id="pull-files"', review) assert '

Review & merge

' in html assert '
' in html assert 'id="pull-review-retry"' in html assert 'id="pull-review-status"' in html assert body < conversation < composer < review < files def test_pull_sheet_renders_mobile_diff_fallbacks_and_review_controls(): script = f""" const createPullSheet = require({json.dumps(str(PULL_SHEET))}); const escapeHtml = value => String(value).replaceAll('&', '&').replaceAll('<', '<'); const text = createPullSheet.renderFile({{ filename:'src/.py', status:'modified', additions:1, deletions:1, diff_available:true, diff_lines:['@@ -1 +1 @@', '-old', '+new'], diff_truncated:true }}, 0, false, escapeHtml); const binary = createPullSheet.renderFile({{ filename:'static/logo.png', diff_available:false, diff_binary:true, diff_truncated:false }}, 1, true, escapeHtml); process.stdout.write(JSON.stringify({{text, binary}})); """ result = subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True) output = json.loads(result.stdout) assert 'aria-controls="pull-diff-0"' in output["text"] assert "<unsafe>" in output["text"] and "+new" in output["text"] assert "Preview truncated" in output["text"] assert 'data-pull-review-file="src/<unsafe>.py"' in output["text"] assert "Binary file · preview unavailable" in output["binary"] assert 'aria-pressed="true"' in output["binary"] and "Reviewed" in output["binary"] @pytest.mark.anyio async def test_assigned_pulls_open_accessible_mobile_completion_sheet(): html = await dashboard() assert 'id="pull-sheet"' in html and 'aria-modal="true"' in html assert 'class="my-work-card-main pull-trigger"' in html assert 'id="pull-sheet-status"' in html assert 'id="pull-files"' in html and 'id="pull-comments"' in html assert 'id="pull-review-progress"' in html and 'aria-live="polite"' in html assert 'id="next-unreviewed-pull-file"' in html assert 'id="load-older-pull-comments"' in html assert 'id="pull-conversation-status"' in html and 'aria-live="assertive"' in html assert '' in html assert "pullController.conversation(item, detail.conversation)" in html assert "pullConversation.loadOlder()" in html assert "pullConversation.append(comment)" in html assert 'id="pull-comment"' in html and 'maxlength="10000"' in html assert 'id="merge-pull"' in html and 'id="open-pull-gitea"' in html assert '' in html assert "pullController.load(item)" in html assert "createPullSheet.renderFile" in html assert "pullController.toggleReviewed" in html assert "pullController.nextUnreviewed" in html assert ".pull-diff { overflow-x:auto;" in html assert ".pull-file-toggle, .pull-review-file { min-height:44px;" in html assert "window.confirm('Merge ' + selectedPull.key" in html assert "expected_head_sha" in html assert "item.kind === 'pull'" in html and "pull-trigger" in html @pytest.mark.anyio async def test_assigned_issues_open_accessible_mobile_action_sheet_with_safe_mutations(): html = await dashboard() assert 'id="issue-sheet"' in html and 'aria-modal="true"' in html assert 'class="my-work-card-main issue-trigger"' in html assert 'id="close-issue-sheet"' in html assert 'id="retry-issue-load"' in html assert 'id="issue-sheet-body"' in html assert 'id="issue-labels"' in html and 'id="issue-assignees"' in html assert 'id="issue-comments"' in html assert 'id="load-older-issue-comments"' in html assert 'id="issue-conversation-status"' in html and 'aria-live="assertive"' in html assert "issueController.conversation(item, detail.conversation)" in html assert "issueConversation.loadOlder()" in html assert "issueConversation.append(comment)" in html assert '.conversation-more { min-height:44px;' in html assert 'id="issue-comment"' in html and 'maxlength="10000"' in html assert 'id="send-issue-comment"' in html assert 'id="close-issue"' in html assert 'id="open-issue-gitea"' in html and 'rel="noopener noreferrer"' in html assert '.issue-sheet-panel { width:min(560px,100%);' in html assert '.issue-sheet-content { overflow-wrap:anywhere;' in html assert '.issue-sheet-actions button, .issue-sheet-actions a { min-height:44px;' in html assert 'padding-bottom:calc(10px + env(safe-area-inset-bottom));' in html assert '' in html assert "issueController.load(item)" in html assert "issueController.comment(selectedIssue" in html assert "window.confirm('Close ' + selectedIssue.key + '?')" in html assert "issueController.close(selectedIssue)" in html assert "lastMyWork = lastMyWork.filter" in html assert "if (issueTrigger?.isConnected) issueTrigger.focus()" in html assert "e.key === 'Escape' && selectedIssue" in html @pytest.mark.anyio async def test_mobile_issue_sheet_releases_assignment_with_confirmed_local_removal(): html = await dashboard() assert 'id="release-issue"' in html assert 'Release assignment' in html assert "window.confirm('Release ' + selectedIssue.key + ' from your My Work?')" in html assert "issueController.release(selectedIssue, lastContextSnapshot?.user?.login)" in html assert "buildMyWork.removeIssue(" in html def test_remove_issue_updates_snapshot_without_mutating_other_work(): payload = { "issues": [ {"repository": "stackchain/api", "number": 17}, {"repository": "stackchain/web", "number": 18}, ], "pull_requests": [{"repository": "stackchain/api", "number": 17}], } script = f""" const buildMyWork = require({json.dumps(str(MY_WORK))}); const original = {json.dumps(payload)}; const updated = buildMyWork.removeIssue(original, 'stackchain/api', 17); process.stdout.write(JSON.stringify({{updated, original}})); """ result = subprocess.run( ["node", "-e", script], check=True, capture_output=True, text=True ) output = json.loads(result.stdout) assert output["updated"]["issues"] == [{"repository": "stackchain/web", "number": 18}] assert output["updated"]["pull_requests"] == payload["pull_requests"] assert output["original"]["issues"] == payload["issues"] @pytest.mark.anyio async def test_mobile_issue_sheet_edits_labels_and_repaints_confirmed_priority(): html = await dashboard() assert 'id="issue-label-editor"' in html assert 'aria-describedby="issue-label-status"' in html assert 'id="issue-label-list"' in html assert 'id="save-issue-labels"' in html assert '.issue-label-option { min-height:44px;' in html assert 'max-width:100%;' in html assert "issueController.updateLabels(selectedIssue" in html assert "buildMyWork.replaceIssueLabels" in html assert "paintMyWork(lastContextSnapshot)" in html @pytest.mark.anyio async def test_mobile_my_work_captures_new_issue_in_accessible_draft_safe_sheet(): html = await dashboard() assert 'id="new-issue"' in html and 'New issue' in html assert 'id="create-issue-sheet"' in html and 'aria-modal="true"' in html assert 'id="create-issue-repository"' in html assert 'id="create-issue-title"' in html and 'maxlength="255"' in html assert 'id="create-issue-body"' in html and 'maxlength="10000"' in html assert 'id="create-issue-labels"' in html and 'aria-describedby="create-issue-label-status"' in html assert 'id="create-issue-label-status"' in html and 'aria-live="polite"' in html assert 'id="submit-new-issue"' in html assert '.create-issue-sheet.open { display:flex; }' in html assert 'height:100dvh;' in html assert 'env(safe-area-inset-bottom)' in html assert '' in html assert 'createIssueCapture({ fetchJson: fetchReviewJson, storage: localStorage })' in html assert 'lastMyWork = buildMyWork(lastContextSnapshot);' in html assert 'openRoutedWork(created' in html assert 'issueCapture.loadLabels(repository)' in html assert "input[name=\"create-issue-label\"]:checked" in html assert '.create-issue-label-option' in html and 'min-height:44px' in html @pytest.mark.anyio async def test_mobile_dashboard_puts_filterable_my_work_before_auxiliary_panels(): html = await dashboard() assert html.index('id="my-work"') < html.index('data-panel-key="context"') assert 'data-work-filter="all"' in html assert 'data-work-filter="issue"' in html assert 'data-work-filter="pull"' in html assert 'data-work-filter="review"' in html assert 'data-work-filter="update"' in html assert '.work-filter' in html and 'min-height: 44px' in html assert '.my-work-card' in html and 'min-height: 44px' in html assert '' in html assert "buildMyWork(data)" in html assert "markMyWorkStale()" in html assert "filterMyWork(lastMyWork, selectedWorkFilter)" in html @pytest.mark.anyio async def test_unread_cards_offer_accessible_mobile_mark_read_without_nested_actions(): html = await dashboard() assert '.mark-update-read' in html and 'min-height:44px' in html assert 'data-notification-id' in html assert 'Unread update' in html assert 'id="my-work-action-status"' in html and 'aria-live="assertive"' in html assert "createNotificationAcknowledger" in html assert "method: 'PATCH'" in html assert "api/v1/notifications/" in html assert ' values.has(key) ? values.get(key) : null, setItem: (key, value) => values.set(key, value), removeItem: key => values.delete(key), }}; const statuses = []; let calls = 0; let fail = true; const replier = buildMyWork.createNotificationReplier({{ storage, post: async (id, body) => {{ calls += 1; await new Promise(resolve => setTimeout(resolve, 5)); if (fail) throw new Error('offline'); return {{id:91, url:'https://forge.example/comment/91'}}; }}, onStatus: status => statuses.push(status), }}); const item = {{notification_id:42}}; replier.saveDraft(item, 'Please retry.'); Promise.all([replier.submit(item, 'Please retry.'), replier.submit(item, 'Please retry.')]) .then(async first => {{ const afterFailure = replier.loadDraft(item); fail = false; const success = await replier.submit(item, afterFailure); process.stdout.write(JSON.stringify({{ first, afterFailure, success, afterSuccess:replier.loadDraft(item), calls, statuses, }})); }}); """ result = subprocess.run( ["node", "-e", script], check=True, capture_output=True, text=True ) output = json.loads(result.stdout) assert output["first"] == [False, False] assert output["afterFailure"] == "Please retry." assert output["success"]["id"] == 91 assert output["afterSuccess"] == "" assert output["calls"] == 2 assert output["statuses"] == [ "Sending reply…", "Could not send reply. Your draft is safe; retry.", "Sending reply…", "Reply posted. You can mark this update read when ready.", ] @pytest.mark.anyio async def test_mobile_update_sheet_has_persistent_accessible_reply_composer(): html = await dashboard() assert 'id="update-reply"' in html assert 'maxlength="10000"' in html assert 'id="send-update-reply"' in html assert 'id="update-reply-status"' in html assert 'aria-live="assertive"' in html assert '.update-reply textarea { width:100%;' in html assert '.update-reply button { min-height:44px;' in html assert '.update-sheet-header button { min-height:44px;' in html assert 'createNotificationReplier' in html assert "method: 'POST'" in html assert "'/reply'" in html assert "notificationReplier.loadDraft(item)" in html assert "notificationReplier.saveDraft(selectedUpdate" in html assert "notificationReplier.submit(selectedUpdate" in html @pytest.mark.anyio async def test_updates_view_offers_confirmed_sticky_mobile_bulk_acknowledgement(): html = await dashboard() assert 'id="bulk-mark-read"' in html assert 'id="bulk-mark-read-bar"' in html assert 'class="my-work-bulk"' in html assert '.my-work-bulk { position:sticky;' in html assert 'padding-bottom:calc(10px + env(safe-area-inset-bottom));' in html assert '.my-work-bulk button { min-height:44px; width:100%; }' in html assert "'next ' + ids.length + ' of ' + allIds.length + ' loaded updates'" in html assert "'Confirm marking ' + bulkLabel + ' read'" in html assert "const ids = allIds.slice(0, 50)" in html assert "createBulkNotificationAcknowledger" in html assert "api/v1/notifications/read" in html assert "body: JSON.stringify({ ids })" in html @pytest.mark.anyio async def test_updates_view_discloses_incomplete_inbox_and_loads_more_on_mobile(): html = await dashboard() assert 'id="notification-page-status"' in html assert 'id="load-more-notifications"' in html assert '.load-more-notifications' in html and 'min-height:44px' in html assert "createNotificationPager" in html assert "api/v1/notifications?page=" in html assert "snapshot.notification_pagination" in html assert "notificationPager.loadMore(lastNotifications)" in html assert "Math.min(lastNotifications.length, 50)" in html @pytest.mark.anyio async def test_mobile_filters_wrap_show_counts_and_persist_for_the_session(): html = await dashboard() assert '.work-filters { display:flex; gap:8px; flex-wrap:wrap; }' in html assert 'data-work-count="all"' in html assert 'data-work-count="issue"' in html assert 'data-work-count="pull"' in html assert 'data-work-count="review"' in html assert 'data-work-count="update"' in html assert "['all', 'issue', 'pull', 'review', 'update', 'draft'].includes(savedFilter)" in html assert 'data-work-count="draft"' in html assert 'sessionStorage.getItem(WORK_FILTER_KEY)' in html assert 'sessionStorage.setItem(WORK_FILTER_KEY, selectedWorkFilter)' in html def test_review_controller_loads_encoded_cross_repo_detail_path(): script = f""" const createReviewController = require({json.dumps(str(REVIEW_SHEET))}); let request; const controller = createReviewController({{ fetchJson: async (url, options) => {{ request = {{ url, accept: options.headers.Accept }}; return {{ title: 'Review API' }}; }} }}); controller.load({{ repository: 'stackchain/api', number: 7 }}).then(detail => process.stdout.write(JSON.stringify({{ request, title: detail.title }})) ); """ result = subprocess.run( ["node", "-e", script], check=True, capture_output=True, text=True ) assert json.loads(result.stdout) == { "request": { "url": "api/v1/repos/stackchain/api/pulls/7/review", "accept": "application/json", }, "title": "Review API", } def test_review_diff_rows_escape_content_and_toggle_accessibly(): script = f""" const reviewSheet = require({json.dumps(str(REVIEW_SHEET))}); const escapeHtml = value => String(value) .replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>'); const html = reviewSheet.renderDiffFile({{ filename: 'src/.py', status: 'modified', additions: 1, deletions: 1, diff_available: true, diff_truncated: true, diff_lines: ['@@ -1 +1 @@', '-old ', '+new & safe'] }}, 2, escapeHtml); const button = {{ attrs: {{ 'aria-expanded': 'false' }}, getAttribute(k) {{ return this.attrs[k]; }}, setAttribute(k,v) {{ this.attrs[k]=v; }} }}; const panel = {{ hidden: true }}; reviewSheet.toggleDiff(button, panel); process.stdout.write(JSON.stringify({{ html, expanded: button.attrs['aria-expanded'], hidden: panel.hidden }})); """ result = subprocess.run( ["node", "-e", script], check=True, capture_output=True, text=True ) output = json.loads(result.stdout) assert 'aria-expanded="false"' in output["html"] assert 'src/<api>.py' in output["html"] assert '-old <token>' in output["html"] assert '+new & safe' in output["html"] assert 'Preview truncated' in output["html"] assert 'class="review-mark"' in output["html"] assert 'data-review-filename="src/<api>.py"' in output["html"] assert 'class="review-diff-line review-inline-target removed"' in output["html"] assert 'data-old-position="1"' in output["html"] assert 'data-new-position="1"' in output["html"] assert 'aria-label="Comment on src/<api>.py line 1"' in output["html"] assert 'Mark reviewed' in output["html"] assert output["expanded"] == "true" assert output["hidden"] is False def test_review_diff_parser_maps_multi_hunk_lines_to_old_or_new_positions(): script = f""" const reviewSheet = require({json.dumps(str(REVIEW_SHEET))}); const rows = reviewSheet.parseDiffLines([ '@@ -10,3 +20,4 @@ function run()', ' context', '-removed', '+added', '+second', String.raw`\\ No newline at end of file`, '@@ -40 +51 @@', '-old tail', '+new tail' ]); process.stdout.write(JSON.stringify(rows)); """ result = subprocess.run( ["node", "-e", script], check=True, capture_output=True, text=True ) assert json.loads(result.stdout) == [ {"text": "@@ -10,3 +20,4 @@ function run()", "kind": "hunk", "commentable": False}, {"text": " context", "kind": "context", "commentable": True, "new_position": 20}, {"text": "-removed", "kind": "removed", "commentable": True, "old_position": 11}, {"text": "+added", "kind": "added", "commentable": True, "new_position": 21}, {"text": "+second", "kind": "added", "commentable": True, "new_position": 22}, {"text": "\\ No newline at end of file", "kind": "note", "commentable": False}, {"text": "@@ -40 +51 @@", "kind": "hunk", "commentable": False}, {"text": "-old tail", "kind": "removed", "commentable": True, "old_position": 40}, {"text": "+new tail", "kind": "added", "commentable": True, "new_position": 51}, ] def test_review_progress_is_explicit_and_restores_for_the_same_head_sha(): script = f""" const reviewSheet = require({json.dumps(str(REVIEW_SHEET))}); const values = new Map(); const storage = {{ getItem(key) {{ return values.has(key) ? values.get(key) : null; }}, setItem(key, value) {{ values.set(key, value); }} }}; const options = {{ storage, repository: 'stackchain/api', number: 7, headSha: 'abc123', files: [{{filename:'src/a.py'}}, {{filename:'src/b.py'}}, {{filename:'README.md'}}] }}; const first = reviewSheet.createProgress(options); const before = first.snapshot(); const marked = first.markReviewed('src/a.py'); const restored = reviewSheet.createProgress(options).snapshot(); process.stdout.write(JSON.stringify({{ before, marked, restored }})); """ result = subprocess.run( ["node", "-e", script], check=True, capture_output=True, text=True ) output = json.loads(result.stdout) assert output["before"] == { "reviewed": [], "reviewedCount": 0, "total": 3, "nextFilename": "src/a.py" } assert output["marked"] == { "reviewed": ["src/a.py"], "reviewedCount": 1, "total": 3, "nextFilename": "src/b.py", } assert output["restored"] == output["marked"] def test_review_progress_resets_when_new_commits_change_the_head_sha(): script = f""" const reviewSheet = require({json.dumps(str(REVIEW_SHEET))}); const values = new Map(); const storage = {{ getItem(key) {{ return values.has(key) ? values.get(key) : null; }}, setItem(key, value) {{ values.set(key, value); }} }}; const base = {{ storage, repository: 'stackchain/api', number: 7, files: [{{filename:'src/a.py'}}] }}; reviewSheet.createProgress({{...base, headSha:'abc123'}}).markReviewed('src/a.py'); const changed = reviewSheet.createProgress({{...base, headSha:'def456'}}).snapshot(); process.stdout.write(JSON.stringify(changed)); """ result = subprocess.run( ["node", "-e", script], check=True, capture_output=True, text=True ) assert json.loads(result.stdout) == { "reviewed": [], "reviewedCount": 0, "total": 1, "nextFilename": "src/a.py", "newHead": True, } def test_review_feedback_draft_restores_notes_summary_and_decision_for_same_head(): script = f""" const reviewSheet = require({json.dumps(str(REVIEW_SHEET))}); const values = new Map(); const storage = {{ getItem(key) {{ return values.has(key) ? values.get(key) : null; }}, setItem(key, value) {{ values.set(key, value); }} }}; const options = {{ storage, repository: 'stackchain/api', number: 7, headSha: 'abc123', files: [{{filename:'src/a.py'}}, {{filename:'src/b.py'}}] }}; const first = reviewSheet.createDraft(options); first.setNote('src/a.py', 'Handle the empty state.'); first.setSummary('One blocker remains.'); first.setDecision('request_changes'); const restored = reviewSheet.createDraft(options).snapshot(); process.stdout.write(JSON.stringify(restored)); """ result = subprocess.run( ["node", "-e", script], check=True, capture_output=True, text=True ) assert json.loads(result.stdout) == { "notes": {"src/a.py": "Handle the empty state."}, "comments": [], "summary": "One blocker remains.", "decision": "request_changes", } def test_review_draft_persists_edits_and_removes_inline_comments_for_same_head(): script = f""" const reviewSheet = require({json.dumps(str(REVIEW_SHEET))}); const values = new Map(); const storage = {{getItem:k => values.get(k) || null, setItem:(k,v) => values.set(k,v), removeItem:k => values.delete(k)}}; const options = {{storage, repository:'stackchain/api', number:7, headSha:'abc123', files:[{{filename:'src/a.py'}}]}}; const first = reviewSheet.createDraft(options); first.setInlineComment({{path:'src/a.py', new_position:42}}, 'Handle empty values.'); first.setInlineComment({{path:'src/a.py', new_position:42}}, 'Handle null and empty values.'); first.setInlineComment({{path:'src/a.py', old_position:9}}, 'Why remove this guard?'); const restored = reviewSheet.createDraft(options); const beforeRemove = restored.snapshot().comments; const afterRemove = restored.removeInlineComment({{path:'src/a.py', old_position:9}}).comments; process.stdout.write(JSON.stringify({{beforeRemove, afterRemove}})); """ result = subprocess.run( ["node", "-e", script], check=True, capture_output=True, text=True ) assert json.loads(result.stdout) == { "beforeRemove": [ {"path": "src/a.py", "body": "Handle null and empty values.", "new_position": 42}, {"path": "src/a.py", "body": "Why remove this guard?", "old_position": 9}, ], "afterRemove": [ {"path": "src/a.py", "body": "Handle null and empty values.", "new_position": 42}, ], } def test_review_feedback_formats_non_empty_file_notes_in_changed_file_order(): script = f""" const reviewSheet = require({json.dumps(str(REVIEW_SHEET))}); const markdown = reviewSheet.formatFeedback({{ decision: 'request_changes', summary: 'One blocker remains.', notes: {{'src/b.py':'Second note', 'src/a.py':'First note', 'README.md':''}} }}, [{{filename:'src/a.py'}}, {{filename:'README.md'}}, {{filename:'src/b.py'}}]); process.stdout.write(markdown); """ result = subprocess.run( ["node", "-e", script], check=True, capture_output=True, text=True ) assert result.stdout == ( "## Intended decision\nRequest changes\n\n" "## Summary\nOne blocker remains.\n\n" "## File notes\n### `src/a.py`\nFirst note\n\n" "### `src/b.py`\nSecond note" ) def test_review_handoff_reserves_window_before_copy_and_reports_blocked_popup(): script = f""" const reviewSheet = require({json.dumps(str(REVIEW_SHEET))}); const events = []; let finishCopy; const reserved = {{ location: {{ href: '' }}, close: () => events.push('close') }}; const success = reviewSheet.copyAndContinue({{ text: 'feedback', url: 'https://forge.example/pulls/7', copy: text => new Promise(resolve => {{ events.push('copy:' + text); finishCopy = resolve; }}), open: () => {{ events.push('reserve'); return reserved; }}, fallback: text => events.push('fallback:' + text), }}); const beforeCopySettles = events.slice(); finishCopy(); const blockedEvents = []; const blocked = reviewSheet.copyAndContinue({{ text: 'copied', url: 'https://forge.example/pulls/8', copy: async () => blockedEvents.push('copy'), open: () => {{ blockedEvents.push('reserve'); return null; }}, fallback: text => blockedEvents.push('fallback:' + text), }}); Promise.all([success, blocked]).then(results => process.stdout.write(JSON.stringify({{ beforeCopySettles, events, blockedEvents, destination: reserved.location.href, results }}))); """ result = subprocess.run( ["node", "-e", script], check=True, capture_output=True, text=True ) assert json.loads(result.stdout) == { "beforeCopySettles": ["reserve", "copy:feedback"], "events": ["reserve", "copy:feedback"], "blockedEvents": ["reserve", "copy", "fallback:copied"], "destination": "https://forge.example/pulls/7", "results": [ {"copied": True, "opened": True}, {"copied": True, "opened": False}, ], } def test_review_handoff_closes_reserved_window_when_copy_fails(): script = f""" const reviewSheet = require({json.dumps(str(REVIEW_SHEET))}); const events = []; reviewSheet.copyAndContinue({{ text: 'keep me', url: 'https://forge.example/pulls/8', copy: async () => {{ throw new Error('denied'); }}, open: () => ({{ close: () => events.push('close') }}), fallback: text => events.push('fallback:' + text), }}).then(result => process.stdout.write(JSON.stringify({{ events, result }}))); """ result = subprocess.run( ["node", "-e", script], check=True, capture_output=True, text=True ) assert json.loads(result.stdout) == { "events": ["close", "fallback:keep me"], "result": {"copied": False, "opened": False}, } @pytest.mark.anyio async def test_review_requests_open_an_accessible_mobile_detail_sheet(): html = await dashboard() assert 'id="review-sheet"' in html assert 'role="dialog"' in html and 'aria-modal="true"' in html assert 'id="review-sheet-status"' in html and 'aria-live="polite"' in html assert 'id="open-review-gitea"' in html and 'rel="noopener noreferrer"' in html assert '' in html assert '@media (max-width: 600px)' in html assert '.review-sheet-panel' in html and 'width:100%' in html assert '.review-action' in html and 'min-height:44px' in html assert '.review-file-toggle' in html and 'min-height:44px' in html assert '.review-mark' in html and 'min-height:44px' in html assert 'id="review-progress"' in html and 'aria-live="polite"' in html assert 'id="next-unreviewed-review"' in html assert '.review-progress-actions' in html and 'position:sticky' in html assert "createReviewController.createProgress" in html assert "progress.markReviewed" in html assert "scrollIntoView" in html assert '.review-diff' in html and 'overflow-x:auto' in html @pytest.mark.anyio async def test_review_sheet_loads_details_and_preserves_safe_gitea_handoff(): html = await dashboard() assert 'data-review-index' in html assert "reviewController.load(selectedReview)" in html assert "review-files" in html assert "review-history" in html assert "open-review-gitea" in html assert 'id="submit-review"' in html assert 'id="review-submit-status"' in html and 'aria-live="assertive"' in html assert "reviewController.submit(selectedReview" in html assert "window.confirm" in html assert "expected_head_sha: selectedReviewHead" in html assert "await load()" in html @pytest.mark.anyio async def test_mobile_review_sheet_captures_and_safely_hands_off_feedback(): html = await dashboard() review_script = REVIEW_SHEET.read_text() assert 'class="review-note"' in review_script assert 'id="review-decision"' in html assert 'id="review-summary"' in html assert 'id="copy-review-feedback"' in html assert 'id="review-copy-fallback"' in html assert 'id="review-handoff-link"' in html assert '.review-handoff-link' in html and 'min-height:44px' in html assert '.review-handoff-link[hidden]' in html and 'display:none' in html assert '.review-note' in html and 'min-height:88px' in html assert '.review-handoff' in html and 'position:sticky' in html assert "createReviewController.createDraft" in html assert "draft.setNote" in html assert "draft?.setSummary" in html assert "draft?.setDecision" in html assert "createReviewController.formatFeedback" in html assert "createReviewController.copyAndContinue" in html assert "navigator.clipboard.writeText" in html assert "window.open('about:blank', '_blank')" in html assert "handoffWindow.opener = null" in html @pytest.mark.anyio async def test_mobile_review_sheet_edits_inline_drafts_and_submits_them_with_review(): html = await dashboard() assert 'id="review-inline-composer"' in html assert 'id="review-inline-body"' in html and 'maxlength="10000"' in html assert 'id="save-inline-comment"' in html assert 'id="delete-inline-comment"' in html assert '.review-inline-target' in html and 'min-height:44px' in html assert '.review-inline-composer' in html and 'position:sticky' in html assert "document.querySelectorAll('.review-inline-target')" in html assert "draft.setInlineComment" in html assert "draft.removeInlineComment" in html assert "comments: snapshot.comments" in html def test_review_controller_submits_once_while_request_is_in_flight(): script = f""" const createReviewController = require({json.dumps(str(REVIEW_SHEET))}); let resolveRequest; const calls = []; const controller = createReviewController({{ createOperationId: () => 'review-op-test', fetchJson: (url, options) => {{ calls.push({{url, options}}); return new Promise(resolve => {{ resolveRequest = resolve; }}); }} }}); const item = {{repository:'stackchain/api', number:7}}; const payload = {{decision:'approve', body:'Looks good.', expected_head_sha:'abc123'}}; const first = controller.submit(item, payload); const second = controller.submit(item, payload); resolveRequest({{id:91, state:'APPROVED'}}); Promise.all([first, second]).then(results => process.stdout.write(JSON.stringify({{ calls, results }}))); """ result = subprocess.run( ["node", "-e", script], check=True, capture_output=True, text=True ) payload = json.loads(result.stdout) assert len(payload["calls"]) == 1 assert payload["calls"][0] == { "url": "api/v1/repos/stackchain/api/pulls/7/review", "options": { "method": "POST", "headers": { "Accept": "application/json", "Content-Type": "application/json", "Idempotency-Key": "review-op-test", }, "body": json.dumps( { "decision": "approve", "body": "Looks good.", "expected_head_sha": "abc123", }, separators=(",", ":"), ), }, } assert payload["results"] == [ {"id": 91, "state": "APPROVED"}, {"id": 91, "state": "APPROVED"}, ] def test_successful_review_can_clear_head_scoped_draft_and_progress(): script = f""" const reviewSheet = require({json.dumps(str(REVIEW_SHEET))}); const values = new Map(); const removed = []; const storage = {{ getItem: key => values.get(key) || null, setItem: (key, value) => values.set(key, value), removeItem: key => {{ removed.push(key); values.delete(key); }}, }}; const options = {{storage, repository:'stackchain/api', number:7, headSha:'abc123', files:[{{filename:'a.py'}}]}}; const draft = reviewSheet.createDraft(options); const progress = reviewSheet.createProgress(options); draft.setSummary('Looks good.'); progress.markReviewed('a.py'); draft.clear(); progress.clear(); process.stdout.write(JSON.stringify({{removed, draft:draft.snapshot(), progress:progress.snapshot()}})); """ result = subprocess.run( ["node", "-e", script], check=True, capture_output=True, text=True ) payload = json.loads(result.stdout) assert payload["removed"] == [ "stackchain.review-draft.v1:stackchain/api#7@abc123", "stackchain.review-progress.v1:stackchain/api#7@abc123", ] assert payload["draft"] == { "notes": {}, "comments": [], "summary": "", "decision": "comment" } assert payload["progress"]["reviewedCount"] == 0 def test_approved_assigned_review_carries_exact_head_progress_into_merge(): script = f""" const reviewSheet = require({json.dumps(str(REVIEW_SHEET))}); const values = new Map(); const storage = {{ getItem: key => values.get(key) || null, setItem: (key, value) => values.set(key, value), }}; const assigned = {{ repository:'stackchain/api', number:7, work_reasons:['assigned_to_me', 'review_requested'] }}; const carried = reviewSheet.prepareMergeContinuation({{ storage, item:assigned, headSha:'abc123', reviewed:['a.py', 'b.py'], decision:'approve' }}); const unassigned = reviewSheet.prepareMergeContinuation({{ storage, item:{{...assigned, number:8, work_reasons:['review_requested']}}, headSha:'def456', reviewed:['c.py'], decision:'approve' }}); const changesRequested = reviewSheet.prepareMergeContinuation({{ storage, item:{{...assigned, number:9}}, headSha:'ghi789', reviewed:['d.py'], decision:'request_changes' }}); process.stdout.write(JSON.stringify({{carried, unassigned, changesRequested, entries:[...values.entries()]}})); """ result = subprocess.run( ["node", "-e", script], check=True, capture_output=True, text=True ) assert json.loads(result.stdout) == { "carried": True, "unassigned": False, "changesRequested": False, "entries": [[ "stackchain.pull-review.v1:stackchain/api#7:abc123", '["a.py","b.py"]', ]], } @pytest.mark.anyio async def test_successful_assigned_approval_continues_to_merge_in_place(): html = await dashboard() assert 'id="continue-review-to-merge"' in html assert '#continue-review-to-merge[hidden]' in html and 'display:none' in html assert '.review-merge-continuation' in html and 'position:sticky' in html assert '#continue-review-to-merge { min-height:44px;' in html assert "createReviewController.prepareMergeContinuation" in html assert "reviewed: progressSnapshot.reviewed" in html assert "decision: snapshot.decision" in html assert "workRoute.open({ ...item, kind:'pull', is_review:false }, { replace:true })" in html assert "qs('#continue-review-to-merge').hidden = false" in html assert "qs('#continue-review-to-merge').focus()" in html @pytest.mark.anyio async def test_mobile_review_failure_offers_an_in_place_retry_for_the_same_item(): html = await dashboard() assert 'id="retry-review-load"' in html assert '.review-retry' in html and 'min-height:44px' in html assert "qs('#retry-review-load').hidden = false" in html assert "qs('#retry-review-load').hidden = true" in html assert "openReviewSheet(selectedReview, reviewTrigger)" in html assert "qs('#retry-review-load').focus()" in html @pytest.mark.anyio async def test_mobile_update_sheet_renders_and_pages_the_complete_conversation(): html = await dashboard() update_sheet = html[html.index('id="update-sheet"'):html.index('id="pull-sheet"')] assert '

Full conversation

' in update_sheet assert 'id="update-comments"' in update_sheet assert 'id="load-older-update-comments"' in update_sheet assert 'id="update-conversation-status"' in update_sheet assert '.conversation-more' in html and 'min-height:44px' in html assert "'/conversation?page='" in html assert "loadConversation: fetchNotificationConversation" in html assert "onConversation: renderUpdateConversation" in html assert "notificationReader.loadOlder()" in html assert "notificationReader.appendReply(result)" in html def test_issue_comment_reuses_operation_key_after_reload_until_success(): script = f""" const createIssueSheet = require({json.dumps(str(ISSUE_SHEET))}); const values = new Map(); const storage = {{getItem:k => values.get(k) || null, setItem:(k,v) => values.set(k,v), removeItem:k => values.delete(k)}}; const calls = []; const item = {{repository:'stackchain/api', number:7}}; const first = createIssueSheet({{storage, createOperationId:() => 'issue-op-185', fetchJson:(_u,o) => {{calls.push(o.headers['Idempotency-Key']); return Promise.reject(new Error('timeout'));}}}}); first.comment(item, 'Ship it').catch(() => {{ const second = createIssueSheet({{storage, createOperationId:() => 'wrong-key', fetchJson:(_u,o) => {{calls.push(o.headers['Idempotency-Key']); return Promise.resolve({{id:82}});}}}}); second.comment(item, 'Ship it').then(() => process.stdout.write(JSON.stringify({{calls, keys:[...values.keys()]}}))); }}); """ result = subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True) assert json.loads(result.stdout) == {"calls": ["issue-op-185", "issue-op-185"], "keys": []} def test_pull_comment_reuses_operation_key_after_reload_until_success(): script = f""" const createPullSheet = require({json.dumps(str(PULL_SHEET))}); const values = new Map(); const storage = {{getItem:k => values.get(k) || null, setItem:(k,v) => values.set(k,v), removeItem:k => values.delete(k)}}; const calls = []; const item = {{repository:'stackchain/api', number:7}}; const first = createPullSheet({{storage, createOperationId:() => 'pull-op-185', fetchJson:(_u,o) => {{calls.push(o.headers['Idempotency-Key']); return Promise.reject(new Error('timeout'));}}}}); first.comment(item, 'Ship it').catch(() => {{ createPullSheet({{storage, createOperationId:() => 'wrong-key', fetchJson:(_u,o) => {{calls.push(o.headers['Idempotency-Key']); return Promise.resolve({{id:91}});}}}}).comment(item, 'Ship it') .then(() => process.stdout.write(JSON.stringify({{calls, keys:[...values.keys()]}}))); }}); """ result = subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True) assert json.loads(result.stdout) == {"calls": ["pull-op-185", "pull-op-185"], "keys": []} def test_notification_reply_reuses_persisted_operation_key(): script = f""" const build = require({json.dumps(str(MY_WORK))}); const values = new Map(); const storage = {{getItem:k => values.get(k) || null, setItem:(k,v) => values.set(k,v), removeItem:k => values.delete(k)}}; const calls = []; const item = {{notification_id:42}}; const first = build.createNotificationReplier({{storage, onStatus:()=>{{}}, createOperationId:() => 'reply-op-185', post:(_id,_body,key) => {{calls.push(key); return Promise.reject(new Error('timeout'));}}}}); first.submit(item, 'Retry').then(() => {{ const second = build.createNotificationReplier({{storage, onStatus:()=>{{}}, createOperationId:() => 'wrong-key', post:(_id,_body,key) => {{calls.push(key); return Promise.resolve({{id:91}});}}}}); second.submit(item, 'Retry').then(() => process.stdout.write(JSON.stringify({{calls, keys:[...values.keys()]}}))); }}); """ result = subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True) assert json.loads(result.stdout) == {"calls": ["reply-op-185", "reply-op-185"], "keys": []} def test_review_submission_reuses_persisted_operation_key(): script = f""" const createReviewController = require({json.dumps(str(REVIEW_SHEET))}); const values = new Map(); const storage = {{getItem:k => values.get(k) || null, setItem:(k,v) => values.set(k,v), removeItem:k => values.delete(k)}}; const calls = []; const item = {{repository:'stackchain/api', number:7}}; const payload = {{decision:'approve', body:'Good', expected_head_sha:'abc', comments:[]}}; const first = createReviewController({{storage, createOperationId:() => 'review-op-185', fetchJson:(_u,o) => {{calls.push(o.headers['Idempotency-Key']); return Promise.reject(new Error('timeout'));}}}}); first.submit(item, payload).catch(() => {{ createReviewController({{storage, createOperationId:() => 'wrong-key', fetchJson:(_u,o) => {{calls.push(o.headers['Idempotency-Key']); return Promise.resolve({{id:93}});}}}}).submit(item, payload) .then(() => process.stdout.write(JSON.stringify({{calls, keys:[...values.keys()]}}))); }}); """ result = subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True) assert json.loads(result.stdout) == {"calls": ["review-op-185", "review-op-185"], "keys": []}