import test from "node:test"; import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import vm from "node:vm"; type FetchEvent = { request: RequestLike; respondWith: (response: Promise) => void; }; type ServiceWorkerEvent = FetchEvent | Record; type RequestLike = { url: string; method: string; mode: string; destination: string; }; function createServiceWorkerHarness() { const cacheEntries = new Map(); const listeners = new Map void>(); let fetchImpl: (request: RequestLike) => Promise = async () => { throw new Error("network unavailable"); }; const cache = { addAll: async (urls: string[]) => { for (const url of urls) { cacheEntries.set(url, new Response(`cached ${url}`, { status: 200 })); } }, delete: async (request: Request) => cacheEntries.delete(request.url), keys: async () => [...cacheEntries.keys()].map((url) => new Request(url)), put: async (request: Request, response: Response) => { cacheEntries.set(request.url, response); }, }; const caches = { delete: async () => true, keys: async () => ["omniroute-pwa-v2"], match: async (request: Request | string) => cacheEntries.get(typeof request === "string" ? request : request.url), open: async () => cache, }; const context = vm.createContext({ URL, Request, Response, caches, fetch: (request: RequestLike) => fetchImpl(request), self: { clients: { claim: async () => undefined }, location: { href: "https://app.example/sw.js", origin: "https://app.example" }, registration: { showNotification: async () => undefined }, skipWaiting: async () => undefined, addEventListener: (type: string, listener: (event: ServiceWorkerEvent) => void) => { listeners.set(type, listener); }, }, }); vm.runInContext(readFileSync("public/sw.js", "utf8"), context); return { cacheEntries, dispatchFetch: async (request: RequestLike) => { const listener = listeners.get("fetch"); assert.ok(listener, "fetch listener must be registered"); let responsePromise: Promise | undefined; const event: FetchEvent = { request, respondWith: (response) => { responsePromise = response; }, }; listener(event); assert.ok(responsePromise, "navigate request must call respondWith"); return responsePromise; }, setFetch: (nextFetch: (request: Request) => Promise) => { fetchImpl = nextFetch; }, }; } test("#11779: network failure on navigation surfaces an error, not a stale shell", async () => { const harness = createServiceWorkerHarness(); const request = { url: "https://app.example/dashboard", method: "GET", mode: "navigate", destination: "document", }; // A cached navigation response from a PREVIOUS deploy exists; serving it // would replay a shell whose /_next/static// chunks are gone. harness.cacheEntries.set(request.url, new Response("cached dashboard", { status: 200 })); const response = await harness.dispatchFetch(request); assert.ok(response.type === "error", "navigation fallback must surface the network failure"); }); test("#5165: successful navigations are cached for later transient failures", async () => { const harness = createServiceWorkerHarness(); const request = { url: "https://app.example/dashboard", method: "GET", mode: "navigate", destination: "document", }; harness.setFetch(async () => new Response("fresh dashboard", { status: 200 })); assert.equal(await (await harness.dispatchFetch(request)).text(), "fresh dashboard"); harness.setFetch(async () => { throw new Error("transient network failure"); }); // #11779: the transient failure surfaces as an error response. The freshly // cached entry stays in the cache (a future navigations-are-cached design // may use it), but it must NOT be replayed as a navigation response -- the // shell's chunk references belong to a specific build. const failed = await harness.dispatchFetch(request); assert.ok(failed.type === "error", "transient failure must surface, not replay cache"); });