feat: mute future updates during triage (Closes #751)
CI / lint (pull_request) Successful in 1m33s
CI / build-release (pull_request) Successful in 6s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy committed 2026-08-13 15:55:02 +00:00
1 parent b93e63ab70
commit 8dc3880167
8 files changed
+190 -3

No files matched your search

+38
View File
@@ -254,6 +254,8 @@ async def test_notification_detail_loads_subject_and_latest_comment_for_inbox_re
"number": 7, "state": "open", "assignees": [],
"body": "Deploy fails after **three** retries.",
})
if request.url.path.endswith("/issues/7/subscriptions/check"):
return httpx.Response(200, json={"subscribed": True, "ignored": False})
if request.url.path.endswith("/issues/comments/9"):
return httpx.Response(200, json={
"id": 9,
@@ -283,6 +285,7 @@ async def test_notification_detail_loads_subject_and_latest_comment_for_inbox_re
"http://127.0.0.1:3000/api/v1/repos/stackchain/api/issues/7",
"http://127.0.0.1:3000/api/v1/repos/stackchain/api/issues/comments/9",
"http://127.0.0.1:3000/api/v1/repos/stackchain/api/issues/7/comments?limit=20&page=1",
"http://127.0.0.1:3000/api/v1/repos/stackchain/api/issues/7/subscriptions/check",
]
assert result == {
"id": 42,
@@ -300,6 +303,7 @@ async def test_notification_detail_loads_subject_and_latest_comment_for_inbox_re
},
"issue": {"number": 7, "assignees": [], "claimable": True},
"acknowledge_supported": True,
"mute_supported": True,
"conversation": {
"comments": [{
"id": 9,
@@ -398,3 +402,37 @@ async def test_reply_to_notification_posts_to_its_issue_conversation(subject_kin
"created_at": "2026-08-07T19:00:00Z",
"url": "https://forge.example/stackchain/api/issues/7#issuecomment-91",
}
@pytest.mark.anyio
async def test_mute_notification_unsubscribes_current_user_from_trusted_thread_target():
requests = []
def upstream(request):
requests.append((request.method, request.url.path))
if request.url.path.endswith("/notifications/threads/42"):
return httpx.Response(200, json={
"repository": {"full_name": "stackchain/api"},
"subject": {
"type": "Pull",
"url": "http://127.0.0.1:3000/api/v1/repos/stackchain/api/pulls/7",
},
})
if request.url.path.endswith("/user"):
return httpx.Response(200, json={"login": "timmy"})
if request.method == "DELETE":
return httpx.Response(204)
return httpx.Response(404)
gitea_proxy.start_client(transport=httpx.MockTransport(upstream))
try:
result = await gitea_proxy.mute_notification(42)
finally:
await gitea_proxy.stop_client()
assert result == {"id": 42, "repository": "stackchain/api", "number": 7, "muted": True}
assert requests == [
("GET", "/api/v1/notifications/threads/42"),
("GET", "/api/v1/user"),
("DELETE", "/api/v1/repos/stackchain/api/issues/7/subscriptions/timmy"),
]
+15
View File
@@ -4958,6 +4958,21 @@ async def test_mobile_update_sheet_wires_touch_safe_online_acknowledge_and_next(
assert '.update-sheet-actions #acknowledge-update-next { min-height:44px;' in html
@pytest.mark.anyio
async def test_mobile_update_sheet_mutes_future_updates_and_advances_only_on_read_success():
html = await dashboard()
assert 'id="mute-update-next" type="button" hidden' in html
assert ">Mute future updates &amp; next</button>" in html
assert "qs('#mute-update-next').hidden = !detail.mute_supported;" in html
assert "async function muteNotification(notificationId)" in html
assert "'/mute', { method: 'POST'" in html
assert "notificationReader.acceptReadAndNext(lastMyWork, item)" in html
assert "Future updates are muted; current item is still unread" in html
assert "qs('#mute-update-next').disabled = offline;" in html
assert ".update-sheet-actions #mute-update-next" in html
def test_notification_reader_keeps_current_update_retryable_when_detail_load_fails():
script = f"""
const buildMyWork = require({json.dumps(str(MY_WORK))});
+28
View File
@@ -215,6 +215,34 @@ async def test_mark_notification_unread_api_is_bounded_and_never_cacheable(monke
assert restored == [42]
@pytest.mark.anyio
async def test_mute_notification_then_marks_read_and_reports_partial_success(monkeypatch):
calls = []
async def mute(thread_id):
calls.append(("mute", thread_id))
return {"id": thread_id, "muted": True}
async def fail_read(thread_id):
calls.append(("read", thread_id))
raise httpx.HTTPError("unavailable")
monkeypatch.setattr(main.gitea_proxy, "mute_notification", mute, raising=False)
monkeypatch.setattr(main, "mark_notification_read", fail_read)
transport = httpx.ASGITransport(app=main.app)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
response = await client.post("/api/v1/notifications/42/mute")
assert response.status_code == 409
assert response.json() == {
"id": 42,
"muted": True,
"status": "unread",
"error": "Future updates are muted; current item is still unread. Retry mark read & next.",
}
assert calls == [("mute", 42), ("read", 42)]
@pytest.mark.anyio
async def test_acknowledge_notification_api_confirms_reaction_and_removes_snapshot_item(monkeypatch):
calls = []