feat: defer Web Push updates until tomorrow (Closes #563)
This commit is contained in:
1 parent
f4a4ed84c3
commit
7ce352553d
5 files changed
+213
-5
No files matched your search
@@ -0,0 +1,70 @@
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
from src import main
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_push_tomorrow_resolves_unread_update_and_persists_canonical_later_item(
|
||||
monkeypatch,
|
||||
):
|
||||
resolved = []
|
||||
applied = []
|
||||
|
||||
async def confirmed_login():
|
||||
return "timmy"
|
||||
|
||||
async def resolve(kind, repository, number, notification_id):
|
||||
resolved.append((kind, repository, number, notification_id))
|
||||
return {
|
||||
"kind": "update",
|
||||
"notification_id": 42,
|
||||
"repository": "stackchain/api",
|
||||
"title": "Retry failed deploy",
|
||||
}
|
||||
|
||||
class Store:
|
||||
def apply(
|
||||
self,
|
||||
login,
|
||||
operation_id,
|
||||
action,
|
||||
item_id,
|
||||
*,
|
||||
wake_at=None,
|
||||
base_revision=None,
|
||||
):
|
||||
applied.append(
|
||||
(login, operation_id, action, item_id, wake_at, base_revision)
|
||||
)
|
||||
return {"revision": 3, "records": {item_id: wake_at}}
|
||||
|
||||
monkeypatch.setattr(main, "_confirmed_login", confirmed_login)
|
||||
monkeypatch.setattr(main.gitea_proxy, "resolve_work_route", resolve)
|
||||
monkeypatch.setattr(main, "_later_store", lambda: Store())
|
||||
|
||||
transport = httpx.ASGITransport(app=main.app)
|
||||
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
|
||||
response = await client.patch(
|
||||
"/api/v1/notifications/42/later",
|
||||
json={"wake_at": "2026-08-12T09:00:00.000Z"},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.headers["cache-control"] == "no-store"
|
||||
assert response.json() == {
|
||||
"id": 42,
|
||||
"status": "deferred",
|
||||
"item_id": "update:stackchain/api::42",
|
||||
"wake_at": "2026-08-12T09:00:00.000Z",
|
||||
"revision": 3,
|
||||
}
|
||||
assert resolved == [("update", None, None, 42)]
|
||||
assert applied == [(
|
||||
"timmy",
|
||||
"push-tomorrow:42:2026-08-12T09:00:00.000Z",
|
||||
"defer",
|
||||
"update:stackchain/api::42",
|
||||
"2026-08-12T09:00:00.000Z",
|
||||
None,
|
||||
)]
|
||||
@@ -105,11 +105,11 @@ async function dispatchMessage(data, ports = []) {{
|
||||
listeners.message({{ data, ports, waitUntil: promise => {{ pending = promise; }} }});
|
||||
if (pending) await pending;
|
||||
}}
|
||||
async function dispatchNotificationClick(route, action = '', notificationId = null) {{
|
||||
async function dispatchNotificationClick(route, action = '', notificationId = null, tag = null) {{
|
||||
let pending;
|
||||
listeners.notificationclick({{
|
||||
action,
|
||||
notification: {{data: {{route, notificationId}}, close: () => {{ state.notificationClosed = true; }}}},
|
||||
notification: {{tag: tag || ('stackchain-update-' + notificationId), data: {{route, notificationId}}, close: () => {{ state.notificationClosed = true; }}}},
|
||||
waitUntil: promise => {{ pending = promise; }},
|
||||
}});
|
||||
if (pending) await pending;
|
||||
@@ -401,7 +401,10 @@ def test_inbound_push_renders_generic_update_and_tap_opens_existing_workflow():
|
||||
"options": {
|
||||
"body": "Tap to review it in Stackchain.",
|
||||
"tag": "stackchain-update-42",
|
||||
"actions": [{"action": "mark-read", "title": "Mark read"}],
|
||||
"actions": [
|
||||
{"action": "mark-read", "title": "Mark read"},
|
||||
{"action": "tomorrow", "title": "Tomorrow"},
|
||||
],
|
||||
"data": {
|
||||
"route": "#/my-work/update/42",
|
||||
"notificationId": 42,
|
||||
@@ -453,6 +456,51 @@ def test_push_mark_read_action_confirms_authenticated_mutation_without_opening_a
|
||||
assert result["state"]["focused"] == []
|
||||
|
||||
|
||||
def test_push_tomorrow_action_defers_privately_without_opening_or_marking_read():
|
||||
result = run_worker_scenario(
|
||||
"""
|
||||
const calls = [];
|
||||
context.self.__STACKCHAIN_NOW = () => new Date('2026-08-11T18:30:00.000Z');
|
||||
context.fetch = async (request, options = {}) => {
|
||||
const url = String(request.url || request);
|
||||
const headers = new Headers(options.headers || {});
|
||||
calls.push({
|
||||
url, method:String(options.method || 'GET'), csrf:headers.get('X-CSRF-Token'),
|
||||
body:options.body ? JSON.parse(options.body) : null,
|
||||
});
|
||||
if (url.endsWith('/api/v1/session')) {
|
||||
return new Response(JSON.stringify({csrf_token:'session-proof'}), {
|
||||
status:200, headers:{'Content-Type':'application/json'},
|
||||
});
|
||||
}
|
||||
return new Response(JSON.stringify({id:42,status:'deferred'}), {
|
||||
status:200, headers:{'Content-Type':'application/json'},
|
||||
});
|
||||
};
|
||||
await dispatchNotificationClick('#/my-work/update/42', 'tomorrow', 42, 'stackchain-update-42');
|
||||
process.stdout.write(JSON.stringify({state,calls}));
|
||||
"""
|
||||
)
|
||||
|
||||
assert result["calls"] == [
|
||||
{
|
||||
"url": "https://forge.example/dashboard/api/v1/session",
|
||||
"method": "GET",
|
||||
"csrf": None,
|
||||
"body": None,
|
||||
},
|
||||
{
|
||||
"url": "https://forge.example/dashboard/api/v1/notifications/42/later",
|
||||
"method": "PATCH",
|
||||
"csrf": "session-proof",
|
||||
"body": {"wake_at": "2026-08-12T09:00:00.000Z"},
|
||||
},
|
||||
]
|
||||
assert result["state"]["notificationClosed"] is True
|
||||
assert result["state"]["opened"] == []
|
||||
assert all(not call["url"].endswith("/read") for call in result["calls"])
|
||||
|
||||
|
||||
def test_push_mark_read_failure_opens_existing_update_reader():
|
||||
result = run_worker_scenario(
|
||||
"""
|
||||
|
||||
Reference in new issue
Block a user