feat: open delegated filings read-only (Closes #872)
CI / lint (pull_request) Successful in 1m53s
CI / build-release (pull_request) Successful in 5s
CI / browser-journey (pull_request) Successful in 57s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy committed 2026-08-15 05:43:28 +00:00
1 parent efafaaddd2
commit 0bb76bdc8b
11 files changed
+238 -18

No files matched your search

+34
View File
@@ -2005,6 +2005,40 @@ async def test_issue_detail_endpoint_returns_assigned_issue_with_no_store(monkey
}
@pytest.mark.anyio
async def test_issue_detail_endpoint_returns_authored_filing_in_read_only_mode(monkeypatch):
calls = []
async def authored(repository, number):
calls.append(("authored", repository, number))
return (repository, number) == ("stackchain/api", 8)
async def detail(repository, number):
calls.append(("detail", repository, number))
return {"repository": repository, "number": number, "title": "Delegated filing"}
monkeypatch.setattr(main.gitea_proxy, "is_authored_issue", authored, raising=False)
monkeypatch.setattr(main.gitea_proxy, "issue_detail", detail)
transport = httpx.ASGITransport(app=main.app)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
response = await client.get(
"/api/v1/repos/stackchain/api/issues/8/detail?access=filed"
)
assert response.status_code == 200
assert response.headers["cache-control"] == "no-store"
assert response.json() == {
"repository": "stackchain/api",
"number": 8,
"title": "Delegated filing",
"read_only": True,
}
assert calls == [
("authored", "stackchain/api", 8),
("detail", "stackchain/api", 8),
]
@pytest.mark.anyio
async def test_assigned_issue_conversation_endpoint_is_authorized_bounded_and_no_store(monkeypatch):
calls = []
+1
View File
@@ -128,6 +128,7 @@ def test_mobile_shell_wires_confirmed_non_my_work_issues_to_the_receipt():
assert 'createIssueFilingReceipt({' in dashboard
assert "filedButton:qs('#issue-filing-receipt-filed')" in dashboard
assert 'onViewFiled:issue =>' in dashboard
assert "is_filed:true, is_assigned:(issue.assignees || []).includes(confirmedOwnerLogin)" in dashboard
assert "confirmed.work_reasons = ['created_by_me']" in dashboard
assert "relatedButton:qs('#issue-filing-receipt-related')" in dashboard
assert 'relatedDraft:issueCapture.buildRelatedDraft(durableDraft)' in dashboard
+63 -1
View File
@@ -1033,6 +1033,8 @@ def test_work_routes_round_trip_all_sheet_kinds_and_reject_unsafe_fragments():
const routes = require({json.dumps(str(WORK_ROUTE))});
const inputs = [
{{kind:'issue',repository:'stackchain/api',number:17}},
{{kind:'filed',repository:'stackchain/api',number:18}},
{{kind:'issue',repository:'stackchain/api',number:19,is_filed:true,is_assigned:false}},
{{kind:'pull',repository:'stackchain/dashboard',number:42}},
{{kind:'review',repository:'stackchain/dashboard',number:42}},
{{kind:'update',notification_id:913}},
@@ -1055,12 +1057,16 @@ process.stdout.write(JSON.stringify({{
assert json.loads(result.stdout) == {
"paths": [
"#/my-work/issue/stackchain/api/17",
"#/my-work/filed/stackchain/api/18",
"#/my-work/filed/stackchain/api/19",
"#/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": "filed", "repository": "stackchain/api", "number": 18},
{"kind": "filed", "repository": "stackchain/api", "number": 19},
{"kind": "pull", "repository": "stackchain/dashboard", "number": 42},
{"kind": "review", "repository": "stackchain/dashboard", "number": 42},
{"kind": "update", "notification_id": 913},
@@ -1158,7 +1164,7 @@ process.stdout.write(JSON.stringify({{calls, hash:location.hash, parsed:routes.p
def test_every_mobile_queue_route_survives_hydration_and_detail_back_navigation():
script = f"""
const routes = require({json.dumps(str(WORK_ROUTE))});
const canonical = ['today', 'agenda', 'attention', 'updates', 'later', 'drafts'];
const canonical = ['today', 'agenda', 'attention', 'filed', 'updates', 'later', 'drafts'];
const parsed = canonical.map(name => [name, routes.parse('#/my-work/' + name)]);
const listeners = {{}};
const location = {{hash:'#/my-work/agenda'}};
@@ -1194,6 +1200,7 @@ process.stdout.write(JSON.stringify({{parsed, calls, hash:location.hash}}));
["today", {"kind": "queue", "filter": "today"}],
["agenda", {"kind": "queue", "filter": "agenda"}],
["attention", {"kind": "queue", "filter": "attention"}],
["filed", {"kind": "queue", "filter": "filed"}],
["updates", {"kind": "queue", "filter": "update"}],
["later", {"kind": "queue", "filter": "later"}],
["drafts", {"kind": "queue", "filter": "draft"}],
@@ -1320,6 +1327,24 @@ const calls = [];
}
@pytest.mark.anyio
async def test_dashboard_opens_delegated_filings_as_read_only_issue_sheets():
html = await dashboard()
assert "else if (item.kind === 'issue' || item.kind === 'filed') openIssueSheet(item, issueTrigger);" in html
assert "const readOnly = issueController.readOnly(item);" in html
assert "qs('#issue-sheet').classList.toggle('read-only', readOnly);" in html
assert "#issue-sheet.read-only .issue-comment-composer" in html
assert "#issue-sheet.read-only #issue-planning" in html
assert "#issue-sheet.read-only #issue-handoff" in html
assert "#issue-sheet.read-only #release-issue" in html
assert "#issue-sheet.read-only #close-issue" in html
assert "#issue-sheet.read-only .detail-defer" in html
assert "if (readOnly) paintIssueConversation(issueConversation.snapshot(), null);" in html
assert "qs('#load-older-issue-comments').hidden = true;" in html
assert "readOnly ? 'Filed issue ready · read-only'" in html
@pytest.mark.anyio
async def test_dashboard_wires_addressable_work_sheets_back_navigation_and_share():
html = await dashboard()
@@ -1549,6 +1574,43 @@ process.stdout.write(JSON.stringify({{updated:updated.issues[0],original:origina
}
def test_issue_sheet_identifies_delegated_filings_as_read_only():
script = f"""
const createIssueSheet = require({json.dumps(str(ISSUE_SHEET))});
const controller = createIssueSheet({{fetchJson:async () => ({{}}), storage:null}});
process.stdout.write(JSON.stringify([
controller.readOnly({{kind:'filed',is_filed:true,is_assigned:false}}),
controller.readOnly({{kind:'issue',is_filed:true,is_assigned:true}}),
controller.readOnly({{kind:'issue',is_assigned:true}}),
]));
"""
result = subprocess.run(
["node", "-e", script], check=True, capture_output=True, text=True
)
assert json.loads(result.stdout) == [True, False, False]
def test_issue_sheet_loads_filed_items_through_read_only_access():
script = f"""
const createIssueSheet = require({json.dumps(str(ISSUE_SHEET))});
const urls = [];
const controller = createIssueSheet({{
fetchJson: async url => {{ urls.push(url); return {{}}; }}, storage:null,
}});
Promise.all([
controller.load({{kind:'issue',repository:'stackchain/api',number:17,is_assigned:true}}),
controller.load({{kind:'filed',repository:'stackchain/api',number:18,is_filed:true,is_assigned:false}}),
]).then(() => process.stdout.write(JSON.stringify(urls)));
"""
result = subprocess.run(
["node", "-e", script], check=True, capture_output=True, text=True
)
assert json.loads(result.stdout) == [
"api/v1/repos/stackchain/api/issues/17/detail",
"api/v1/repos/stackchain/api/issues/18/detail?access=filed",
]
def test_issue_release_is_single_flight_and_requires_confirmed_unassignment():
script = f"""
const createIssueSheet = require({json.dumps(str(ISSUE_SHEET))});
+67
View File
@@ -39,6 +39,33 @@ async def test_work_route_endpoint_resolves_one_authorized_item(monkeypatch):
}
@pytest.mark.anyio
async def test_work_route_endpoint_accepts_filed_issue_identity(monkeypatch):
requested = []
async def resolve(kind, repository, number, notification_id):
requested.append((kind, repository, number, notification_id))
return {
"kind": "filed",
"repository": repository,
"number": number,
"title": "Delegated filing",
"is_filed": True,
"is_assigned": False,
}
monkeypatch.setattr(main.gitea_proxy, "resolve_work_route", resolve)
transport = httpx.ASGITransport(app=main.app)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
response = await client.get(
"/api/v1/work-route?kind=filed&repository=stackchain/dashboard&number=88"
)
assert response.status_code == 200
assert requested == [("filed", "stackchain/dashboard", 88, None)]
assert response.json()["kind"] == "filed"
@pytest.mark.anyio
async def test_resolve_work_route_returns_only_a_requested_review():
requests = []
@@ -80,6 +107,46 @@ async def test_resolve_work_route_returns_only_a_requested_review():
}
@pytest.mark.anyio
async def test_resolve_work_route_returns_an_open_issue_filed_by_the_current_user():
requests = []
async def upstream(request):
requests.append(request.url.path)
if request.url.path.endswith("/user"):
return httpx.Response(200, json={"login": "timmy"})
return httpx.Response(200, json={
"id": 701,
"number": 88,
"title": "Delegated filing",
"state": "open",
"html_url": gitea_proxy.GITEA_URL + "/stackchain/dashboard/issues/88",
"assignees": [{"login": "alex"}],
"user": {"login": "timmy"},
})
gitea_proxy.start_client(transport=httpx.MockTransport(upstream))
try:
result = await gitea_proxy.resolve_work_route(
"filed", "stackchain/dashboard", 88, None
)
finally:
await gitea_proxy.stop_client()
assert len(requests) == 2
assert result == {
"kind": "filed",
"repository": "stackchain/dashboard",
"number": 88,
"title": "Delegated filing",
"state": "open",
"url": gitea_proxy.GITEA_URL + "/stackchain/dashboard/issues/88",
"is_filed": True,
"is_assigned": False,
"work_reasons": ["created_by_me"],
}
@pytest.mark.anyio
async def test_resolve_work_route_rejects_a_notification_that_is_already_read():
async def upstream(request):