feat: release mobile issue assignments (#183)
CI / lint (pull_request) Successful in 14s
CI / build-frontend (pull_request) Successful in 5s

This commit is contained in:
timmy committed 2026-08-07 09:24:21 +00:00
1 parent e96ff42dff
commit 87058d72e6
8 files changed
+264 -1

No files matched your search

+62
View File
@@ -460,6 +460,68 @@ async def test_claim_available_issue_endpoint_reports_assignment_race_as_conflic
}
@pytest.mark.anyio
async def test_gitea_release_issue_removes_only_authenticated_user_and_confirms_peers():
requests = []
async def handler(request):
requests.append(request)
if request.method == "GET" and request.url.path.endswith("/issues/17"):
return httpx.Response(200, json={
"id": 81, "number": 17, "title": "Shared work", "state": "open",
"pull_request": None,
"assignees": [{"login": "timmy"}, {"login": "alex"}],
"labels": [],
})
if request.method == "GET" and request.url.path == "/api/v1/user":
return httpx.Response(200, json={"login": "timmy"})
return httpx.Response(200, json={
"id": 81, "number": 17, "title": "Shared work", "state": "open",
"assignees": [{"login": "alex"}], "labels": [],
})
gitea_proxy.start_client(transport=httpx.MockTransport(handler))
try:
result = await gitea_proxy.release_assigned_issue("stackchain/api", 17)
finally:
await gitea_proxy.stop_client()
assert [(request.method, request.url.path) for request in requests] == [
("GET", "/api/v1/user"),
("GET", "/api/v1/repos/stackchain/api/issues/17"),
("PATCH", "/api/v1/repos/stackchain/api/issues/17"),
]
assert requests[2].content == b'{"assignees":["alex"]}'
assert result["assignees"] == ["alex"]
assert result["available"] is False
@pytest.mark.anyio
async def test_release_issue_endpoint_invalidates_available_work_and_returns_confirmation(monkeypatch):
calls = []
async def release(repository, number):
calls.append((repository, number))
return {
"number": number, "repository": repository, "state": "open",
"assignees": [], "available": True,
}
monkeypatch.setattr(main.gitea_proxy, "release_assigned_issue", release, raising=False)
monkeypatch.setattr(main, "_available_issue_snapshot_value", [{"number": 99}])
monkeypatch.setattr(main, "_available_issue_snapshot_created_at", 123.0)
transport = httpx.ASGITransport(app=main.app)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
response = await client.patch("/api/v1/repos/stackchain/api/issues/17/release")
assert response.status_code == 200
assert response.headers["cache-control"] == "no-store"
assert response.json()["available"] is True
assert calls == [("stackchain/api", 17)]
assert main._available_issue_snapshot_value is None
assert main._available_issue_snapshot_created_at is None
@pytest.mark.anyio
async def test_issue_detail_endpoint_returns_assigned_issue_with_no_store(monkeypatch):
async def assigned(repository, number):
+79
View File
@@ -110,6 +110,51 @@ process.stdout.write(JSON.stringify({{
}
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_my_work_reviews_filter_and_summary_are_actionable():
items = [
{"title": "Issue", "kind": "issue", "is_review": False, "is_assigned": True},
@@ -1221,6 +1266,40 @@ async def test_assigned_issues_open_accessible_mobile_action_sheet_with_safe_mut
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()