feat: notify on background delivery completion (#260)
CI / lint (pull_request) Successful in 28s
CI / build-frontend (pull_request) Successful in 5s

This commit is contained in:
timmy committed 2026-08-08 04:18:32 +00:00
1 parent a7afd85634
commit 1b10a5a8a8
6 files changed
+310 -15

No files matched your search

+107
View File
@@ -64,6 +64,59 @@ const fetchJson = async (url, options = {{}}) => {{
}
def test_closed_app_sync_returns_privacy_safe_actionable_delivery_receipts():
script = f"""
const createBackgroundIssueSync = require({json.dumps(str(SYNC))});
const queued = [
{{id:'capture-1',operationId:'capture-1',ownerLogin:'timmy',status:'queued',repository:'stackchain/api',title:'Secret title',body:'Secret body',labelIds:[]}},
{{id:'reply-1',operationId:'reply-1',ownerLogin:'timmy',status:'queued',kind:'pull-comment',repository:'stackchain/web',number:8,body:'Secret reply'}},
{{id:'bad-1',operationId:'bad-1',ownerLogin:'timmy',status:'queued',repository:'stackchain/api',title:'Bad',body:'Secret failure',labelIds:[]}},
];
const store = {{
claimNext: async () => queued.shift() || null,
complete: async () => {{}}, release: async () => {{}}, fail: async () => {{}},
countBlocked: async () => 0,
}};
let issueMutations = 0;
const fetchJson = async (url) => {{
if (url === 'api/v1/background-identity') return {{login:'timmy'}};
if (url.endsWith('/issues') && issueMutations++ === 0) return {{number:44,repository:'stackchain/api',title:'Secret title'}};
if (url.includes('/pulls/8/comments')) return {{id:91,body:'Secret reply'}};
const error = new Error('Sensitive validation detail'); error.status = 422; throw error;
}};
(async () => {{
const result = await createBackgroundIssueSync({{store,fetchJson}}).flush();
process.stdout.write(JSON.stringify(result));
}})();
"""
output = run_node(script)
assert output["login"] == "timmy"
assert output["receipts"] == [
{
"id": "capture-1",
"status": "confirmed",
"kind": "issue",
"route": "#/my-work/issue/stackchain/api/44",
},
{
"id": "reply-1",
"status": "confirmed",
"kind": "message",
"route": "#/my-work/pull/stackchain/web/8",
},
{
"id": "bad-1",
"status": "attention",
"kind": "issue",
"route": "#/my-work/drafts",
},
]
serialized = json.dumps(output["receipts"])
assert "Secret" not in serialized
assert "Sensitive" not in serialized
@pytest.mark.parametrize(
("item", "expected_url"),
[
@@ -141,6 +194,47 @@ const transaction=work=>{{const run=tail.then(()=>work({{
]
def test_delivery_receipt_preference_is_account_bound_and_hidden_from_outbox():
script = f"""
const createBackgroundIssueSync = require({json.dumps(str(SYNC))});
const records=new Map();let tail=Promise.resolve();
const transaction=work=>{{const run=tail.then(()=>work({{
getAll:async()=>[...records.values()].map(value=>({{...value}})),
put:async value=>records.set(value.id,{{...value}}),delete:async id=>records.delete(id),
}}));tail=run.catch(()=>{{}});return run;}};
(async()=>{{
const store=createBackgroundIssueSync.createIssueSyncStore({{transaction}});
await store.setReceiptPreference('timmy', true);
await store.reconcile([{{id:'issue',ownerLogin:'timmy',status:'queued'}}], 'issue');
const result={{
timmy:await store.getReceiptPreference('timmy'),
alexander:await store.getReceiptPreference('alexander'),
snapshot:await store.snapshot(),
blocked:await store.countBlocked('alexander'),
}};
await store.setReceiptPreference('timmy', false);
result.disabled=await store.getReceiptPreference('timmy');
process.stdout.write(JSON.stringify(result));
}})();
"""
output = run_node(script)
assert output == {
"timmy": True,
"alexander": False,
"snapshot": [
{
"id": "issue",
"ownerLogin": "timmy",
"status": "queued",
"outboxLane": "issue",
}
],
"blocked": 1,
"disabled": False,
}
def test_closed_app_sync_leaves_another_accounts_issue_queued():
script = f"""
const createBackgroundIssueSync = require({json.dumps(str(SYNC))});
@@ -392,3 +486,16 @@ async def test_dashboard_wires_indexeddb_outbox_and_background_sync_fallback():
assert "issueOutbox.reconcileBackground(records);" in html
assert "authoredOutbox.reconcileBackground(records);" in html
assert "if ('indexedDB' in window)" in html
@pytest.mark.anyio
async def test_dashboard_offers_explicit_account_bound_delivery_receipt_opt_in():
html = await dashboard()
assert 'id="delivery-receipts" type="checkbox"' in html
assert "deliveryReceipts.addEventListener('change', async () => {" in html
assert "await Notification.requestPermission()" in html
assert "backgroundIssueSync.setReceiptPreference(confirmedOwnerLogin, enabled)" in html
assert "await backgroundIssueSync.getReceiptPreference(confirmedOwnerLogin)" in html
assert "window.addEventListener('hashchange', openDeliveryReceiptRoute);" in html
assert "if (window.location.hash !== '#/my-work/drafts') return;" in html
+61 -5
View File
@@ -13,7 +13,7 @@ def run_worker_scenario(scenario: str) -> dict:
const fs = require('fs');
const vm = require('vm');
const listeners = {{}};
const state = {{ added: [], deleted: [], claimed: false, skipped: false, fetches: [], puts: [], backgroundFlushes: 0, failFetch: false, fetchStatus: 200, cachedBody: null }};
const state = {{ added: [], deleted: [], claimed: false, skipped: false, fetches: [], puts: [], backgroundFlushes: 0, notifications: [], focused: [], opened: [], failFetch: false, fetchStatus: 200, cachedBody: null }};
const cache = {{
addAll: async urls => {{ state.added = urls; }},
match: async request => state.cachedBody === null ? null : new Response(state.cachedBody),
@@ -26,8 +26,16 @@ const context = {{
location: {{ href: 'https://forge.example/dashboard/service-worker.js', origin: 'https://forge.example' }},
addEventListener: (name, handler) => {{ listeners[name] = handler; }},
skipWaiting: async () => {{ state.skipped = true; }},
clients: {{ claim: async () => {{ state.claimed = true; }} }},
__issueSync: {{ flush: async () => {{ state.backgroundFlushes += 1; }} }},
clients: {{
claim: async () => {{ state.claimed = true; }},
matchAll: async () => state.clientList || [],
openWindow: async url => {{ state.opened.push(url); }},
}},
registration: {{showNotification: async (title, options) => state.notifications.push({{title,options}})}},
__issueSync: {{
flush: async () => {{ state.backgroundFlushes += 1; return state.flushResult; }},
getReceiptPreference: async login => state.receiptLogin === login,
}},
}},
importScripts: () => {{}},
caches: {{
@@ -60,6 +68,14 @@ async function dispatchSync(tag) {{
listeners.sync({{ tag, waitUntil: promise => {{ pending = promise; }} }});
if (pending) await pending;
}}
async function dispatchNotificationClick(route) {{
let pending;
listeners.notificationclick({{
notification: {{data: {{route}}, close: () => {{ state.notificationClosed = true; }}}},
waitUntil: promise => {{ pending = promise; }},
}});
if (pending) await pending;
}}
(async () => {{
{scenario}
}})().catch(error => {{ console.error(error); process.exit(1); }});
@@ -70,10 +86,10 @@ async function dispatchSync(tag) {{
return json.loads(completed.stdout)
def test_operator_session_boundary_ships_in_a_new_shell_cache():
def test_background_delivery_receipts_ship_in_a_new_shell_cache():
source = WORKER.read_text()
assert "stackchain-dashboard-shell-v16" in source
assert "stackchain-dashboard-shell-v17" in source
def test_background_sync_event_flushes_closed_app_issue_outbox_only_for_its_tag():
@@ -88,6 +104,46 @@ def test_background_sync_event_flushes_closed_app_issue_outbox_only_for_its_tag(
assert result["backgroundFlushes"] == 1
def test_opted_in_background_sync_notifies_privately_and_receipt_tap_focuses_route():
result = run_worker_scenario(
"""
state.receiptLogin = 'timmy';
state.flushResult = {login:'timmy', receipts:[
{id:'capture-1',status:'confirmed',kind:'issue',route:'#/my-work/issue/stackchain/api/44'},
{id:'bad-1',status:'attention',kind:'message',route:'#/my-work/drafts'},
]};
state.clientList = [{url:'https://forge.example/dashboard/', navigate:async function(url){ this.url=url; }, focus:async function(){ state.focused.push(this.url); }}];
await dispatchSync('stackchain-issue-outbox-v1');
await dispatchNotificationClick('#/my-work/issue/stackchain/api/44');
process.stdout.write(JSON.stringify(state));
"""
)
assert result["notifications"] == [
{
"title": "Queued issue created",
"options": {
"body": "Tap to open it in Stackchain.",
"tag": "stackchain-delivery-capture-1",
"data": {"route": "#/my-work/issue/stackchain/api/44"},
},
},
{
"title": "Queued work needs attention",
"options": {
"body": "Tap to review it in Drafts.",
"tag": "stackchain-delivery-bad-1",
"data": {"route": "#/my-work/drafts"},
},
},
]
assert result["focused"] == [
"https://forge.example/dashboard/#/my-work/issue/stackchain/api/44"
]
assert result["opened"] == []
assert result["notificationClosed"] is True
def test_background_mutations_obtain_session_bound_csrf_proof():
source = WORKER.read_text()