import test from 'node:test'; import assert from 'node:assert/strict'; import { readFile } from 'node:fs/promises'; import vm from 'node:vm'; const source = await readFile(new URL('../service-worker.js', import.meta.url), 'utf8'); function loadWorker({ scope = 'https://example.test/', cacheKeys = [], currentHits = new Map(), fetchImpl = async () => { throw new Error('offline'); } } = {}) { const listeners = new Map(); const deleted = []; const puts = []; let cacheOpenCount = 0; const currentCache = { addAll: async () => {}, put: async request => { puts.push(typeof request === 'string' ? request : request.url); }, match: async request => currentHits.get(typeof request === 'string' ? request : request.url), }; const caches = { keys: async () => cacheKeys, delete: async key => { deleted.push(key); return true; }, open: async () => { cacheOpenCount += 1; return currentCache; }, match: async () => { throw new Error('global cache matching must not be used'); }, }; const self = { registration: { scope }, addEventListener: (type, handler) => listeners.set(type, handler), skipWaiting: async () => {}, clients: { claim: async () => {} }, }; vm.runInNewContext(source, { self, caches, fetch: fetchImpl, URL, Promise, String }); return { listeners, deleted, puts, getCacheOpenCount: () => cacheOpenCount }; } async function dispatchExtendable(handler) { let pending; handler({ waitUntil(value) { pending = value; } }); await pending; } async function dispatchFetch(handler, request) { let response; handler({ request, respondWith(value) { response = value; } }); return response; } test('root activation deletes only its obsolete Timmy caches including the legacy v4 cache', async () => { const { listeners, deleted } = loadWorker({ cacheKeys: [ 'timmy-shell-v4', 'timmy-shell:/:v4', 'timmy-shell:/:v5', 'timmy-shell:/timmy-staging/:v4', 'sibling-shared-origin-cache', ], }); await dispatchExtendable(listeners.get('activate')); assert.deepEqual(deleted.sort(), ['timmy-shell-v4', 'timmy-shell:/:v4']); }); test('offline shell lookup uses only the current named cache', async () => { const request = { method: 'GET', url: 'https://example.test/app.js' }; const current = { body: 'current app' }; const { listeners } = loadWorker({ currentHits: new Map([[request.url, current]]) }); const response = await dispatchFetch(listeners.get('fetch'), request); assert.equal(await response, current); }); test('scoped API GET requests bypass the shell cache entirely', async () => { const request = { method: 'GET', url: 'https://example.test/timmy-staging/api/healthz' }; const networkResponse = { headers: { get: () => 'no-store' }, clone: () => ({}) }; const { listeners, puts, getCacheOpenCount } = loadWorker({ scope: 'https://example.test/timmy-staging/', fetchImpl: async () => networkResponse, }); const response = await dispatchFetch(listeners.get('fetch'), request); assert.equal(await response, networkResponse); assert.equal(getCacheOpenCount(), 0); assert.deepEqual(puts, []); }); test('no-store shell responses are returned without being cached', async () => { const request = { method: 'GET', url: 'https://example.test/styles.css' }; const networkResponse = { headers: { get: () => 'private, no-store' }, clone: () => ({}) }; const { listeners, puts } = loadWorker({ fetchImpl: async () => networkResponse }); const response = await dispatchFetch(listeners.get('fetch'), request); await response; await Promise.resolve(); assert.deepEqual(puts, []); });