SW-RR-01. The activation marker was read with response.text() whenever no Content-Length was present, so a large or non-terminating body could consume the whole activation step. It now reads through a bounded reader that stops one byte past the ceiling, cancels its reader, applies a read deadline and decodes UTF-8 fatally. SW-RR-02. A matching nonce is not identity. An activation or reset result whose event.source is null can no longer stand in for the expected worker; only a strict identity match is admitted. SW-RR-03. The build generator and the shared manifest decoder now read one exported extension table, so .mjs and .png stop being emitted-then-refused. .json is deliberately outside it: every JSON file in a build output is a control document the generator already excludes, not a cacheable asset. SW-RR-04. Both caches.open and cache.match are closed as a miss. Letting a match rejection propagate rejected respondWith itself, so the entry never reached its network fallback. WP-RR-01. focus and openWindow now carry the certainty phase showNotification already had — NOT_APPLIED, MAYBE_APPLIED, CONFIRMED — and an effect that lands after the handler deadline is observed exactly once. The evidence never authorizes a retry. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
70 lines
2.0 KiB
TypeScript
70 lines
2.0 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
|
|
import {
|
|
CACHEABLE_ASSET_CONTENT_TYPES,
|
|
decodeStaticAssetManifest,
|
|
} from "../../src/contracts/service-worker-static-manifest.ts";
|
|
|
|
/**
|
|
* SW-RR-03. The build generator and the shared runtime decoder must agree on
|
|
* exactly which asset kinds exist. A generator that emits `.mjs` or `.png` while
|
|
* the decoder refuses them turns a correct build into a runtime contract
|
|
* failure, and the reverse admits a kind no build produces.
|
|
*/
|
|
describe("SW-RR-03 one authoritative cacheable asset table", () => {
|
|
it("covers every extension the generator emits", async () => {
|
|
const generator = await import(
|
|
"../../scripts/generate-service-worker-assets.ts"
|
|
);
|
|
expect(generator).toBeDefined();
|
|
for (const extension of [
|
|
".js",
|
|
".mjs",
|
|
".css",
|
|
".woff2",
|
|
".svg",
|
|
".png",
|
|
".webp",
|
|
]) {
|
|
expect(CACHEABLE_ASSET_CONTENT_TYPES[extension]).toBeDefined();
|
|
}
|
|
});
|
|
|
|
it("decodes a manifest row for every table entry", () => {
|
|
const assets = Object.entries(CACHEABLE_ASSET_CONTENT_TYPES).map(
|
|
([extension, contentType], index) => ({
|
|
url: `/assets/name-abcdefgh${index}${extension}`,
|
|
sha256: `sha256:${"a".repeat(64)}`,
|
|
bytes: 16,
|
|
contentType,
|
|
}),
|
|
);
|
|
const decoded = decodeStaticAssetManifest({
|
|
schemaVersion: 1,
|
|
buildId: "build-1",
|
|
releaseId: "release-1",
|
|
assets,
|
|
setDigest: `sha256:${"b".repeat(64)}`,
|
|
});
|
|
expect(decoded).toMatchObject({ ok: true });
|
|
});
|
|
|
|
it("refuses a row whose extension is not in the table", () => {
|
|
const decoded = decodeStaticAssetManifest({
|
|
schemaVersion: 1,
|
|
buildId: "build-1",
|
|
releaseId: "release-1",
|
|
setDigest: `sha256:${"b".repeat(64)}`,
|
|
assets: [
|
|
{
|
|
url: "/assets/control-abcdefgh.json",
|
|
sha256: `sha256:${"a".repeat(64)}`,
|
|
bytes: 16,
|
|
contentType: "application/json",
|
|
},
|
|
],
|
|
});
|
|
expect(decoded.ok).toBe(false);
|
|
});
|
|
});
|