// WebKit drops a push subscription after three pushes that show no // notification, so on an Apple endpoint every path through the worker's push // handler must end in one. Run: bun test tools/sw-push-quota.test.js import { test, expect, mock } from 'bun:test'; let respond = async () => true; mock.module('../src/nwc-respond.js', () => ({ respondFromBg: (...a) => respond(...a), tendRenewals: async () => {}, tendLnPays: async () => [] })); mock.module('../src/nwc-lock.js', () => ({ withRequestLock: async (_id, fn) => fn(), withWorkLock: async (fn) => fn() })); mock.module('../src/nwc-bg.js', () => ({ appendBgLog: async () => {}, takeLease: async () => ({ waited: 0, release: async () => {} }) })); mock.module('../src/bg-autowithdraw.js', () => ({ bgAutoWithdraw: async () => {} })); mock.module('../src/dm-inbox.js', () => ({ allInboxes: async () => [{ muted: ['m'.repeat(64)] }], classifyDm: async () => ({ own: true }), shouldNotifyDm: () => false, classifyCall: async () => null, })); const listeners = {}; let endpoint = 'https://web.push.apple.com/abc'; let windows = []; const shown = [], closed = []; globalThis.self = { location: { hostname: 'v3.coinos.io', origin: 'https://v3.coinos.io' }, addEventListener: (t, f) => { listeners[t] = f; }, clients: { matchAll: async () => windows }, registration: { pushManager: { getSubscription: async () => ({ endpoint }) }, showNotification: async (title, o) => { shown.push({ title, ...o }); }, getNotifications: async ({ tag }) => shown.filter((n) => n.tag === tag).map((n) => ({ close: () => closed.push(n) })), }, }; await import('../src/sw-nwc.js'); async function push(data) { shown.length = 0; closed.length = 0; let done; listeners.push({ data: { json: () => data }, waitUntil: (p) => { done = p; } }); await done; return shown.map((n) => n.title); } const nwc = { type: 'nwc', servicePubkey: 'a'.repeat(64), event: { id: 'e'.repeat(64), kind: 23194 } }; const hidden = { visibilityState: 'hidden', postMessage: () => {} }; const visible = { visibilityState: 'visible', postMessage: () => {} }; test('apple: every quiet path shows the stand-in', async () => { endpoint = 'https://web.push.apple.com/abc'; windows = []; respond = async () => true; // a lookup answered, or another device took it expect(await push(nwc)).toEqual(['Coinos']); expect(await push({ type: 'notify', reason: 'dm', wrap: {} })).toEqual(['Coinos']); expect(await push({ type: 'notify', reason: 'reaction', from: 'm'.repeat(64) })).toEqual(['Coinos']); expect(await push({ type: 'notify', reason: 'call', wrapId: 'x' })).toEqual(['Coinos']); expect(await push({ type: 'weird' })).toEqual(['Coinos']); expect(closed.length).toBe(0); }); test('apple: a send finished from an earlier wake shows its own line and drops the stand-in', async () => { endpoint = 'https://web.push.apple.com/abc'; windows = []; respond = async () => ({ notices: [{ title: 'Payment sent', body: '-21 sats via a connected app', tag: 'nwc-sent' }] }); expect(await push(nwc)).toEqual(['Coinos', 'Payment sent']); expect(closed.length).toBe(1); }); test('apple: an app on screen gets the stand-in closed at once', async () => { windows = [visible]; expect(await push({ type: 'notify', reason: 'payment', amountSat: 5 })).toEqual(['Coinos']); expect(closed.length).toBe(1); }); test('apple: a suspended background app does not swallow the request', async () => { windows = [hidden]; let asked = 0; respond = async () => { asked++; return { notify: { title: 'Payment sent', body: '' } }; }; // the stand-in goes up first and the outcome replaces it (91dbee4) expect(await push(nwc)).toEqual(['Coinos', 'Payment sent']); expect(closed.length).toBe(1); expect(asked).toBe(1); }); test('elsewhere: quiet stays quiet', async () => { endpoint = 'https://fcm.googleapis.com/fcm/send/abc'; windows = []; respond = async () => true; expect(await push(nwc)).toEqual([]); windows = [hidden]; let asked = 0; respond = async () => { asked++; return true; }; expect(await push(nwc)).toEqual([]); expect(asked).toBe(0); // on a computer the open (if hidden) window answers windows = []; respond = async () => false; expect(await push(nwc)).toEqual(['Spend request']); }); // An Android phone freezes an app behind another one, minutes in: its relay // sockets are dead and the worker's nudge sits unread until it is opened, so // a zap sent from another app spun until then (wake to payment: 15 s at the // median on Android, 2026-11-10). On a phone only a window on screen is // handed the request; behind another app, the worker answers (and nudges the // window anyway — the request's lock decides if it is still awake). test('an Android phone: the app behind another one does not swallow the request', async () => { endpoint = 'https://fcm.googleapis.com/fcm/send/abc'; self.navigator = { userAgent: 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/155.0.0.0 Mobile Safari/537.36' }; try { let asked = 0, nudged = 0; respond = async () => { asked++; return true; }; windows = [{ visibilityState: 'hidden', postMessage: () => { nudged++; } }]; expect(await push(nwc)).toEqual([]); expect(asked).toBe(1); expect(nudged).toBe(1); // on screen, it answers itself with everything it knows asked = 0; nudged = 0; windows = [{ visibilityState: 'visible', postMessage: () => { nudged++; } }]; expect(await push(nwc)).toEqual([]); expect(asked).toBe(0); expect(nudged).toBe(1); } finally { delete self.navigator; } });