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" 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_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_my_work_filter_counts_distinguish_prs_from_review_requests(): items = [ {"kind": "issue", "is_review": False}, {"kind": "pull", "is_review": False}, {"kind": "pull", "is_review": True}, ] script = f""" const buildMyWork = require({json.dumps(str(MY_WORK))}); process.stdout.write(JSON.stringify(buildMyWork.countMyWork({json.dumps(items)}))); """ result = subprocess.run( ["node", "-e", script], check=True, capture_output=True, text=True ) assert json.loads(result.stdout) == {"all": 3, "issue": 1, "pull": 1, "review": 1, "update": 0} 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'}}; 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"}, }] assert output["during"] == { "repository": "stackchain/api", "title": "Capture work", "body": "Context" } assert output["after"] == {"repository": "", "title": "", "body": ""} assert output["results"][0]["number"] == 17 assert output["results"][1]["number"] == 17 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_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_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_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_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"}, ], } @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="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_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="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 'openIssueSheet(created' 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'].includes(savedFilter)" 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 'Mark reviewed' in output["html"] assert output["expanded"] == "true" assert output["hidden"] is False 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."}, "summary": "One blocker remains.", "decision": "request_changes", } 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 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({{ 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", }, "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": {}, "summary": "", "decision": "comment"} assert payload["progress"]["reviewedCount"] == 0 @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