stackchain-dashboard/tests/test_my_work.py
timmy 6e6e63e553
All checks were successful
CI / lint (pull_request) Successful in 29s
CI / build-frontend (pull_request) Successful in 4s
security: enforce strict browser execution boundary (#295)
2026-08-08 11:38:18 +00:00

3731 lines
149 KiB
Python

import json
import os
import subprocess
from pathlib import Path
import pytest
from tests.dashboard_bundle import dashboard
MY_WORK = Path(__file__).parents[1] / "frontend" / "my-work.js"
LATER_WORK = Path(__file__).parents[1] / "frontend" / "later-work.js"
DETAIL_DEFER = Path(__file__).parents[1] / "frontend" / "detail-defer.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 '<script src="static/work-route.js"></script>' 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_attention_includes_only_assigned_deadline_and_priority_critical_work():
payload = {
"user": {"login": "timmy"},
"issues": [
{"number": 1, "title": "Assigned P0", "repository": "stackchain/api",
"labels": ["P0"], "assignees": ["timmy"], "due_date": "2026-08-10T23:59:59Z"},
{"number": 2, "title": "Assigned overdue", "repository": "stackchain/api",
"labels": [], "assignees": ["timmy"], "due_date": "2026-08-06T23:59:59Z"},
{"number": 3, "title": "Assigned due today", "repository": "stackchain/api",
"labels": [], "assignees": ["timmy"], "due_date": "2026-08-07T23:59:59Z"},
{"number": 4, "title": "Assigned future", "repository": "stackchain/api",
"labels": [], "assignees": ["timmy"], "due_date": "2026-08-10T23:59:59Z"},
{"number": 5, "title": "Unassigned P0", "repository": "stackchain/api",
"labels": ["critical"], "assignees": [], "due_date": "2026-08-10T23:59:59Z"},
{"number": 6, "title": "Unassigned overdue", "repository": "stackchain/api",
"labels": [], "assignees": [], "due_date": "2026-08-06T23:59:59Z"},
],
"pull_requests": [
{"number": 7, "title": "Updated review", "repository": "stackchain/web",
"labels": [], "assignees": [], "work_reasons": ["review_requested"]},
],
"notifications": [
{"id": 70, "number": 7, "repository": "stackchain/web", "unread": True,
"subject_type": "PullRequest", "url": "https://forge.example/web/pulls/7"},
],
}
script = f"""
const buildMyWork = require({json.dumps(str(MY_WORK))});
const items = buildMyWork({json.dumps(payload)}, new Date('2026-08-07T12:00:00Z'));
const attention = buildMyWork.filterMyWork(items, 'attention');
process.stdout.write(JSON.stringify({{
titles: attention.map(item => item.title),
count: buildMyWork.countMyWork(items).attention,
reasons: Object.fromEntries(attention.map(item => [item.title, item.attention_reason])),
}}));
"""
result = subprocess.run(
["node", "-e", script], check=True, capture_output=True, text=True
)
assert json.loads(result.stdout) == {
"titles": ["Assigned P0", "Updated review", "Assigned overdue", "Assigned due today"],
"count": 4,
"reasons": {
"Assigned P0": "P0 priority",
"Updated review": "Unread update",
"Assigned overdue": "Overdue",
"Assigned due today": "Due today",
},
}
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_handoff_is_single_flight_and_requires_confirmed_transfer():
script = f"""
const createIssueSheet = require({json.dumps(str(ISSUE_SHEET))});
let calls = [];
let finish;
const controller = createIssueSheet({{
fetchJson: (url, options) => {{
calls.push({{url, options}});
return new Promise(resolve => {{ finish = resolve; }});
}},
storage: null,
}});
const item = {{repository:'stackchain/api', number:17}};
const first = controller.handoff(item, 'alex', 'timmy');
const duplicate = controller.handoff(item, 'alex', 'timmy');
finish({{number:17, repository:'stackchain/api', recipient:'alex', assignees:['alex']}});
Promise.all([first, duplicate]).then(results => process.stdout.write(JSON.stringify({{
calls, same: first === duplicate, results
}})));
"""
output = json.loads(subprocess.run(
["node", "-e", script], check=True, capture_output=True, text=True
).stdout)
assert output["same"] is True
assert len(output["calls"]) == 1
assert output["calls"][0]["url"].endswith("/issues/17/handoff")
assert output["calls"][0]["options"]["method"] == "PATCH"
assert json.loads(output["calls"][0]["options"]["body"]) == {"recipient": "alex"}
assert output["results"][0]["assignees"] == ["alex"]
def test_issue_handoff_candidates_use_the_assigned_issue_route():
script = f"""
const createIssueSheet = require({json.dumps(str(ISSUE_SHEET))});
let call;
const controller = createIssueSheet({{
fetchJson: async (url, options) => {{ call = {{url, options}}; return [{{login:'alex', name:'Alexander'}}]; }},
storage: null,
}});
controller.loadHandoffCandidates({{repository:'stackchain/api', number:17}})
.then(result => process.stdout.write(JSON.stringify({{call, result}})));
"""
output = json.loads(subprocess.run(
["node", "-e", script], check=True, capture_output=True, text=True
).stdout)
assert output["call"]["url"].endswith("/issues/17/handoff-candidates")
assert output["call"]["options"]["headers"]["Accept"] == "application/json"
assert output["result"] == [{"login": "alex", "name": "Alexander"}]
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_mobile_issue_sheet_exposes_touch_safe_teammate_handoff():
html = await dashboard()
assert 'id="issue-handoff-recipient"' in html
assert 'id="load-issue-handoff"' in html
assert 'id="confirm-issue-handoff"' in html
assert 'id="issue-handoff-status" class="small" aria-live="assertive"' in html
assert '.issue-handoff select, .issue-handoff button { min-height:44px;' in html
assert "issueController.loadHandoffCandidates(selectedIssue)" in html
assert "issueController.handoff(selectedIssue, recipient" 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 '<link rel="manifest" href="manifest.webmanifest"' in html
assert "issueCapture.stageSharedContent(sharedLaunch)" in html
assert 'id="use-shared-content"' in html
assert 'id="resume-issue-draft"' in html
assert 'id="shared-content-conflict"' in html
assert '.shared-content-actions button { min-height:44px;' in html
assert "history.replaceState({}, '', cleanUrl)" in html
assert "navigator.serviceWorker.register('service-worker.js')" in html
@pytest.mark.anyio
async def test_mobile_my_work_exposes_touch_safe_milestone_lane_and_issue_editor():
html = await dashboard()
assert 'id="work-milestone-filter"' in html
assert '<option value="unplanned">Unplanned</option>' 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('<h2>Full conversation</h2>', body)
planning = html.index('id="issue-planning"', conversation)
labels = html.index('id="issue-label-editor"', planning)
milestone = html.index('id="issue-milestone"', planning)
assert '<summary>Plan &amp; edit</summary>' in html[planning:labels]
assert '<details class="issue-planning" id="issue-planning">' 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_attention_filter_unions_updates_and_reviews_without_double_counting():
items = [
{"title": "Unread issue", "kind": "issue", "has_update": True, "is_review": False},
{"title": "Requested review", "kind": "pull", "has_update": False, "is_review": True},
{"title": "Updated review", "kind": "pull", "has_update": True, "is_review": True},
{"title": "Ordinary assignment", "kind": "issue", "has_update": False, "is_review": False},
]
script = f"""
const buildMyWork = require({json.dumps(str(MY_WORK))});
const items = {json.dumps(items)};
process.stdout.write(JSON.stringify({{
attention: buildMyWork.filterMyWork(items, 'attention').map(item => item.title),
count: buildMyWork.countMyWork(items).attention,
}}));
"""
result = subprocess.run(
["node", "-e", script], check=True, capture_output=True, text=True
)
assert json.loads(result.stdout) == {
"attention": ["Unread issue", "Requested review", "Updated review"],
"count": 3,
}
def test_later_queue_defers_work_locally_and_scopes_it_to_confirmed_login():
script = f"""
const createLaterWork = require({json.dumps(str(LATER_WORK))});
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 login = 'timmy';
const item = {{kind:'pull',is_review:true,repository:'stackchain/api',number:17,title:'Review me'}};
const store = createLaterWork({{storage,getLogin:() => login,now:() => new Date('2026-08-08T12:00:00Z')}});
const deferred = store.defer(item, new Date('2026-08-08T16:00:00Z'));
const timmy = store.partition([item]);
login = 'alexander';
const alexander = store.partition([item]);
process.stdout.write(JSON.stringify({{
deferred,
timmy:{{active:timmy.active.length,later:timmy.later}},
alexander:{{active:alexander.active.length,later:alexander.later.length}},
keys:Array.from(values.keys()),
}}));
"""
result = subprocess.run(
["node", "-e", script], check=True, capture_output=True, text=True
)
assert json.loads(result.stdout) == {
"deferred": "deferred",
"timmy": {
"active": 0,
"later": [{
"kind": "pull", "is_review": True, "repository": "stackchain/api",
"number": 17, "title": "Review me", "deferred_until": "2026-08-08T16:00:00.000Z",
}],
},
"alexander": {"active": 1, "later": 0},
"keys": ["stackchain.later-work.v1.timmy"],
}
def test_later_queue_reports_invalid_identity_and_failed_storage_without_false_success():
script = f"""
const createLaterWork = require({json.dumps(str(LATER_WORK))});
let login = '';
const item = {{kind:'issue',repository:'stackchain/api',number:17}};
const unavailable = createLaterWork({{
storage: {{getItem:() => null,setItem:() => {{ throw new Error('quota'); }},removeItem:() => {{}}}},
getLogin:() => login, now:() => new Date('2026-08-08T12:00:00Z'),
}});
const noIdentity = unavailable.defer(item, new Date('2026-08-08T16:00:00Z'));
login = 'timmy';
const invalid = unavailable.defer(item, new Date('2026-08-08T11:00:00Z'));
const failedWrite = unavailable.defer(item, new Date('2026-08-08T16:00:00Z'));
process.stdout.write(JSON.stringify({{noIdentity, invalid, failedWrite}}));
"""
result = subprocess.run(
["node", "-e", script], check=True, capture_output=True, text=True
)
assert json.loads(result.stdout) == {
"noIdentity": "unavailable",
"invalid": "invalid",
"failedWrite": "unavailable",
}
def test_later_queue_prunes_missing_work_and_wakes_expired_items_without_reload():
script = f"""
const createLaterWork = require({json.dumps(str(LATER_WORK))});
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 clock = new Date('2026-08-08T12:00:00Z');
let scheduled = null;
let wakes = 0;
const store = createLaterWork({{
storage, getLogin:() => 'timmy', now:() => clock,
setTimer:(callback, delay) => {{ scheduled = {{callback,delay}}; return 7; }},
clearTimer:() => {{}}, onWake:() => {{ wakes += 1; }},
}});
const kept = {{kind:'issue',repository:'stackchain/api',number:17,title:'Kept'}};
const gone = {{kind:'issue',repository:'stackchain/api',number:18,title:'Gone'}};
store.defer(kept, new Date('2026-08-08T13:00:00Z'));
store.defer(gone, new Date('2026-08-09T13:00:00Z'));
const before = store.partition([kept]);
const persisted = JSON.parse(values.get('stackchain.later-work.v1.timmy'));
clock = new Date('2026-08-08T13:00:01Z');
scheduled.callback();
const after = store.partition([kept]);
process.stdout.write(JSON.stringify({{
before:{{active:before.active.length,later:before.later.length}},
persisted:Object.keys(persisted), delay:scheduled.delay, wakes,
after:{{active:after.active.map(item => item.title),later:after.later.length}},
storageEmpty:!values.has('stackchain.later-work.v1.timmy'),
}}));
"""
result = subprocess.run(
["node", "-e", script], check=True, capture_output=True, text=True
)
assert json.loads(result.stdout) == {
"before": {"active": 0, "later": 1},
"persisted": ["issue:stackchain/api:17:"],
"delay": 3600000,
"wakes": 1,
"after": {"active": ["Kept"], "later": 0},
"storageEmpty": True,
}
def test_later_queue_preserves_rank_and_retains_unloaded_paginated_work():
script = f"""
const createLaterWork = require({json.dumps(str(LATER_WORK))});
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 store = createLaterWork({{
storage,getLogin:() => 'timmy',now:() => new Date('2026-08-08T12:00:00Z'),
setTimer:() => 1, clearTimer:() => {{}},
}});
const first = {{kind:'issue',repository:'stackchain/api',number:1,title:'Higher priority'}};
const second = {{kind:'pull',repository:'stackchain/api',number:2,title:'Lower priority'}};
store.defer(second, new Date('2026-08-09T12:00:00Z'));
store.defer(first, new Date('2026-08-09T12:00:00Z'));
store.partition([first], {{pruneMissing:false}});
const visible = store.partition([first, second], {{pruneMissing:false}});
process.stdout.write(JSON.stringify({{
titles:visible.later.map(item => item.title),
records:Object.keys(JSON.parse(values.get('stackchain.later-work.v1.timmy'))).length,
}}));
"""
result = subprocess.run(
["node", "-e", script], check=True, capture_output=True, text=True
)
assert json.loads(result.stdout) == {
"titles": ["Higher priority", "Lower priority"],
"records": 2,
}
def test_later_queue_presets_and_bring_back_now_preserve_work_identity():
script = f"""
const createLaterWork = require({json.dumps(str(LATER_WORK))});
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 now = new Date('2026-08-08T12:00:00Z');
const store = createLaterWork({{storage,getLogin:() => 'timmy',now:() => now}});
const item = {{kind:'update',repository:'stackchain/api',number:17,notification_id:91}};
const today = store.presetUntil('today');
const tomorrow = store.presetUntil('tomorrow');
store.defer(item, tomorrow);
const removed = store.restore(item);
process.stdout.write(JSON.stringify({{
today:today.toISOString(), tomorrow:tomorrow.toISOString(), removed,
partition:store.partition([item]),
}}));
"""
result = subprocess.run(
["node", "-e", script], check=True, capture_output=True, text=True,
env={**os.environ, "TZ": "UTC"},
)
assert json.loads(result.stdout) == {
"today": "2026-08-08T16:00:00.000Z",
"tomorrow": "2026-08-09T09:00:00.000Z",
"removed": True,
"partition": {"active": [{
"kind": "update", "repository": "stackchain/api", "number": 17,
"notification_id": 91,
}], "later": []},
}
def test_detail_defer_closes_normal_triage_but_keeps_session_open_for_reconcile():
script = f"""
const createDetailDefer = require({json.dumps(str(DETAIL_DEFER))});
const calls = [];
let sessionActive = false;
const controller = createDetailDefer({{
laterWork: {{
presetUntil:preset => new Date(preset === 'today' ? '2026-08-08T16:00:00Z' : '2026-08-09T09:00:00Z'),
defer:(item, until) => {{ calls.push(['defer', item.title, until.toISOString()]); return 'deferred'; }},
}},
session: {{ active:() => sessionActive }},
close:() => calls.push(['close']),
refresh:() => calls.push(['refresh']),
focus:() => calls.push(['focus']),
announce:message => calls.push(['announce', message]),
formatTime:value => value.toISOString(),
}});
const item = {{kind:'issue',repository:'stackchain/api',number:17,title:'Read first'}};
const outside = controller.defer(item, 'today');
sessionActive = true;
const session = controller.defer(item, 'tomorrow');
process.stdout.write(JSON.stringify({{outside,session,calls}}));
"""
result = subprocess.run(
["node", "-e", script], check=True, capture_output=True, text=True
)
assert json.loads(result.stdout) == {
"outside": True,
"session": True,
"calls": [
["defer", "Read first", "2026-08-08T16:00:00.000Z"],
["close"],
["refresh"],
["announce", "Deferred until 2026-08-08T16:00:00.000Z. It stays unread and unchanged in Gitea."],
["focus"],
["defer", "Read first", "2026-08-09T09:00:00.000Z"],
["refresh"],
["announce", "Deferred until 2026-08-09T09:00:00.000Z. It stays unread and unchanged in Gitea."],
],
}
def test_detail_defer_reports_storage_failure_without_closing_or_refreshing():
script = f"""
const createDetailDefer = require({json.dumps(str(DETAIL_DEFER))});
const calls = [];
const controller = createDetailDefer({{
laterWork: {{
presetUntil:() => new Date('2026-08-08T16:00:00Z'),
defer:() => 'unavailable',
}},
session: {{active:() => false}},
close:() => calls.push('close'), refresh:() => calls.push('refresh'),
focus:() => calls.push('focus'), announce:message => calls.push(message),
}});
const saved = controller.defer({{kind:'issue',repository:'stackchain/api',number:17}}, 'today');
process.stdout.write(JSON.stringify({{saved,calls}}));
"""
result = subprocess.run(
["node", "-e", script], check=True, capture_output=True, text=True
)
assert json.loads(result.stdout) == {
"saved": False,
"calls": ["Could not save Later on this device."],
}
@pytest.mark.anyio
async def test_mobile_detail_sheets_offer_touch_safe_defer_without_a_gitea_mutation():
html = await dashboard()
assert '<script src="static/detail-defer.js"></script>' in html
assert html.count('class="detail-defer"') == 4
assert html.count('data-detail-defer-preset="today"') == 4
assert html.count('data-detail-defer-preset="tomorrow"') == 4
assert 'const detailDefer = createDetailDefer({' in html
assert 'selectedUpdate || selectedReview || selectedIssue || selectedPull' in html
assert "detailDefer.defer(item, button.dataset.detailDeferPreset)" in html
assert ".detail-defer summary, .detail-defer button { min-height:44px;" in html
assert "fetch(" not in DETAIL_DEFER.read_text()
@pytest.mark.anyio
async def test_mobile_my_work_wires_touch_safe_non_mutating_later_actions():
html = await dashboard()
assert '<script src="static/later-work.js"></script>' in html
assert 'data-work-filter="later"' in html
assert 'data-work-count="later"' in html
assert 'const laterWork = createLaterWork({' in html
assert 'getLogin: () => planningOwnerLogin' in html
assert 'laterWork.partition(lastMyWork,' in html
assert 'data-later-preset="today"' in html
assert 'data-later-preset="tomorrow"' in html
assert 'data-later-restore' in html
assert "Deferred until ' + escapeHtml(fmt(item.deferred_until))" in html
assert "const result = laterWork.defer(item, until)" in html
assert 'laterWork.restore(item)' in html
assert '.later-actions button { min-height:44px;' in html
assert "'Deferred until ' + fmt(until)" in html
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('<span class="small" aria-live="polite" data-work-session-progress>') == 4
assert html.count('<button type="button" data-work-session-previous>') == 4
assert html.count('<button type="button" data-work-session-next>') == 4
assert '.work-session-nav button { min-height:44px;' in html
assert 'padding-bottom:calc(10px + env(safe-area-inset-bottom))' in html
assert 'aria-live="polite" data-work-session-progress' in html
@pytest.mark.anyio
async def test_dashboard_wires_work_session_to_existing_sheet_flows_and_completion_actions():
html = await dashboard()
assert 'const workSession = createWorkSession({' in html
assert 'function openWorkSessionItem(item)' in html
assert "qs('#start-work-session').addEventListener('click'" in html
assert "document.querySelectorAll('[data-work-session-previous]')" in html
assert "document.querySelectorAll('[data-work-session-next]')" in html
assert 'workSession.complete();' in html
assert 'workSession.reconcile();' in html
for opener in ('openIssueSheet(item', 'openPullSheet(item', 'openReviewSheet(item', 'notificationReader.open(item'):
assert opener in html
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, "attention": 1, "issue": 1, "pull": 1, "review": 1, "update": 0
}
def test_work_pager_is_single_flight_and_unions_pull_responsibilities():
script = f"""
const buildMyWork = require({json.dumps(str(MY_WORK))});
let calls = 0;
let release;
const pages = [];
const states = [];
const statuses = [];
const pager = buildMyWork.createWorkPager({{
load: (stream, page) => {{
calls += 1;
return new Promise(resolve => {{ release = () => resolve({{
stream, page, total: 51, has_more: false,
items: [
{{id:1, title:'shared', work_reasons:['review_requested']}},
{{id:51, title:'older', work_reasons:['review_requested']}}
],
}}); }});
}},
onItems: (stream, items) => states.push({{stream, items}}),
onPagination: pagination => pages.push(pagination),
onStatus: status => statuses.push(status),
}});
pager.reset({{review:{{page:1,total:51,has_more:true}}}});
const existing = [{{id:1,title:'shared',work_reasons:['assigned_to_me']}}];
const first = pager.loadMore('review', existing);
const duplicate = pager.loadMore('review', existing);
release();
Promise.all([first, duplicate]).then(results => process.stdout.write(JSON.stringify({{
calls, pages, 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"] == [{
"stream": "review",
"items": [
{"id": 1, "title": "shared", "work_reasons": ["assigned_to_me", "review_requested"]},
{"id": 51, "title": "older", "work_reasons": ["review_requested"]},
],
}]
assert output["pages"][-1]["review"] == {"page": 2, "total": 51, "has_more": False}
assert output["statuses"] == [
"Loading older review requests…", "51 of 51 review requests loaded."
]
assert output["results"] == [True, False]
def test_work_pager_keeps_items_and_retries_same_page_after_failure():
script = f"""
const buildMyWork = require({json.dumps(str(MY_WORK))});
const requested = [];
const states = [];
const statuses = [];
const pager = buildMyWork.createWorkPager({{
load: async (stream, page) => {{ requested.push([stream,page]); throw new Error('offline'); }},
onItems: (stream, items) => states.push(items),
onPagination: () => {{}},
onStatus: status => statuses.push(status),
}});
pager.reset({{issue:{{page:2,total:125,has_more:true}}}});
pager.loadMore('issue', [{{id:1}}]).then(result =>
pager.loadMore('issue', [{{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": [["issue", 3], ["issue", 3]],
"states": [],
"statuses": [
"Loading older issues…", "Could not load older issues. Retry.",
"Loading older issues…", "Could not load older issues. Retry.",
],
"result": False,
"retry": False,
}
def test_find_work_claim_is_single_flight_and_removes_only_confirmed_issue():
script = f"""
const createFindWork = require({json.dumps(str(PICK_WORK))});
let calls = 0;
let release;
const states = [];
const statuses = [];
const pages = [];
const controller = createFindWork({{
fetchJson: (url, options) => {{
calls += 1;
return new Promise(resolve => {{ release = () => resolve({{
id:17,number:7,title:'Available',repository:'stackchain/api',
state:'open',labels:[],assignees:['timmy'],url:'https://forge.example/issues/7'
}}); }});
}},
onItems: items => states.push(items),
onPagination: page => pages.push(page),
onStatus: status => statuses.push(status),
}});
controller.reset({{items:[
{{id:17,number:7,title:'Available',repository:'stackchain/api'}},
{{id:18,number:8,title:'Other',repository:'stackchain/web'}}
],page:1,total:2,has_more:false}});
const item = controller.items()[0];
const first = controller.claim(item);
const duplicate = controller.claim(item);
release();
Promise.all([first,duplicate]).then(results => process.stdout.write(JSON.stringify({{
calls,states,statuses,pages,results,remaining:controller.items()
}})));
"""
result = subprocess.run(
["node", "-e", script], check=True, capture_output=True, text=True
)
output = json.loads(result.stdout)
assert output["calls"] == 1
assert [item["number"] for item in output["remaining"]] == [8]
assert [item["number"] for item in output["states"][-1]] == [8]
assert output["statuses"] == ["Assigning stackchain/api#7…", "Assigned stackchain/api#7 to you."]
assert output["pages"][-1] == {"page": 1, "total": 1, "has_more": False}
assert output["results"][0]["assignees"] == ["timmy"]
assert output["results"][1]["assignees"] == ["timmy"]
def test_find_work_loads_paginated_results_without_duplicates():
script = f"""
const createFindWork = require({json.dumps(str(PICK_WORK))});
const calls = [];
const states = [];
const pages = [];
const controller = createFindWork({{
fetchJson: url => {{
calls.push(url);
const page = Number(new URL(url, 'https://example.test/').searchParams.get('page'));
return Promise.resolve(page === 1 ? {{
items:[{{id:1,number:1,repository:'stackchain/api'}}],page:1,total:2,has_more:true
}} : {{
items:[{{id:1,number:1,repository:'stackchain/api'}},{{id:2,number:2,repository:'stackchain/web'}}],
page:2,total:2,has_more:false
}});
}},
onItems: items => states.push(items),
onPagination: page => pages.push(page),
onStatus: () => {{}},
}});
controller.load().then(() => controller.loadMore()).then(() =>
process.stdout.write(JSON.stringify({{calls,states,pages,items:controller.items()}}))
);
"""
result = subprocess.run(
["node", "-e", script], check=True, capture_output=True, text=True
)
output = json.loads(result.stdout)
assert output["calls"] == ["api/v1/available-issues?page=1", "api/v1/available-issues?page=2"]
assert [item["id"] for item in output["items"]] == [1, 2]
assert output["pages"][-1] == {"page": 2, "total": 2, "has_more": False}
def test_find_work_keeps_retained_cards_and_announces_refresh_freshness():
script = f"""
const createFindWork = require({json.dumps(str(PICK_WORK))});
const statuses = [];
const states = [];
const responses = [
{{items:[{{id:1,number:1,repository:'stackchain/api'}}],page:1,total:1,has_more:false,
stale:true,revalidating:true}},
{{items:[{{id:1,number:1,repository:'stackchain/api'}}],page:1,total:1,has_more:false,
stale:true,refresh_failed:true}},
];
const controller = createFindWork({{
fetchJson: () => Promise.resolve(responses.shift()),
onItems: items => states.push(items),
onPagination: () => {{}},
onStatus: status => statuses.push(status),
}});
controller.load().then(() => controller.load()).then(() =>
process.stdout.write(JSON.stringify({{statuses,states,items:controller.items()}}))
);
"""
result = subprocess.run(
["node", "-e", script], check=True, capture_output=True, text=True
)
output = json.loads(result.stdout)
assert output["statuses"] == [
"Showing saved available work while the catalog refreshes…",
"Showing saved available work. Catalog refresh failed; retrying shortly.",
]
assert [item["number"] for item in output["items"]] == [1]
assert len(output["states"]) == 2
def test_find_work_preview_stays_with_issue_across_pagination_and_clears_when_claimed():
script = f"""
const createFindWork = require({json.dumps(str(PICK_WORK))});
const states = [];
const controller = createFindWork({{
fetchJson: (url, options) => options?.method === 'PATCH'
? Promise.resolve({{number:7,repository:'stackchain/api',assignees:['timmy']}})
: Promise.resolve({{
items:[
{{id:17,number:7,title:'Preview me',repository:'stackchain/api',body:'Full scope'}},
{{id:18,number:8,title:'Next',repository:'stackchain/web',body:''}}
],page:2,total:2,has_more:false
}}),
onItems: items => states.push(items),
onPagination: () => {{}},
onStatus: () => {{}},
}});
controller.reset({{
items:[{{id:17,number:7,title:'Preview me',repository:'stackchain/api',body:'Full scope'}}],
page:1,total:2,has_more:true
}});
const target = controller.items()[0];
const opened = controller.togglePreview(target);
controller.loadMore().then(() => {{
const afterPagination = controller.isPreviewed(controller.items()[0]);
return controller.claim(controller.items()[0]).then(() => process.stdout.write(JSON.stringify({{
opened, afterPagination, afterClaim:controller.isPreviewed(target), states
}})));
}});
"""
result = subprocess.run(
["node", "-e", script], check=True, capture_output=True, text=True
)
output = json.loads(result.stdout)
assert output["opened"] is True
assert output["afterPagination"] is True
assert output["afterClaim"] is False
assert [item["number"] for item in output["states"][-1]] == [8]
@pytest.mark.anyio
async def test_mobile_my_work_exposes_truthful_work_pagination_control():
html = await dashboard()
assert 'id="work-page-status"' in html
assert 'id="load-more-work"' in html
assert '>Load older work<' in html
assert '.load-more-work { min-height:44px;' in html
@pytest.mark.anyio
async def test_mobile_assigned_issue_sheet_exposes_touch_sized_content_editor():
html = await dashboard()
assert 'id="edit-issue-content"' in html
assert 'id="issue-edit-form"' in html
assert 'id="issue-edit-title" type="text" maxlength="255"' in html
assert 'id="issue-edit-body" maxlength="10000"' in html
assert 'id="save-issue-content"' in html
assert 'id="cancel-issue-content"' in html
assert 'id="retry-issue-load" type="button" hidden>Reload latest issue</button>' 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</a>' 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, "attention": 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_surfaces_pending_merge_confirmation_guidance():
script = f"""
const createPullSheet = require({json.dumps(str(PULL_SHEET))});
const controller = createPullSheet({{
storage: null,
fetchJson: () => Promise.resolve({{
number: 7,
merged: false,
state: 'unknown',
confirmation_pending: true,
error: 'Merge confirmation is pending. Check its status before retrying.',
}}),
}});
controller.merge({{repository:'stackchain/api', number:7}}, 'abc123')
.then(() => process.stdout.write(JSON.stringify({{resolved:true}})))
.catch(error => process.stdout.write(JSON.stringify({{resolved:false, message:error.message}})));
"""
result = subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True)
assert json.loads(result.stdout) == {
"resolved": False,
"message": "Merge confirmation is pending. Check its status before retrying.",
}
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('<h2>Full conversation</h2>', body)
composer = html.index('id="pull-comment-title"', conversation)
review = html.index('id="pull-review"', composer)
files = html.index('id="pull-files"', review)
assert '<summary><h2>Review &amp; merge</h2></summary>' in html
assert '<details class="pull-review" id="pull-review">' 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('&', '&amp;').replaceAll('<', '&lt;');
const text = createPullSheet.renderFile({{
filename:'src/<unsafe>.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 "&lt;unsafe>" in output["text"] and "+new" in output["text"]
assert "Preview truncated" in output["text"]
assert 'data-pull-review-file="src/&lt;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 '<script src="static/conversation.js"></script>' 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 '<script src="static/pull-sheet.js"></script>' 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 '<script src="static/issue-sheet.js"></script>' 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 '<script src="static/create-issue-sheet.js"></script>' 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 '<script src="static/my-work.js"></script>' 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 '<a class="my-work-card"' not in html
@pytest.mark.anyio
async def test_mobile_update_reader_is_in_app_safe_area_aware_and_actionable():
html = await dashboard()
assert 'id="update-sheet"' in html and 'aria-modal="true"' in html
assert 'class="read-update"' in html
assert 'id="keep-update-unread"' in html
assert 'id="mark-update-read-next"' in html
assert 'id="retry-update-load"' in html
assert 'id="update-comments"' in html
assert 'id="update-subject-body"' in html
assert 'id="open-update-gitea"' in html
assert '.update-sheet-panel { width:min(560px,100%);' in html
assert '.update-sheet-content { overflow-wrap:anywhere;' in html
assert 'padding-bottom:calc(10px + env(safe-area-inset-bottom));' in html
assert '.update-sheet-actions button, .update-sheet-actions a { min-height:44px;' in html
assert "createNotificationReader" in html
assert "api/v1/notifications/" in html
assert "notificationReader.markReadAndNext(lastMyWork)" in html
def test_notification_replier_preserves_failed_draft_and_clears_only_after_success():
script = f"""
const buildMyWork = require({json.dumps(str(MY_WORK))});
const values = new Map();
const storage = {{
getItem: key => values.has(key) ? values.get(key) : null,
setItem: (key, value) => values.set(key, value),
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="today"' in html
assert 'data-work-count="attention"' 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 'data-work-count="later"' in html
assert "['all', 'today', 'attention', 'issue', 'pull', 'review', 'update', 'later', '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('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;');
const html = reviewSheet.renderDiffFile({{
filename: 'src/<api>.py', status: 'modified', additions: 1, deletions: 1,
diff_available: true, diff_truncated: true,
diff_lines: ['@@ -1 +1 @@', '-old <token>', '+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/&lt;api&gt;.py' in output["html"]
assert '-old &lt;token&gt;' in output["html"]
assert '+new &amp; safe' in output["html"]
assert 'Preview truncated' in output["html"]
assert 'class="review-mark"' in output["html"]
assert 'data-review-filename="src/&lt;api&gt;.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/&lt;api&gt;.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 '<script src="static/review-sheet.js"></script>' 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 '<h2>Full conversation</h2>' 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": []}