import { describe, expect, it, vi } from "vitest"; import { createServiceWorkerPageController } from "../../src/adapters/service-worker/service-worker-page-controller.ts"; import { createServiceWorkerRuntime } from "../../src/adapters/service-worker/service-worker-lifecycle.ts"; import { createServiceWorkerMessage } from "../../src/adapters/service-worker/service-worker-protocol.ts"; import { installStaticAssets } from "../../src/adapters/service-worker/service-worker-static-assets.ts"; import { SERVICE_WORKER_BOUNDS, SERVICE_WORKER_SCRIPT_PATH, STATIC_CACHE_PREFIX, staticCacheName, type ServiceWorkerProtocolIdentity, type StaticAssetManifestV1, } from "../../src/contracts/service-worker.ts"; const ORIGIN = "https://app.example"; const SCRIPT_URL = `${ORIGIN}/service-worker.js`; function pageContainer(options: { waiting?: boolean; controlled?: boolean } = {}) { const listeners = new Set<(event: MessageEvent) => void>(); const waitingMessages: unknown[] = []; const controllerMessages: unknown[] = []; const worker = (messages: unknown[]) => ({ scriptURL: SCRIPT_URL, postMessage(message: unknown) { messages.push(message); }, }) as unknown as ServiceWorker; const waiting = options.waiting ? worker(waitingMessages) : null; const active = options.waiting ? null : worker([]); const controlled = options.controlled ? worker(controllerMessages) : null; const registration = { scope: `${ORIGIN}/`, installing: null, waiting, active, update: vi.fn(async () => {}), } as unknown as ServiceWorkerRegistration; const container = { controller: controlled, register: vi.fn(async () => registration), addEventListener(_type: string, listener: (event: MessageEvent) => void) { listeners.add(listener); }, removeEventListener(_type: string, listener: (event: MessageEvent) => void) { listeners.delete(listener); }, } as unknown as ServiceWorkerContainer; return { container, waiting, controlled, registration, waitingMessages, controllerMessages, listenerCount: () => listeners.size, dispatch(data: unknown, source?: { postMessage(message: unknown): void }) { const event = { data, origin: ORIGIN, source } as unknown as MessageEvent; for (const listener of [...listeners]) listener(event); }, }; } function pageController(container: ServiceWorkerContainer, blockers = [() => false]) { return createServiceWorkerPageController({ selection: { mode: "ACTIVE", scriptPath: SERVICE_WORKER_SCRIPT_PATH, handlers: [], }, disabledCleanup: false, routerBasePath: "/", origin: ORIGIN, buildId: "page-build", container, blockers, }); } const identity: ServiceWorkerProtocolIdentity = { serviceWorkerProtocolVersion: 1, cacheSchemaVersion: 1, buildId: "worker-build", releaseId: "release-1", contractSetDigest: `sha256:${"1".repeat(64)}`, staticAssetSetDigest: `sha256:${"2".repeat(64)}`, }; function workerScope() { const deleted: string[] = []; const clients = ["client-a", "client-b"].map((id) => ({ id, url: `${ORIGIN}/app/${id}`, messages: [] as unknown[], postMessage(message: unknown) { this.messages.push(message); }, })); const scope = { caches: { open: vi.fn(), keys: vi.fn(async () => [ `${STATIC_CACHE_PREFIX}${"a".repeat(16)}`, `${STATIC_CACHE_PREFIX}${"b".repeat(16)}`, "foreign-cache", ]), delete: vi.fn(async (name: string) => { deleted.push(name); return true; }), match: vi.fn(), }, clients: { matchAll: vi.fn(async () => clients) }, registrationScope: `${ORIGIN}/app/`, skipWaiting: vi.fn(async () => {}), fetcher: vi.fn(), digest: vi.fn(), }; return { scope, clients, deleted }; } describe("service worker static cache authority", () => { const setDigest = `sha256:${"c".repeat(64)}` as const; const currentCacheName = `${STATIC_CACHE_PREFIX}${setDigest.slice( "sha256:".length, "sha256:".length + 16, )}`; function staticRuntime( caches: Readonly<{ open: (name: string) => Promise; keys: () => Promise; delete: (name: string) => Promise; }>, ) { return createServiceWorkerRuntime( { caches, clients: { matchAll: vi.fn(async () => []) }, registrationScope: `${ORIGIN}/app/`, skipWaiting: vi.fn(async () => {}), fetcher: vi.fn(), digest: vi.fn(), } as never, { identity: { ...identity, staticAssetSetDigest: setDigest }, handlers: ["PWA_STATIC_ASSETS"], manifest: { schemaVersion: 1, buildId: identity.buildId, releaseId: identity.releaseId, setDigest, // SW-URL-01. Generator output is root-relative. assets: [ { url: "/assets/app.0123456789abcdef.js", sha256: `sha256:${"d".repeat(64)}`, bytes: 10, contentType: "text/javascript", }, ], }, runtimeConfigUrl: "/runtime-config.json", releaseManifestUrl: "/release-manifest.json", }, ); } it("classifies a generated root-relative asset against an absolute Request URL", async () => { const currentResponse = new Response("current", { status: 200 }); const opened: string[] = []; const runtime = staticRuntime({ open: vi.fn(async (name: string) => { opened.push(name); return { match: async () => currentResponse.clone(), delete: async () => true, }; }), keys: vi.fn(async () => [currentCacheName]), delete: vi.fn(async () => true), }); const served = await runtime.onFetch({ method: "GET", url: `${ORIGIN}/assets/app.0123456789abcdef.js`, }); expect(served).not.toBeNull(); expect(opened).toEqual([currentCacheName]); }); it("matches static responses only in the current release cache", async () => { const previousCacheName = `${STATIC_CACHE_PREFIX}${"e".repeat(16)}`; const previousMatch = vi.fn( async () => new Response("previous", { status: 200 }), ); const deleteEntry = async (): Promise => true; const emptyMatch = async (): Promise => undefined; const previousCache = { match: previousMatch, delete: deleteEntry }; const emptyCache = { match: emptyMatch, delete: deleteEntry }; const runtime = staticRuntime({ open: vi.fn(async (name: string) => name === previousCacheName ? previousCache : emptyCache, ), keys: vi.fn(async () => [currentCacheName, previousCacheName]), delete: vi.fn(async () => true), }); // Only the previous cache holds the entry, so the request falls through to // the network rather than serving a stale release. await expect( runtime.onFetch({ method: "GET", url: `${ORIGIN}/assets/app.0123456789abcdef.js`, }), ).resolves.toBeNull(); expect(previousMatch).not.toHaveBeenCalled(); }); it("deletes an invalid hit only from the current release cache", async () => { const deletes: string[] = []; const runtime = staticRuntime({ open: vi.fn(async (name: string) => { const recordDelete = async (url: string): Promise => { deletes.push(`${name}:${url}`); return true; }; return { match: async () => new Response("bad", { status: 500 }), delete: recordDelete, }; }), keys: vi.fn(async () => [currentCacheName]), delete: vi.fn(async () => true), }); await expect( runtime.onFetch({ method: "GET", url: `${ORIGIN}/assets/app.0123456789abcdef.js`, }), ).resolves.toBeNull(); expect(deletes).toEqual([ `${currentCacheName}:${ORIGIN}/assets/app.0123456789abcdef.js`, ]); }); }); describe("service worker exact ownership and truthful removal", () => { it("deletes only exact owned static cache names", async () => { const fixture = workerScope(); fixture.scope.caches.keys = vi.fn(async () => [ `${STATIC_CACHE_PREFIX}${"a".repeat(16)}`, `${STATIC_CACHE_PREFIX}${"b".repeat(16)}`, // SW-02. Same prefix, not owned. `${STATIC_CACHE_PREFIX}not-owned`, `${STATIC_CACHE_PREFIX}${"c".repeat(17)}`, `${STATIC_CACHE_PREFIX}${"A".repeat(16)}`, "foreign-cache", ]); const runtime = createServiceWorkerRuntime(fixture.scope as never, { identity, handlers: [], manifest: null, runtimeConfigUrl: "/runtime-config.json", releaseManifestUrl: "/release-manifest.json", }); const request = createServiceWorkerMessage({ kind: "CACHE_RESET_REQUEST", sourceBuildId: "page-build", targetBuildId: identity.buildId, nonce: "nonce-reset-0001", }); await runtime.onCacheResetRequest(request, fixture.clients[0] as never); expect(fixture.deleted).toEqual([ `${STATIC_CACHE_PREFIX}${"a".repeat(16)}`, `${STATIC_CACHE_PREFIX}${"b".repeat(16)}`, ]); }); it.each([ { label: "unregister false", unregister: async () => false, expected: { kind: "FAILED" }, }, { label: "unregister rejects", unregister: async () => { throw new TypeError("unregister exploded"); }, expected: { kind: "FAILED" }, }, { label: "unregister true", unregister: async () => true, expected: { kind: "DISABLED" }, }, ])( "does not hide $label behind DISABLED", async ({ unregister, expected }) => { const registration = { scope: `${ORIGIN}/`, installing: null, waiting: null, active: { scriptURL: SCRIPT_URL }, unregister: vi.fn(unregister), update: vi.fn(async () => {}), } as unknown as ServiceWorkerRegistration; const controller = createServiceWorkerPageController({ container: { controller: null, register: vi.fn(), getRegistration: vi.fn(async () => registration), addEventListener: vi.fn(), removeEventListener: vi.fn(), } as never, routerBasePath: "/", origin: ORIGIN, selection: { mode: "REMOVE_REGISTRATION", scriptPath: "service-worker.js", handlers: [], }, } as never); await expect(controller.start()).resolves.toMatchObject(expected); }, ); it("reports an ownership mismatch as INCOMPATIBLE rather than DISABLED", async () => { const foreign = { scope: `${ORIGIN}/`, installing: null, waiting: null, active: { scriptURL: `${ORIGIN}/someone-else.js` }, unregister: vi.fn(async () => true), update: vi.fn(async () => {}), } as unknown as ServiceWorkerRegistration; const controller = createServiceWorkerPageController({ container: { controller: null, register: vi.fn(), getRegistration: vi.fn(async () => foreign), addEventListener: vi.fn(), removeEventListener: vi.fn(), } as never, routerBasePath: "/", origin: ORIGIN, selection: { mode: "REMOVE_REGISTRATION", scriptPath: "service-worker.js", handlers: [], }, } as never); await expect(controller.start()).resolves.toMatchObject({ kind: "INCOMPATIBLE", }); expect(foreign.unregister).not.toHaveBeenCalled(); }); }); describe("service worker page protocol", () => { it("does not attach late listeners when stopped during registration", async () => { const browser = pageContainer(); let completeRegistration: ((value: ServiceWorkerRegistration) => void) | undefined; const deferredRegistration = new Promise((resolve) => { completeRegistration = resolve; }); vi.mocked(browser.container.register).mockReturnValue(deferredRegistration); const controller = pageController(browser.container); const starting = controller.start(); await Promise.resolve(); await controller.stop(); completeRegistration?.(browser.registration); await expect(starting).resolves.toEqual({ kind: "FAILED", code: "STOPPED" }); expect(browser.listenerCount()).toBe(0); }); it("answers a worker drain request only after local blockers are clear", async () => { const browser = pageContainer({ waiting: true }); const controller = pageController(browser.container); await controller.start(); // SW-06. Replies are correlated by source identity, so the fake request // comes from the registration's waiting worker. const source = browser.waiting as unknown as { postMessage(message: unknown): void; }; const sourceMessages = vi.spyOn(source, "postMessage"); browser.dispatch( createServiceWorkerMessage({ kind: "CLIENT_DRAIN_REQUEST", sourceBuildId: "worker-build", targetBuildId: "page-build", nonce: "drain-1", }), source, ); expect(sourceMessages).toHaveBeenCalledWith( expect.objectContaining({ kind: "CLIENT_DRAINED", sourceBuildId: "page-build", targetBuildId: "worker-build", nonce: "drain-1", }), ); await controller.stop(); }); it("settles a pending activation and removes its listener when stopped", async () => { const browser = pageContainer({ waiting: true }); const controller = pageController(browser.container); await controller.start(); const activation = controller.requestActivation(); await Promise.resolve(); expect(browser.listenerCount()).toBe(2); await controller.stop(); await expect(activation).resolves.toEqual({ kind: "FAILED", code: "STOPPED" }); expect(browser.listenerCount()).toBe(0); }); it("waits for the correlated cache reset result", async () => { const browser = pageContainer({ controlled: true }); const controller = pageController(browser.container); await controller.start(); let settled = false; const result = controller.resetOwnedCaches().then((outcome) => { settled = true; return outcome; }); await Promise.resolve(); expect(settled).toBe(false); const request = browser.controllerMessages.at(-1) as { nonce?: string }; browser.dispatch({ ...createServiceWorkerMessage({ kind: "CACHE_RESET_RESULT", sourceBuildId: "worker-build", targetBuildId: "page-build", nonce: request.nonce, }), cachesDeleted: 2, }, browser.controlled ?? undefined); await expect(result).resolves.toEqual({ kind: "RESET", cachesDeleted: 2 }); await controller.stop(); }); }); describe("service worker worker-side protocol", () => { it("retains the immediately previous verified static cache on activation", async () => { const current = staticCacheName(identity.staticAssetSetDigest); const stale = `${STATIC_CACHE_PREFIX}${"3".repeat(16)}`; const previous = `${STATIC_CACHE_PREFIX}${"4".repeat(16)}`; const names = [stale, previous, current, "foreign-cache"]; const deleted: string[] = []; const markerPut = vi.fn(async () => {}); const cache = { match: vi.fn(async () => undefined), put: markerPut, delete: vi.fn(async () => true), } as unknown as Cache; const scope = { caches: { open: vi.fn(async () => cache), keys: vi.fn(async () => names), delete: vi.fn(async (name: string) => { deleted.push(name); return true; }), match: vi.fn(), }, clients: { matchAll: vi.fn(async () => []) }, skipWaiting: vi.fn(async () => {}), fetcher: vi.fn(), digest: vi.fn(), }; const runtime = createServiceWorkerRuntime(scope as never, { identity, handlers: ["PWA_STATIC_ASSETS"], manifest: { schemaVersion: 1, buildId: identity.buildId, releaseId: identity.releaseId, setDigest: identity.staticAssetSetDigest as `sha256:${string}`, assets: [], }, runtimeConfigUrl: "/runtime-config.json", releaseManifestUrl: "/release-manifest.json", }); await expect(runtime.onActivate()).resolves.toBe(1); expect(deleted).toEqual([stale]); expect(markerPut).toHaveBeenCalledOnce(); }); it("waits for every exact client drain acknowledgement before activation", async () => { const fixture = workerScope(); const runtime = createServiceWorkerRuntime(fixture.scope as never, { identity, handlers: [], manifest: null, runtimeConfigUrl: "/runtime-config.json", releaseManifestUrl: "/release-manifest.json", }); const request = createServiceWorkerMessage({ kind: "ACTIVATE_REQUEST", sourceBuildId: "page-build", targetBuildId: "worker-build", nonce: "activation-1", }); const activation = runtime.onActivateRequest(request); await Promise.resolve(); expect(fixture.scope.skipWaiting).not.toHaveBeenCalled(); runtime.onClientMessage( createServiceWorkerMessage({ kind: "CLIENT_DRAINED", sourceBuildId: "page-build", targetBuildId: "worker-build", nonce: "activation-1", }), "client-a", ); await Promise.resolve(); expect(fixture.scope.skipWaiting).not.toHaveBeenCalled(); runtime.onClientMessage( createServiceWorkerMessage({ kind: "CLIENT_DRAINED", sourceBuildId: "page-build", targetBuildId: "worker-build", nonce: "activation-1", }), "client-b", ); await expect(activation).resolves.toBe("ACCEPTED"); expect(fixture.scope.skipWaiting).toHaveBeenCalledTimes(1); }); it("enumerates uncontrolled window clients before an activation drain", async () => { const fixture = workerScope(); const runtime = createServiceWorkerRuntime(fixture.scope as never, { identity, handlers: [], manifest: null, runtimeConfigUrl: "/runtime-config.json", releaseManifestUrl: "/release-manifest.json", }); const request = createServiceWorkerMessage({ kind: "ACTIVATE_REQUEST", sourceBuildId: "page-build", targetBuildId: "worker-build", nonce: "activation-uncontrolled", }); const activation = runtime.onActivateRequest(request); await Promise.resolve(); for (const client of fixture.clients) { runtime.onClientMessage( createServiceWorkerMessage({ kind: "CLIENT_DRAINED", sourceBuildId: "page-build", targetBuildId: "worker-build", nonce: "activation-uncontrolled", }), client.id, ); } await expect(activation).resolves.toBe("ACCEPTED"); expect(fixture.scope.clients.matchAll).toHaveBeenCalledWith({ type: "window", includeUncontrolled: true, }); }); it("drains only page clients inside the exact registration scope", async () => { const inScope = { id: "client-in-scope", url: `${ORIGIN}/app/nested/page`, messages: [] as unknown[], postMessage(message: unknown) { this.messages.push(message); }, }; const outOfScope = { id: "client-out-of-scope", url: `${ORIGIN}/application/page`, messages: [] as unknown[], postMessage(message: unknown) { this.messages.push(message); }, }; const fixture = workerScope(); fixture.scope.clients.matchAll.mockResolvedValue([ inScope, outOfScope, ]); const runtime = createServiceWorkerRuntime(fixture.scope as never, { identity, handlers: [], manifest: null, runtimeConfigUrl: "/runtime-config.json", releaseManifestUrl: "/release-manifest.json", }); const activation = runtime.onActivateRequest( createServiceWorkerMessage({ kind: "ACTIVATE_REQUEST", sourceBuildId: "page-build", targetBuildId: "worker-build", nonce: "activation-scope", }), ); await Promise.resolve(); runtime.onClientMessage( createServiceWorkerMessage({ kind: "CLIENT_DRAINED", sourceBuildId: "page-build", targetBuildId: "worker-build", nonce: "activation-scope", }), inScope.id, ); runtime.onClientMessage( createServiceWorkerMessage({ kind: "CLIENT_DRAINED", sourceBuildId: "page-build", targetBuildId: "worker-build", nonce: "activation-scope", }), outOfScope.id, ); await expect(activation).resolves.toBe("ACCEPTED"); expect(inScope.messages).not.toHaveLength(0); expect(outOfScope.messages).toEqual([]); }); it("deletes only owned caches and returns the correlated reset count", async () => { const fixture = workerScope(); const runtime = createServiceWorkerRuntime(fixture.scope as never, { identity, handlers: [], manifest: null, runtimeConfigUrl: "/runtime-config.json", releaseManifestUrl: "/release-manifest.json", }); const source = { id: "client-a", url: `${ORIGIN}/app/client-a`, postMessage: vi.fn(), }; await runtime.onCacheResetRequest( createServiceWorkerMessage({ kind: "CACHE_RESET_REQUEST", sourceBuildId: "page-build", targetBuildId: "worker-build", nonce: "reset-1", }), source, ); expect(fixture.deleted).toEqual([ `${STATIC_CACHE_PREFIX}${"a".repeat(16)}`, `${STATIC_CACHE_PREFIX}${"b".repeat(16)}`, ]); expect(source.postMessage).toHaveBeenCalledWith( expect.objectContaining({ kind: "CACHE_RESET_RESULT", nonce: "reset-1", cachesDeleted: 2, }), ); }); }); describe("service worker static asset install", () => { const manifest: StaticAssetManifestV1 = { schemaVersion: 1, buildId: "worker-build", releaseId: "release-1", setDigest: `sha256:${"a".repeat(64)}`, assets: [ { url: `${ORIGIN}/assets/app.js`, sha256: `sha256:${"b".repeat(64)}`, bytes: 1, contentType: "application/javascript", }, ], }; it("aborts and rolls back a candidate cache at the overall install deadline", async () => { vi.useFakeTimers(); const deleteCache = vi.fn(async (_name: string) => true); const fetcher = vi.fn( (_input: RequestInfo | URL, init?: RequestInit) => new Promise((_resolve, reject) => { init?.signal?.addEventListener("abort", () => { reject(new DOMException("Install deadline", "AbortError")); }); }), ); const result = installStaticAssets(manifest, { caches: { open: vi.fn(async () => ({ put: vi.fn() }) as unknown as Cache), delete: deleteCache, }, fetcher: fetcher as typeof fetch, digest: vi.fn(), }); await vi.advanceTimersByTimeAsync(SERVICE_WORKER_BOUNDS.installDeadlineMs); await expect(result).resolves.toEqual({ kind: "REJECTED", code: "INSTALL_DEADLINE_EXCEEDED", }); expect(fetcher.mock.calls[0]?.[1]?.signal?.aborted).toBe(true); // SW-09. The public result closes at the deadline with one exact delete, // and a second exact delete is registered once the abandoned install work // actually settles. Both target the same owned candidate cache. await vi.advanceTimersByTimeAsync(0); expect(deleteCache).toHaveBeenCalledTimes(2); expect(new Set(deleteCache.mock.calls.map((call) => call[0])).size).toBe(1); vi.useRealTimers(); }); it("observes and cleans non-cooperative late install work", async () => { vi.useFakeTimers(); const deleteCache = vi.fn(async (_name: string) => true); const cancel = vi.fn(async () => {}); let releaseFetch: ((response: Response) => void) | undefined; // A fetch that ignores the abort signal entirely. const fetcher = vi.fn( () => new Promise((resolve) => { releaseFetch = resolve; }), ); const result = installStaticAssets(manifest, { caches: { open: vi.fn(async () => ({ put: vi.fn() }) as unknown as Cache), delete: deleteCache, }, fetcher: fetcher as typeof fetch, digest: vi.fn(), }); await vi.advanceTimersByTimeAsync(SERVICE_WORKER_BOUNDS.installDeadlineMs); await expect(result).resolves.toEqual({ kind: "REJECTED", code: "INSTALL_DEADLINE_EXCEEDED", }); // The late response arrives after the public bound; its body is cancelled // and no unhandled rejection escapes. releaseFetch?.({ body: { cancel } } as unknown as Response); await vi.advanceTimersByTimeAsync(0); expect(cancel).toHaveBeenCalledOnce(); vi.useRealTimers(); }); it("stops reading as soon as the streamed body exceeds declared bytes", async () => { const put = vi.fn(async () => {}); const cancel = vi.fn(async () => {}); const body = new ReadableStream({ start(controller) { controller.enqueue(new Uint8Array([1, 2])); }, cancel, }); await expect( installStaticAssets(manifest, { caches: { open: vi.fn(async () => ({ put }) as unknown as Cache), delete: vi.fn(async () => true), }, fetcher: vi.fn(async () => new Response(body, { status: 200, headers: { "content-type": "application/javascript" }, }), ), digest: vi.fn(), }), ).resolves.toEqual({ kind: "REJECTED", code: "BYTES_MISMATCH" }); expect(put).not.toHaveBeenCalled(); expect(cancel).toHaveBeenCalledTimes(1); }); it("aborts sibling asset fetches after the first install failure", async () => { vi.useFakeTimers(); try { let siblingSignal: AbortSignal | undefined; const twoAssetManifest: StaticAssetManifestV1 = { ...manifest, assets: [ manifest.assets[0]!, { url: `${ORIGIN}/assets/chunk.js`, sha256: `sha256:${"c".repeat(64)}`, bytes: 1, contentType: "application/javascript", }, ], }; const fetcher = vi.fn( async (input: RequestInfo | URL, init?: RequestInit) => { if (String(input).endsWith("app.js")) { return new Response(null, { status: 500 }); } siblingSignal = init?.signal ?? undefined; await new Promise((resolve) => setTimeout(resolve, 10)); return new Response(new Uint8Array([1]), { status: 200, headers: { "content-type": "application/javascript" }, }); }, ); const installing = installStaticAssets(twoAssetManifest, { caches: { open: vi.fn(async () => ({ put: vi.fn() }) as unknown as Cache), delete: vi.fn(async () => true), }, fetcher: fetcher as typeof fetch, digest: vi.fn(async () => twoAssetManifest.assets[1]!.sha256), }); await vi.advanceTimersByTimeAsync(10); await expect(installing).resolves.toEqual({ kind: "REJECTED", code: "STATUS_INVALID", }); expect(siblingSignal?.aborted).toBe(true); } finally { vi.useRealTimers(); } }); it("accepts replies only from the captured waiting or controller source", async () => { const browser = pageContainer({ waiting: true }); const controller = pageController(browser.container); await controller.start(); const foreign = { postMessage: vi.fn() }; // SW-06. A same-origin but unrecognised source must not close admission. browser.dispatch( createServiceWorkerMessage({ kind: "CLIENT_DRAIN_REQUEST", sourceBuildId: "worker-build", targetBuildId: "page-build", nonce: "drain-foreign", }), foreign, ); expect(foreign.postMessage).not.toHaveBeenCalled(); await controller.stop(); }); it("coalesces concurrent activation and reset commands", async () => { const browser = pageContainer({ waiting: true }); const controller = pageController(browser.container); await controller.start(); // SW-06. Ten concurrent callers issue exactly one request. const activations = Array.from({ length: 10 }, () => controller.requestActivation(), ); await Promise.resolve(); expect(browser.waitingMessages).toHaveLength(1); expect(new Set(activations).size).toBe(1); await controller.stop(); await Promise.allSettled(activations); }); it("ends an activation whose reply source was swapped", async () => { const browser = pageContainer({ waiting: true }); const controller = pageController(browser.container); await controller.start(); const activation = controller.requestActivation(); await Promise.resolve(); const request = browser.waitingMessages.at(-1) as { nonce?: string }; // A different worker answers: terminate immediately rather than waiting for // the drain timeout. browser.dispatch( createServiceWorkerMessage({ kind: "ACTIVATED_RELOAD_REQUIRED", sourceBuildId: "worker-build", targetBuildId: "page-build", nonce: request.nonce, }), { postMessage: vi.fn() }, ); await expect(activation).resolves.toEqual({ kind: "PROTOCOL_MISMATCH" }); await controller.stop(); }); });