stackchain-dashboard/tests/test_my_work.py
timmy 2072de337e
All checks were successful
CI / lint (pull_request) Successful in 14s
CI / build-frontend (pull_request) Successful in 5s
feat: add mobile My Work sessions (#193)
2026-08-07 11:54:31 +00:00

2390 lines
96 KiB
Python

import json
import subprocess
from pathlib import Path
import pytest
from src.views import dashboard
MY_WORK = Path(__file__).parents[1] / "frontend" / "my-work.js"
REVIEW_SHEET = Path(__file__).parents[1] / "frontend" / "review-sheet.js"
ISSUE_SHEET = Path(__file__).parents[1] / "frontend" / "issue-sheet.js"
CREATE_ISSUE_SHEET = Path(__file__).parents[1] / "frontend" / "create-issue-sheet.js"
PULL_SHEET = Path(__file__).parents[1] / "frontend" / "pull-sheet.js"
PICK_WORK = Path(__file__).parents[1] / "frontend" / "pick-work.js"
def test_my_work_queue_prioritizes_labels_then_reviews_and_keeps_repo_identity():
payload = {
"user": {"login": "timmy"},
"issues": [
{
"id": 1,
"number": 7,
"title": "Assigned issue",
"state": "open",
"repository": "stackchain/mobile",
"labels": [],
"assignees": ["timmy"],
"updated_at": "2026-08-06T12:00:00Z",
"url": "https://forge.example/mobile/issues/7",
},
{
"id": 2,
"number": 7,
"title": "Priority issue",
"state": "open",
"repository": "stackchain/api",
"labels": ["P0"],
"assignees": [],
"updated_at": "2026-08-06T11:00:00Z",
"url": "https://forge.example/api/issues/7",
},
],
"pull_requests": [
{
"id": 3,
"number": 4,
"title": "Review PR",
"state": "open",
"repository": "stackchain/web",
"work_reasons": ["review_requested"],
"updated_at": "2026-08-06T13:00:00Z",
"url": "https://forge.example/web/pulls/4",
}
],
}
script = f"""
const buildMyWork = require({json.dumps(str(MY_WORK))});
const queue = buildMyWork({json.dumps(payload)});
process.stdout.write(JSON.stringify(queue));
"""
result = subprocess.run(
["node", "-e", script], check=True, capture_output=True, text=True
)
queue = json.loads(result.stdout)
assert [item["title"] for item in queue] == [
"Priority issue",
"Review PR",
"Assigned issue",
]
assert queue[0]["key"] == "stackchain/api#7"
assert queue[0]["reason"] == "P0 priority"
assert queue[1]["reason"] == "Needs your review"
assert queue[1]["is_review"] is True
assert queue[2]["reason"] == "Assigned to you"
def test_my_work_surfaces_and_ranks_due_issues_after_p0_before_ordinary_work():
payload = {
"user": {"login": "timmy"},
"issues": [
{"number": 1, "title": "Ordinary", "repository": "stackchain/api",
"labels": [], "assignees": ["timmy"], "updated_at": "2026-08-07T12:00:00Z"},
{"number": 2, "title": "Due today", "repository": "stackchain/api",
"labels": [], "assignees": ["timmy"], "due_date": "2026-08-07T23:59:59Z"},
{"number": 3, "title": "Overdue", "repository": "stackchain/api",
"labels": [], "assignees": ["timmy"], "due_date": "2026-08-06T23:59:59Z"},
{"number": 4, "title": "P0 future", "repository": "stackchain/api",
"labels": ["P0"], "assignees": ["timmy"], "due_date": "2026-08-10T23:59:59Z"},
],
"pull_requests": [],
}
script = f"""
const buildMyWork = require({json.dumps(str(MY_WORK))});
const queue = buildMyWork({json.dumps(payload)}, new Date('2026-08-07T12:00:00Z'));
process.stdout.write(JSON.stringify(queue.map(item => ({{title:item.title,reason:item.reason,due_label:item.due_label}}))));
"""
result = subprocess.run(
["node", "-e", script], check=True, capture_output=True, text=True
)
assert json.loads(result.stdout) == [
{"title": "P0 future", "reason": "P0 priority", "due_label": "Due Aug 10"},
{"title": "Overdue", "reason": "Overdue", "due_label": "Overdue"},
{"title": "Due today", "reason": "Due today", "due_label": "Due today"},
{"title": "Ordinary", "reason": "Assigned to you"},
]
def test_confirmed_issue_labels_replace_snapshot_and_reprioritize_queue():
payload = {
"user": {"login": "timmy"},
"issues": [
{"number": 1, "title": "Older", "repository": "stackchain/api",
"labels": [], "assignees": ["timmy"], "updated_at": "2026-08-06T10:00:00Z"},
{"number": 2, "title": "Newer", "repository": "stackchain/api",
"labels": [], "assignees": ["timmy"], "updated_at": "2026-08-06T12:00:00Z"},
],
"pull_requests": [],
}
script = f"""
const buildMyWork = require({json.dumps(str(MY_WORK))});
const original = {json.dumps(payload)};
const updated = buildMyWork.replaceIssueLabels(original, 'stackchain/api', 1, ['P0']);
process.stdout.write(JSON.stringify({{
titles: buildMyWork(updated).map(item => item.title),
labels: updated.issues[0].labels,
original: original.issues[0].labels,
}}));
"""
result = subprocess.run(
["node", "-e", script], check=True, capture_output=True, text=True
)
assert json.loads(result.stdout) == {
"titles": ["Older", "Newer"],
"labels": ["P0"],
"original": [],
}
def test_confirmed_issue_content_updates_my_work_without_mutating_snapshot():
payload = {
"issues": [{"number": 17, "repository": "stackchain/api", "title": "Old", "body": "Old body"}],
"pull_requests": [],
}
script = f"""
const buildMyWork = require({json.dumps(str(MY_WORK))});
const original = {json.dumps(payload)};
const updated = buildMyWork.replaceIssueContent(
original, 'stackchain/api', 17,
{{title:'Clarified',body:'New body',updated_at:'2026-08-07T10:01:00Z'}}
);
process.stdout.write(JSON.stringify({{updated:updated.issues[0],original:original.issues[0]}}));
"""
result = subprocess.run(
["node", "-e", script], check=True, capture_output=True, text=True
)
assert json.loads(result.stdout) == {
"updated": {
"number": 17, "repository": "stackchain/api", "title": "Clarified",
"body": "New body", "updated_at": "2026-08-07T10:01:00Z",
},
"original": {
"number": 17, "repository": "stackchain/api", "title": "Old", "body": "Old body",
},
}
def test_issue_release_is_single_flight_and_requires_confirmed_unassignment():
script = f"""
const createIssueSheet = require({json.dumps(str(ISSUE_SHEET))});
let calls = 0;
let finish;
const controller = createIssueSheet({{
fetchJson: () => {{
calls += 1;
return new Promise(resolve => {{ finish = resolve; }});
}},
storage: null,
}});
const item = {{repository:'stackchain/api', number:17}};
const first = controller.release(item, 'timmy');
const duplicate = controller.release(item, 'timmy');
finish({{number:17, repository:'stackchain/api', assignees:[], available:true}});
Promise.all([first, duplicate]).then(results => process.stdout.write(JSON.stringify({{
calls, same: first === duplicate, results
}})));
"""
result = subprocess.run(
["node", "-e", script], check=True, capture_output=True, text=True
)
output = json.loads(result.stdout)
assert output["calls"] == 1
assert output["same"] is True
assert output["results"][0]["available"] is True
def test_issue_release_rejects_response_that_still_assigns_current_user():
script = f"""
const createIssueSheet = require({json.dumps(str(ISSUE_SHEET))});
const controller = createIssueSheet({{
fetchJson: async () => ({{number:17, assignees:['timmy']}}), storage: null,
}});
controller.release({{repository:'stackchain/api', number:17}}, 'timmy')
.then(() => process.stdout.write('unexpected'))
.catch(error => process.stdout.write(error.message));
"""
result = subprocess.run(
["node", "-e", script], check=True, capture_output=True, text=True
)
assert result.stdout == "Issue release was not confirmed."
def test_issue_content_edit_is_single_flight_and_keeps_scoped_draft_until_confirmed():
script = f"""
const createIssueSheet = require({json.dumps(str(ISSUE_SHEET))});
const values = new Map();
const storage = {{
getItem:key => values.get(key) || null,
setItem:(key,value) => values.set(key,value),
removeItem:key => values.delete(key),
}};
let calls = [];
let finish;
const controller = createIssueSheet({{
storage,
fetchJson:(url, options) => {{
calls.push({{url, options}});
return new Promise(resolve => {{ finish = resolve; }});
}},
}});
const item = {{repository:'stackchain/api', number:17}};
const draft = {{title:'Clarified scope', body:'Updated body', expectedUpdatedAt:'2026-08-07T10:00:00Z'}};
controller.saveEditDraft(item, draft);
const first = controller.updateContent(item, draft);
const duplicate = controller.updateContent(item, draft);
const during = controller.loadEditDraft(item);
finish({{repository:'stackchain/api',number:17,title:'Clarified scope',body:'Updated body',updated_at:'2026-08-07T10:01:00Z'}});
Promise.all([first, duplicate]).then(results => process.stdout.write(JSON.stringify({{
calls:calls.map(call => ({{url:call.url,method:call.options.method,body:JSON.parse(call.options.body)}})),
same:first === duplicate, during, after:controller.loadEditDraft(item), results
}})));
"""
result = subprocess.run(
["node", "-e", script], check=True, capture_output=True, text=True
)
output = json.loads(result.stdout)
assert output["calls"] == [{
"url": "api/v1/repos/stackchain/api/issues/17/content",
"method": "PATCH",
"body": {
"title": "Clarified scope", "body": "Updated body",
"expected_updated_at": "2026-08-07T10:00:00Z",
},
}]
assert output["same"] is True
assert output["during"] == {
"title": "Clarified scope", "body": "Updated body",
"expectedUpdatedAt": "2026-08-07T10:00:00Z",
}
assert output["after"] is None
assert output["results"][0]["updated_at"] == "2026-08-07T10:01:00Z"
def test_issue_due_date_update_is_single_flight_and_keeps_draft_until_confirmed():
script = f"""
const createIssueSheet = require({json.dumps(str(ISSUE_SHEET))});
const values = new Map();
const storage = {{
getItem:key => values.get(key) || null,
setItem:(key,value) => values.set(key,value),
removeItem:key => values.delete(key),
}};
let calls = [];
let finish;
const controller = createIssueSheet({{
storage,
fetchJson:(url, options) => {{
calls.push({{url, options}});
return new Promise(resolve => {{ finish = resolve; }});
}},
}});
const item = {{repository:'stackchain/api', number:17}};
const first = controller.updateDueDate(item, '2026-08-09T23:59:59Z');
const duplicate = controller.updateDueDate(item, '2026-08-09T23:59:59Z');
const during = controller.loadDueDateDraft(item);
finish({{repository:'stackchain/api',number:17,state:'open',due_date:'2026-08-09T23:59:59Z'}});
Promise.all([first, duplicate]).then(results => process.stdout.write(JSON.stringify({{
calls:calls.map(call => ({{url:call.url,method:call.options.method,body:JSON.parse(call.options.body)}})),
same:first === duplicate, during, after:controller.loadDueDateDraft(item), results
}})));
"""
result = subprocess.run(
["node", "-e", script], check=True, capture_output=True, text=True
)
output = json.loads(result.stdout)
assert output["calls"] == [{
"url": "api/v1/repos/stackchain/api/issues/17/due-date",
"method": "PATCH",
"body": {"due_date": "2026-08-09T23:59:59Z"},
}]
assert output["same"] is True
assert output["during"] == "2026-08-09T23:59:59Z"
assert output["after"] is None
assert output["results"][0]["due_date"] == "2026-08-09T23:59:59Z"
@pytest.mark.anyio
async def test_mobile_issue_sheet_exposes_touch_sized_due_date_editor_and_card_badge():
html = await dashboard()
assert 'id="issue-due-date" type="date"' in html
assert 'id="save-issue-due-date"' in html
assert 'id="clear-issue-due-date"' in html
assert 'class="pill due-badge"' in html
assert '.issue-due-editor input, .issue-due-editor button { min-height:44px;' in html
assert 'padding-bottom:calc(10px + env(safe-area-inset-bottom))' in html
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_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, "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_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
@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_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_unread_updates_enrich_matching_work_and_keep_unassigned_mentions_actionable():
payload = {
"user": {"login": "timmy"},
"issues": [{
"id": 1, "number": 7, "title": "Assigned issue", "repository": "stackchain/api",
"labels": [], "assignees": ["timmy"], "updated_at": "2026-08-06T10:00:00Z",
"url": "https://forge.example/stackchain/api/issues/7",
}],
"pull_requests": [],
"notifications": [
{
"id": 42, "number": 7, "title": "Assigned issue", "repository": "stackchain/api",
"subject_type": "Issue", "unread": True, "updated_at": "2026-08-06T12:00:00Z",
"url": "https://forge.example/stackchain/api/issues/7#issuecomment-9",
},
{
"id": 43, "number": 8, "title": "Mention only", "repository": "stackchain/web",
"subject_type": "Issue", "unread": True, "updated_at": "2026-08-06T13:00:00Z",
"url": "https://forge.example/stackchain/web/issues/8#issuecomment-2",
},
],
}
script = f"""
const buildMyWork = require({json.dumps(str(MY_WORK))});
const queue = buildMyWork({json.dumps(payload)});
process.stdout.write(JSON.stringify({{
queue,
updates: buildMyWork.filterMyWork(queue, 'update'),
counts: buildMyWork.countMyWork(queue),
summary: buildMyWork.summarizeMyWork(queue),
}}));
"""
result = subprocess.run(
["node", "-e", script], check=True, capture_output=True, text=True
)
output = json.loads(result.stdout)
assert len(output["queue"]) == 2
assert [item["key"] for item in output["updates"]] == ["stackchain/web#8", "stackchain/api#7"]
assert output["updates"][0]["kind"] == "update"
assert output["updates"][1]["kind"] == "issue"
assert output["updates"][1]["url"].endswith("#issuecomment-9")
assert output["counts"] == {"all": 2, "issue": 1, "pull": 0, "review": 0, "update": 2}
assert output["summary"] == "2 unread updates · 0 reviews · 1 assigned"
def test_unread_update_correlation_distinguishes_issue_and_pull_with_same_number():
payload = {
"user": {"login": "timmy"},
"issues": [{"number": 7, "title": "Issue seven", "repository": "stackchain/api", "url": "https://forge.example/issues/7"}],
"pull_requests": [{"number": 7, "title": "Pull seven", "repository": "stackchain/api", "url": "https://forge.example/pulls/7"}],
"notifications": [{
"id": 42, "number": 7, "title": "Issue seven", "repository": "stackchain/api",
"subject_type": "Issue", "unread": True, "url": "https://forge.example/issues/7#comment-1",
}],
}
script = f"""
const buildMyWork = require({json.dumps(str(MY_WORK))});
process.stdout.write(JSON.stringify(buildMyWork({json.dumps(payload)})));
"""
result = subprocess.run(
["node", "-e", script], check=True, capture_output=True, text=True
)
queue = json.loads(result.stdout)
issue = next(item for item in queue if item["kind"] == "issue")
pull = next(item for item in queue if item["kind"] == "pull")
assert issue["has_update"] is True
assert issue["url"].endswith("#comment-1")
assert pull["has_update"] is False
def test_notification_identity_survives_merge_and_acknowledgement_preserves_assigned_work():
payload = {
"user": {"login": "timmy"},
"issues": [{
"number": 7, "title": "Assigned issue", "repository": "stackchain/api",
"assignees": ["timmy"], "url": "https://forge.example/issues/7",
}],
"notifications": [
{"id": 42, "number": 7, "title": "Assigned issue", "repository": "stackchain/api",
"subject_type": "Issue", "unread": True, "url": "https://forge.example/issues/7#comment"},
{"id": 43, "number": 8, "title": "Mention", "repository": "stackchain/web",
"subject_type": "Issue", "unread": True, "url": "https://forge.example/issues/8"},
],
}
script = f"""
const buildMyWork = require({json.dumps(str(MY_WORK))});
const before = buildMyWork({json.dumps(payload)});
const afterMerged = buildMyWork.acknowledgeNotification(before, 42);
const afterStandalone = buildMyWork.acknowledgeNotification(before, 43);
process.stdout.write(JSON.stringify({{before, afterMerged, afterStandalone}}));
"""
result = subprocess.run(
["node", "-e", script], check=True, capture_output=True, text=True
)
output = json.loads(result.stdout)
assigned = next(item for item in output["before"] if item["kind"] == "issue")
assert assigned["notification_id"] == 42
assert assigned["has_update"] is True
assert len(output["afterMerged"]) == 2
acknowledged = next(item for item in output["afterMerged"] if item["kind"] == "issue")
assert acknowledged["has_update"] is False
assert "notification_id" not in acknowledged
assert [item["notification_id"] for item in output["afterStandalone"] if item["has_update"]] == [42]
def test_notification_acknowledger_is_single_flight_and_rolls_back_on_failure():
script = f"""
const buildMyWork = require({json.dumps(str(MY_WORK))});
const original = [{{kind:'update', notification_id:42, has_update:true}}];
let calls = 0;
let release;
const states = [];
const statuses = [];
const controller = buildMyWork.createNotificationAcknowledger({{
markRead: () => {{ calls += 1; return new Promise((resolve, reject) => {{ release = reject; }}); }},
onItems: items => states.push(items),
onStatus: status => statuses.push(status),
}});
const first = controller.acknowledge(original, 42);
const duplicate = controller.acknowledge(original, 42);
release(new Error('offline'));
Promise.all([first, duplicate]).then(results => process.stdout.write(JSON.stringify({{
calls, states, statuses, results
}})));
"""
result = subprocess.run(
["node", "-e", script], check=True, capture_output=True, text=True
)
output = json.loads(result.stdout)
assert output["calls"] == 1
assert output["states"] == [[], [{"kind": "update", "notification_id": 42, "has_update": True}]]
assert output["statuses"] == ["Marking update read…", "Could not mark update read. Retry."]
assert output["results"] == [False, False]
def test_bulk_notification_acknowledger_deduplicates_and_keeps_partial_failures_retryable():
script = f"""
const buildMyWork = require({json.dumps(str(MY_WORK))});
const original = [
{{kind:'issue', key:'repo#1', notification_id:42, has_update:true}},
{{kind:'update', key:'repo#2', notification_id:43, has_update:true}},
{{kind:'update', key:'repo#3', notification_id:44, has_update:true}},
];
let calls = 0;
let release;
const states = [];
const statuses = [];
const controller = buildMyWork.createBulkNotificationAcknowledger({{
markRead: ids => {{
calls += 1;
return new Promise(resolve => {{ release = () => resolve({{marked:[42,43], failed:[44]}}); }});
}},
onItems: items => states.push(items),
onStatus: status => statuses.push(status),
}});
const first = controller.acknowledge(original, [42, 43, 42, 44]);
const duplicate = controller.acknowledge(original, [42, 43, 44]);
release();
Promise.all([first, duplicate]).then(results => process.stdout.write(JSON.stringify({{
calls, states, statuses, results,
retryIds: buildMyWork.notificationIds(states.at(-1)),
}})));
"""
result = subprocess.run(
["node", "-e", script], check=True, capture_output=True, text=True
)
output = json.loads(result.stdout)
assert output["calls"] == 1
assert output["results"] == [
{"marked": [42, 43], "failed": [44]},
False,
]
assert output["states"] == [[
{"kind": "issue", "key": "repo#1", "has_update": False},
{"kind": "update", "key": "repo#3", "notification_id": 44, "has_update": True},
]]
assert output["retryIds"] == [44]
assert output["statuses"] == [
"Marking 3 updates read…",
"2 marked read · 1 could not be updated — retry.",
]
def test_notification_pager_is_single_flight_and_merges_unique_updates():
script = f"""
const buildMyWork = require({json.dumps(str(MY_WORK))});
let calls = 0;
let release;
const pages = [];
const notifications = [];
const statuses = [];
const pager = buildMyWork.createNotificationPager({{
load: page => {{
calls += 1;
return new Promise(resolve => {{ release = () => resolve({{
items: [{{id:50, title:'duplicate'}}, {{id:51, title:'older'}}],
page, total: 75, has_more: false,
}}); }});
}},
onNotifications: items => notifications.push(items),
onPagination: page => pages.push(page),
onStatus: status => statuses.push(status),
}});
pager.reset({{page:1, total:75, has_more:true}});
const existing = [{{id:50, title:'newer'}}];
const first = pager.loadMore(existing);
const duplicate = pager.loadMore(existing);
release();
Promise.all([first, duplicate]).then(results => process.stdout.write(JSON.stringify({{
calls, pages, notifications, statuses, results
}})));
"""
result = subprocess.run(
["node", "-e", script], check=True, capture_output=True, text=True
)
output = json.loads(result.stdout)
assert output["calls"] == 1
assert output["notifications"] == [[
{"id": 50, "title": "newer"},
{"id": 51, "title": "older"},
]]
assert output["pages"][-1] == {"page": 2, "total": 75, "has_more": False}
assert output["statuses"] == ["Loading older updates…", "75 of 75 unread updates loaded."]
assert output["results"] == [True, False]
def test_notification_pager_keeps_loaded_updates_and_retries_the_same_page_after_failure():
script = f"""
const buildMyWork = require({json.dumps(str(MY_WORK))});
const states = [];
const statuses = [];
const requested = [];
const pager = buildMyWork.createNotificationPager({{
load: async page => {{ requested.push(page); throw new Error('offline'); }},
onNotifications: items => states.push(items),
onPagination: () => {{}},
onStatus: status => statuses.push(status),
}});
pager.reset({{page:2, total:125, has_more:true}});
pager.loadMore([{{id:1}}]).then(result =>
pager.loadMore([{{id:1}}]).then(retry =>
process.stdout.write(JSON.stringify({{requested, states, statuses, result, retry}}))
)
);
"""
result = subprocess.run(
["node", "-e", script], check=True, capture_output=True, text=True
)
assert json.loads(result.stdout) == {
"requested": [3, 3],
"states": [],
"statuses": [
"Loading older updates…", "Could not load older updates. Retry.",
"Loading older updates…", "Could not load older updates. Retry.",
],
"result": False,
"retry": False,
}
def test_notification_reader_marks_current_read_and_opens_next_update():
script = f"""
const buildMyWork = require({json.dumps(str(MY_WORK))});
const original = [
{{kind:'update', key:'repo#1', notification_id:42, has_update:true, title:'First'}},
{{kind:'issue', key:'repo#2', notification_id:43, has_update:true, title:'Second'}},
];
const loaded = [];
const opened = [];
const details = [];
const states = [];
const statuses = [];
const closed = [];
const reader = buildMyWork.createNotificationReader({{
load: async id => {{ loaded.push(id); return {{id, latest_comment:{{body:'Comment ' + id}}}}; }},
markRead: async id => id,
onOpen: item => opened.push(item.notification_id),
onDetail: detail => details.push(detail.id),
onItems: items => states.push(items),
onStatus: status => statuses.push(status),
onClose: () => closed.push(true),
}});
reader.open(original[0], original).then(() =>
reader.markReadAndNext(original).then(result =>
process.stdout.write(JSON.stringify({{loaded, opened, details, states, statuses, closed, result}}))
)
);
"""
result = subprocess.run(
["node", "-e", script], check=True, capture_output=True, text=True
)
output = json.loads(result.stdout)
assert output["loaded"] == [42, 43]
assert output["opened"] == [42, 43]
assert output["details"] == [42, 43]
assert output["states"] == [[
{"kind": "issue", "key": "repo#2", "notification_id": 43,
"has_update": True, "title": "Second"},
]]
assert output["statuses"] == [
"Loading update…", "Update ready.", "Marking update read…",
"Loading update…", "Update ready.",
]
assert output["closed"] == []
assert output["result"]["next"]["notification_id"] == 43
def test_notification_reader_keeps_current_update_retryable_when_detail_load_fails():
script = f"""
const buildMyWork = require({json.dumps(str(MY_WORK))});
const item = {{kind:'update', notification_id:42, has_update:true}};
const events = [];
const reader = buildMyWork.createNotificationReader({{
load: async () => {{ throw new Error('offline'); }},
markRead: async () => {{}}, onOpen: () => events.push('open'),
onDetail: () => events.push('detail'), onItems: () => events.push('items'),
onStatus: status => events.push(status), onClose: () => events.push('close'),
}});
reader.open(item, [item]).then(result =>
process.stdout.write(JSON.stringify({{result, events}}))
);
"""
result = subprocess.run(
["node", "-e", script], check=True, capture_output=True, text=True
)
assert json.loads(result.stdout) == {
"result": False,
"events": [
"open", "Loading update…",
"Could not load update. Retry or open it in Gitea.",
],
}
def test_notification_reader_does_not_advance_or_remove_item_when_mark_read_fails():
script = f"""
const buildMyWork = require({json.dumps(str(MY_WORK))});
const item = {{kind:'update', notification_id:42, has_update:true}};
const events = [];
const reader = buildMyWork.createNotificationReader({{
load: async id => ({{id}}), markRead: async () => {{ throw new Error('offline'); }},
onOpen: () => events.push('open'), onDetail: () => events.push('detail'),
onItems: () => events.push('items'), onStatus: status => events.push(status),
onClose: () => events.push('close'),
}});
reader.open(item, [item]).then(() =>
reader.markReadAndNext([item]).then(result =>
process.stdout.write(JSON.stringify({{result, events}}))
)
);
"""
result = subprocess.run(
["node", "-e", script], check=True, capture_output=True, text=True
)
assert json.loads(result.stdout) == {
"result": False,
"events": [
"open", "Loading update…", "detail", "Update ready.",
"Marking update read…", "Could not mark update read. Retry.",
],
}
def test_notification_reader_wraps_to_an_earlier_visible_unread_update():
script = f"""
const buildMyWork = require({json.dumps(str(MY_WORK))});
const items = [
{{kind:'update', notification_id:42, has_update:true}},
{{kind:'update', notification_id:43, has_update:true}},
];
const opened = [];
const reader = buildMyWork.createNotificationReader({{
load: async id => ({{id}}), markRead: async () => {{}},
onOpen: item => opened.push(item.notification_id), onDetail: () => {{}},
onItems: () => {{}}, onStatus: () => {{}}, onClose: () => {{}},
}});
reader.open(items[1], items).then(() => reader.markReadAndNext(items)).then(result =>
process.stdout.write(JSON.stringify({{opened, next:result.next.notification_id}}))
);
"""
result = subprocess.run(
["node", "-e", script], check=True, capture_output=True, text=True
)
assert json.loads(result.stdout) == {"opened": [43, 42], "next": 42}
def test_notification_reader_closes_and_announces_when_final_update_is_cleared():
script = f"""
const buildMyWork = require({json.dumps(str(MY_WORK))});
const item = {{kind:'update', notification_id:42, has_update:true}};
const events = [];
const reader = buildMyWork.createNotificationReader({{
load: async id => ({{id}}), markRead: async () => {{}},
onOpen: () => {{}}, onDetail: () => {{}}, onItems: items => events.push(['items', items]),
onStatus: status => events.push(['status', status]), onClose: () => events.push(['close']),
}});
reader.open(item, [item]).then(() => {{
events.length = 0;
return reader.markReadAndNext([item]);
}}).then(result => process.stdout.write(JSON.stringify({{result, events}})));
"""
result = subprocess.run(
["node", "-e", script], check=True, capture_output=True, text=True
)
output = json.loads(result.stdout)
assert output["result"] == {"items": [], "next": None}
assert output["events"] == [
["status", "Marking update read…"],
["items", []],
["close"],
["status", "Inbox cleared."],
]
def test_issue_sheet_loads_encoded_assigned_issue_detail_path():
script = f"""
const createIssueSheet = require({json.dumps(str(ISSUE_SHEET))});
let request;
const controller = createIssueSheet({{ fetchJson: async (url, options) => {{
request = {{url, accept:options.headers.Accept}};
return {{title:'Fix mobile flow'}};
}} }});
controller.load({{repository:'stackchain/api', number:7}}).then(detail =>
process.stdout.write(JSON.stringify({{request, title:detail.title}}))
);
"""
result = subprocess.run(
["node", "-e", script], check=True, capture_output=True, text=True
)
assert json.loads(result.stdout) == {
"request": {
"url": "api/v1/repos/stackchain/api/issues/7/detail",
"accept": "application/json",
},
"title": "Fix mobile flow",
}
def test_issue_sheet_loads_labels_through_assigned_issue_boundary():
script = f"""
const createIssueSheet = require({json.dumps(str(ISSUE_SHEET))});
let request;
const controller = createIssueSheet({{ fetchJson: async (url, options) => {{
request = {{url, accept:options.headers.Accept}};
return [{{id:3, name:'P0'}}];
}} }});
controller.loadLabels({{repository:'stackchain/api', number:7}}).then(labels =>
process.stdout.write(JSON.stringify({{request, labels}}))
);
"""
result = subprocess.run(
["node", "-e", script], check=True, capture_output=True, text=True
)
assert json.loads(result.stdout) == {
"request": {
"url": "api/v1/repos/stackchain/api/issues/7/labels",
"accept": "application/json",
},
"labels": [{"id": 3, "name": "P0"}],
}
def test_issue_sheet_comment_is_single_flight_and_preserves_draft_until_success():
script = f"""
const createIssueSheet = require({json.dumps(str(ISSUE_SHEET))});
const values = new Map();
const storage = {{
getItem:key => values.get(key) || null,
setItem:(key,value) => values.set(key,value),
removeItem:key => values.delete(key),
}};
let calls = 0;
let release;
const controller = createIssueSheet({{
storage,
fetchJson: (url, options) => {{
calls += 1;
return new Promise(resolve => {{ release = () => resolve({{id:82, body:'Ready'}}); }});
}},
}});
const item = {{repository:'stackchain/api', number:7}};
controller.saveDraft(item, 'Ready');
const first = controller.comment(item, 'Ready');
const duplicate = controller.comment(item, 'Ready');
const during = controller.loadDraft(item);
release();
Promise.all([first, duplicate]).then(results => process.stdout.write(JSON.stringify({{
calls, during, after:controller.loadDraft(item), results
}})));
"""
result = subprocess.run(
["node", "-e", script], check=True, capture_output=True, text=True
)
output = json.loads(result.stdout)
assert output["calls"] == 1
assert output["during"] == "Ready"
assert output["after"] == ""
assert output["results"] == [{"id": 82, "body": "Ready"}, {"id": 82, "body": "Ready"}]
def test_issue_sheet_label_save_is_single_flight_and_keeps_selection_until_confirmed():
script = f"""
const createIssueSheet = require({json.dumps(str(ISSUE_SHEET))});
const values = new Map();
const storage = {{
getItem:key => values.get(key) || null,
setItem:(key,value) => values.set(key,value),
removeItem:key => values.delete(key),
}};
const calls = [];
let release;
const controller = createIssueSheet({{
storage,
fetchJson: (url, options={{}}) => {{
calls.push({{url, method:options.method || 'GET', body:options.body ? JSON.parse(options.body) : null}});
return new Promise(resolve => {{ release = () => resolve({{number:7, labels:['P0']}}); }});
}},
}});
const item = {{repository:'stackchain/api', number:7}};
const first = controller.updateLabels(item, [3]);
const duplicate = controller.updateLabels(item, [3]);
const during = controller.loadLabelDraft(item);
release();
Promise.all([first, duplicate]).then(results => process.stdout.write(JSON.stringify({{
calls, during, after:controller.loadLabelDraft(item), results
}})));
"""
result = subprocess.run(
["node", "-e", script], check=True, capture_output=True, text=True
)
output = json.loads(result.stdout)
assert output["calls"] == [{
"url": "api/v1/repos/stackchain/api/issues/7/labels",
"method": "PATCH",
"body": {"label_ids": [3]},
}]
assert output["during"] == [3]
assert output["after"] == []
assert output["results"] == [
{"number": 7, "labels": ["P0"]},
{"number": 7, "labels": ["P0"]},
]
def test_issue_sheet_close_is_single_flight_and_waits_for_confirmed_closed_state():
script = f"""
const createIssueSheet = require({json.dumps(str(ISSUE_SHEET))});
let calls = 0;
let release;
const controller = createIssueSheet({{
fetchJson: (url, options) => {{
calls += 1;
return new Promise(resolve => {{ release = () => resolve({{number:7, state:'closed'}}); }});
}},
}});
const item = {{repository:'stackchain/api', number:7}};
const first = controller.close(item);
const duplicate = controller.close(item);
release();
Promise.all([first, duplicate]).then(results =>
process.stdout.write(JSON.stringify({{calls, results}}))
);
"""
result = subprocess.run(
["node", "-e", script], check=True, capture_output=True, text=True
)
assert json.loads(result.stdout) == {
"calls": 1,
"results": [
{"number": 7, "state": "closed"},
{"number": 7, "state": "closed"},
],
}
def test_pull_sheet_preserves_comment_draft_and_single_flights_mutations():
script = f"""
const createPullSheet = require({json.dumps(str(PULL_SHEET))});
const values = new Map();
const storage = {{getItem:k => values.get(k) || null, setItem:(k,v) => values.set(k,v), removeItem:k => values.delete(k)}};
const calls = [];
let releaseComment;
const controller = createPullSheet({{
storage,
fetchJson: (url, options={{}}) => {{
calls.push({{url, method:options.method || 'GET', body:options.body ? JSON.parse(options.body) : null}});
if (url.endsWith('/comments')) return new Promise(resolve => {{ releaseComment = () => resolve({{id:91, body:'Ship it'}}); }});
return Promise.resolve({{number:7, merged:true, state:'closed'}});
}},
}});
const item = {{repository:'stackchain/api', number:7}};
controller.saveDraft(item, 'Ship it');
const first = controller.comment(item, 'Ship it');
const duplicate = controller.comment(item, 'Ship it');
const during = controller.loadDraft(item);
releaseComment();
Promise.all([first, duplicate]).then(async comments => {{
const merges = await Promise.all([controller.merge(item, 'abc123'), controller.merge(item, 'abc123')]);
process.stdout.write(JSON.stringify({{calls, during, after:controller.loadDraft(item), comments, merges}}));
}});
"""
result = subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True)
output = json.loads(result.stdout)
assert output["during"] == "Ship it"
assert output["after"] == ""
assert output["calls"] == [
{"url": "api/v1/repos/stackchain/api/pulls/7/comments", "method": "POST", "body": {"body": "Ship it"}},
{"url": "api/v1/repos/stackchain/api/pulls/7/merge", "method": "POST", "body": {"expected_head_sha": "abc123"}},
]
assert len(output["comments"]) == 2 and len(output["merges"]) == 2
def test_pull_sheet_enables_merge_only_for_current_safe_state():
script = f"""
const createPullSheet = require({json.dumps(str(PULL_SHEET))});
const states = [
{{state:'open', draft:false, mergeable:true, merged:false, ci_state:'success', head_sha:'abc'}},
{{state:'open', draft:true, mergeable:true, merged:false, ci_state:'success', head_sha:'abc'}},
{{state:'open', draft:false, mergeable:true, merged:false, ci_state:'failure', head_sha:'abc'}},
{{state:'open', draft:false, mergeable:false, merged:false, ci_state:'success', head_sha:'abc'}},
];
process.stdout.write(JSON.stringify(states.map(createPullSheet.mergeEligibility)));
"""
result = subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True)
output = json.loads(result.stdout)
assert output[0] == {"allowed": True, "reason": "Ready to merge"}
assert output[1]["allowed"] is False and "draft" in output[1]["reason"].lower()
assert output[2]["allowed"] is False and "CI" in output[2]["reason"]
assert output[3]["allowed"] is False and "conflict" in output[3]["reason"].lower()
def test_pull_sheet_persists_head_scoped_file_review_and_gates_merge():
script = f"""
const createPullSheet = require({json.dumps(str(PULL_SHEET))});
const values = new Map();
const storage = {{getItem:k => values.get(k) || null, setItem:(k,v) => values.set(k,v), removeItem:k => values.delete(k)}};
const item = {{repository:'stackchain/api', number:7}};
const detail = {{state:'open', draft:false, mergeable:true, merged:false, ci_state:'success', head_sha:'abc', files:[
{{filename:'src/api.py'}}, {{filename:'frontend/app.js'}}
]}};
const first = createPullSheet({{storage, fetchJson:()=>Promise.resolve()}});
const before = first.reviewState(item, detail);
const afterOne = first.toggleReviewed(item, detail, 'src/api.py');
const restored = createPullSheet({{storage, fetchJson:()=>Promise.resolve()}}).reviewState(item, detail);
const complete = first.toggleReviewed(item, detail, 'frontend/app.js');
const changedHead = first.reviewState(item, {{...detail, head_sha:'def'}});
process.stdout.write(JSON.stringify({{
before, afterOne, restored, complete, changedHead,
blocked:createPullSheet.mergeEligibility(detail, afterOne),
allowed:createPullSheet.mergeEligibility(detail, complete),
next:first.nextUnreviewed(detail, afterOne),
}}));
"""
result = subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True)
output = json.loads(result.stdout)
assert output["before"] == {"reviewed": [], "total": 2, "complete": False}
assert output["afterOne"]["reviewed"] == ["src/api.py"]
assert output["restored"] == output["afterOne"]
assert output["complete"]["complete"] is True
assert output["changedHead"]["reviewed"] == []
assert output["blocked"] == {"allowed": False, "reason": "Review every changed file before merging"}
assert output["allowed"] == {"allowed": True, "reason": "Ready to merge"}
assert output["next"] == "frontend/app.js"
def test_pull_sheet_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="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="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 'openIssueSheet(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-comment-body"' 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="issue"' in html
assert 'data-work-count="pull"' in html
assert 'data-work-count="review"' in html
assert 'data-work-count="update"' in html
assert "['all', 'issue', 'pull', 'review', 'update'].includes(savedFilter)" in html
assert 'sessionStorage.getItem(WORK_FILTER_KEY)' in html
assert 'sessionStorage.setItem(WORK_FILTER_KEY, selectedWorkFilter)' in html
def test_review_controller_loads_encoded_cross_repo_detail_path():
script = f"""
const createReviewController = require({json.dumps(str(REVIEW_SHEET))});
let request;
const controller = createReviewController({{ fetchJson: async (url, options) => {{
request = {{ url, accept: options.headers.Accept }};
return {{ title: 'Review API' }};
}} }});
controller.load({{ repository: 'stackchain/api', number: 7 }}).then(detail =>
process.stdout.write(JSON.stringify({{ request, title: detail.title }}))
);
"""
result = subprocess.run(
["node", "-e", script], check=True, capture_output=True, text=True
)
assert json.loads(result.stdout) == {
"request": {
"url": "api/v1/repos/stackchain/api/pulls/7/review",
"accept": "application/json",
},
"title": "Review API",
}
def test_review_diff_rows_escape_content_and_toggle_accessibly():
script = f"""
const reviewSheet = require({json.dumps(str(REVIEW_SHEET))});
const escapeHtml = value => String(value)
.replaceAll('&', '&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
@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
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": []}