import { test } from "node:test"; import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import vm from "node:vm"; // The service workers' CACHING STRATEGY, run for real: each worker file is // evaluated in a node:vm context with a fake `self` / `caches` / `fetch`, and // its fetch handler is driven with synthetic events. contract.test.ts pins the // URL families these workers match; this pins what they DO with a match. // // The case that matters: /icons/* keeps its URL across a redeploy that changes // what it draws (a site's icons are lit with its accent), so it must be // network-first — a cache-first icon would outlive every accent change. // /_next/static/* is content-hashed and stays cache-first. const REPO = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../.."); const ORIGIN = "https://archive.example"; type FakeResponse = { body: string; ok: boolean; clone(): FakeResponse }; function res(body: string, ok = true): FakeResponse { return { body, ok, clone: () => res(body, ok) }; } // One worker, loaded fresh: returns a `request(path, {online})` that dispatches a // GET fetch event and resolves to the served body, plus what the network saw // and a way to seed the caches. function loadWorker(file: string) { const handlers: Record void> = {}; const stores = new Map>(); const store = (name: string) => { if (!stores.has(name)) stores.set(name, new Map()); return stores.get(name)!; }; let online = true; let network = ""; let networkOk = true; const fetched: string[] = []; const keyOf = (r: string | { url: string }) => new URL(typeof r === "string" ? r : r.url, ORIGIN).href; const context = { self: { addEventListener: (type: string, fn: (e: unknown) => void) => { handlers[type] = fn; }, location: { origin: ORIGIN }, skipWaiting: () => {}, clients: { claim: async () => {} }, }, caches: { open: async (name: string) => ({ match: async (r: string | { url: string }) => store(name).get(keyOf(r)), put: async (r: string | { url: string }, v: FakeResponse) => { store(name).set(keyOf(r), v); }, }), keys: async () => [...stores.keys()], delete: async (name: string) => stores.delete(name), }, fetch: async (r: { url: string }) => { fetched.push(new URL(r.url).pathname); if (!online) throw new TypeError("Failed to fetch"); return res(network, networkOk); }, Response: { error: () => ({ body: "", ok: false }) }, URL, console, }; vm.runInNewContext(readFileSync(path.join(REPO, file), "utf8"), context, { filename: file, }); assert.equal(typeof handlers.fetch, "function", `${file} registers no fetch handler`); return { fetched, seed: (cacheName: string, p: string, body: string) => store(cacheName).set(new URL(p, ORIGIN).href, res(body)), cached: (cacheName: string, p: string) => store(cacheName).get(new URL(p, ORIGIN).href)?.body, request: async (p: string, opts: { online: boolean; network?: string; ok?: boolean }) => { online = opts.online; network = opts.network ?? ""; networkOk = opts.ok ?? true; let served: Promise | undefined; handlers.fetch({ request: { method: "GET", url: new URL(p, ORIGIN).href, mode: "no-cors", destination: "image" }, respondWith: (p2: Promise) => { served = p2; }, }); assert.ok(served, `${p}: the worker did not answer`); return (await served).body; }, }; } for (const file of ["export/service-worker/site-sw.js", "export/service-worker/sw-hub.js"]) { test(`${file}: /icons/ is network-first in shell-v2 — a redeployed accent reaches readers`, async () => { const sw = loadWorker(file); sw.seed("shell-v2", "/icons/icon.svg", "old mark"); // Online: the fresh icon wins over the cached one, and replaces it. assert.equal(await sw.request("/icons/icon.svg", { online: true, network: "new mark" }), "new mark"); assert.deepEqual(sw.fetched, ["/icons/icon.svg"]); assert.equal(sw.cached("shell-v2", "/icons/icon.svg"), "new mark"); // Offline: the cached icon is still served. assert.equal(await sw.request("/icons/icon.svg", { online: false }), "new mark"); }); test(`${file}: a non-ok online icon response is passed through and never replaces the cached icon`, async () => { const sw = loadWorker(file); sw.seed("shell-v2", "/icons/icon-192.png", "cached icon"); // A 404/503 mid-deploy reaches the page as it is, but is not stored… assert.equal( await sw.request("/icons/icon-192.png", { online: true, network: "<503>", ok: false }), "<503>", ); assert.deepEqual(sw.fetched, ["/icons/icon-192.png"]); assert.equal(sw.cached("shell-v2", "/icons/icon-192.png"), "cached icon"); // …so the next offline launch still has the good icon. assert.equal(await sw.request("/icons/icon-192.png", { online: false }), "cached icon"); }); test(`${file}: a failed icon fetch with nothing cached is a network error, not a cached miss`, async () => { // The first test's offline half covers a failed fetch WITH a cached icon. const sw = loadWorker(file); assert.equal(await sw.request("/icons/icon.svg", { online: false }), ""); assert.deepEqual(sw.fetched, ["/icons/icon.svg"]); assert.equal(sw.cached("shell-v2", "/icons/icon.svg"), undefined); }); test(`${file}: /_next/static/ stays cache-first`, async () => { const sw = loadWorker(file); sw.seed("shell-v2", "/_next/static/chunks/app.js", "cached chunk"); assert.equal( await sw.request("/_next/static/chunks/app.js", { online: true, network: "network chunk" }), "cached chunk", ); assert.deepEqual(sw.fetched, []); }); }