import json import subprocess from pathlib import Path import pytest from tests.dashboard_bundle import dashboard ROOT = Path(__file__).resolve().parents[1] OUTBOX = ROOT / "frontend" / "notification-read-outbox.js" BACKGROUND_SYNC = ROOT / "frontend" / "background-issue-sync.js" WORKER = ROOT / "frontend" / "service-worker.js" def run_node(script): result = subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True) return json.loads(result.stdout) def test_offline_read_admission_is_account_bound_deduplicated_and_suppresses_saved_updates(): script = f""" const createOutbox = require({json.dumps(str(OUTBOX))}); const values = new Map(); const storage = {{ getItem:key => values.has(key) ? values.get(key) : null, setItem:(key,value) => values.set(key,value), }}; const reconciled = []; const backgroundSync = {{ reconcile: async (items, lane) => reconciled.push([lane, items]), requestSync: async () => true, }}; const outbox = createOutbox({{ storage, backgroundSync, getOwnerLogin:() => 'timmy', now:() => 1234, }}); (async () => {{ const first = await outbox.enqueueDurably(42); const duplicate = await outbox.enqueueDurably(42); const visible = outbox.suppress([ {{notification_id:42, title:'Queued'}}, {{notification_id:43, title:'Visible'}}, ]); process.stdout.write(JSON.stringify({{ first, duplicate, items:outbox.list(), visible, reconciled, }})); }})(); """ output = run_node(script) assert output["first"]["item"]["notificationId"] == 42 assert output["duplicate"]["item"]["id"] == output["first"]["item"]["id"] assert len(output["items"]) == 1 assert output["items"][0]["ownerLogin"] == "timmy" assert [item["notification_id"] for item in output["visible"]] == [43] assert output["reconciled"][-1][0] == "notification-read" assert len(output["reconciled"][-1][1]) == 1 def test_foreground_flush_patches_each_unique_read_and_keeps_transient_failures_queued(): script = f""" const createOutbox = require({json.dumps(str(OUTBOX))}); const values = new Map(); const storage = {{getItem:key => values.get(key) || null,setItem:(key,value) => values.set(key,value)}}; const calls = []; const outbox = createOutbox({{ storage, getOwnerLogin:() => 'timmy', fetchJson: async (url, options) => {{ calls.push([url, options.method]); if (url.includes('/43/')) {{ const error = new Error('offline'); error.status = 503; throw error; }} return {{ok:true}}; }}, }}); (async () => {{ await outbox.enqueueDurably(42); await outbox.enqueueDurably(43); const result = await outbox.flush('timmy'); process.stdout.write(JSON.stringify({{calls, result, items:outbox.list()}})); }})(); """ output = run_node(script) assert output["calls"] == [ ["api/v1/notifications/42/read", "PATCH"], ["api/v1/notifications/43/read", "PATCH"], ] assert output["result"]["confirmed"] == [42] assert [item["notificationId"] for item in output["items"]] == [43] assert output["items"][0]["status"] == "queued" def test_background_delivery_uses_patch_and_reports_read_receipt(): script = f""" const createSync = require({json.dumps(str(BACKGROUND_SYNC))}); const requests = []; const store = {{ upsert:async () => {{}}, claim:async () => ({{ id:'notification-read:timmy:42', kind:'notification-read', notificationId:42, ownerLogin:'timmy', status:'sending', }}), complete:async () => {{}}, release:async () => {{}}, fail:async () => {{}}, }}; const sync = createSync({{ store, fetchJson:async (url, options) => {{ requests.push([url, options.method, options.body || null]); return {{ok:true}}; }}, }}); (async () => {{ const result = await sync.send({{ id:'notification-read:timmy:42', kind:'notification-read', notificationId:42, ownerLogin:'timmy', status:'queued', }}, 'timmy'); process.stdout.write(JSON.stringify({{requests, result}})); }})(); """ output = run_node(script) assert output["requests"] == [["api/v1/notifications/42/read", "PATCH", None]] assert output["result"]["receipt"] == { "id": "notification-read:timmy:42", "status": "confirmed", "kind": "notification-read", "route": "#/my-work/updates", } @pytest.mark.anyio async def test_dashboard_wires_offline_queue_read_next_through_precached_background_outbox(): html = await dashboard() worker = WORKER.read_text() assert '' in html assert "const notificationReadOutbox = createNotificationReadOutbox({" in html assert "queueRead: notificationId => notificationReadOutbox.enqueueDurably(notificationId)" in html assert "notificationReadOutbox.suppress(saved.notifications || [])" in html assert "notificationReadOutbox.flush(activeFlushLogin)" in html assert "Queue read & next" in html assert "BASE + 'static/notification-read-outbox.js'" in worker