chore: sync the frontend template from 4dc033c to 8157ad4
The product was materialized from the template at `4dc033c` and has stayed on it through 43 template commits, so it was missing all three rounds of adapter remediation — including files it never had, such as the shared `abortable-operation` primitive and the `exact-snapshot` decoder that later fixes are written against. Taking only the newest round was not possible for that reason: the delta is coherent only as a whole. The product had not touched `src/adapters` at all since materialization, so the 140-file delta applied with a three-way merge and no conflicts. `package.json` was the single overlap and merged cleanly: the product owns `name`, the template contributed `check:adapter-inventory`, `check:remediation-ledger` and the image-resolve-signal type fixture. All 24 product-owned files — README, index.html, CI workflow, i18n catalog, home page, generated schemas, evidence scripts, component and visual snapshots — are byte-identical to `main`. `template.lock.json` now pins the synced revision and tree. Verified in this repository, not inherited from the template: six type projects, lint, nine gates (adapter inventory, remediation ledger, registries, diagnostics, realtime boundaries, architecture, browser file/storage boundaries, optional recipes, documentation), the production build, and 2,054 of 2,073 tests. The 19 failures are all in `tests/unit/ci-artifact-contract.test.ts` and are the same pre-existing sandbox RLIMIT, EMFILE, umask and `/tmp` permission behaviour the template records; four suites that failed once under parallel load pass in isolation. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
002ba3624e
commit
4bff9ca151
@@ -0,0 +1,176 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { mkdir, mkdtemp, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
import {
|
||||
CACHEABLE_ASSET_CONTENT_TYPES,
|
||||
decodeStaticAssetManifest,
|
||||
isCanonicalStaticAssetUrl,
|
||||
} 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);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* SW-02. Sharing only the extension table left the generator and the decoder
|
||||
* with different path grammars: the generator emitted a URL for a directory
|
||||
* containing an `@` or a space, and the decoder then refused the manifest it
|
||||
* had just produced, failing the release build at install time.
|
||||
*/
|
||||
describe("SW-02 the generator and the decoder share one path grammar", () => {
|
||||
async function distWith(
|
||||
files: Readonly<Record<string, string>>,
|
||||
): Promise<string> {
|
||||
const root = await mkdtemp(path.join(tmpdir(), "sw-assets-"));
|
||||
for (const [relative, content] of Object.entries(files)) {
|
||||
const absolute = path.join(root, relative);
|
||||
await mkdir(path.dirname(absolute), { recursive: true });
|
||||
await writeFile(absolute, content);
|
||||
}
|
||||
return root;
|
||||
}
|
||||
|
||||
it("emits a manifest the runtime decoder accepts for every table entry", async () => {
|
||||
const { collectStaticAssets } = await import(
|
||||
"../../scripts/generate-service-worker-assets.ts"
|
||||
);
|
||||
const files: Record<string, string> = {};
|
||||
for (const [index, extension] of Object.keys(
|
||||
CACHEABLE_ASSET_CONTENT_TYPES,
|
||||
).entries()) {
|
||||
files[`assets/name-abcdefg${index}${extension}`] = `content-${index}`;
|
||||
}
|
||||
files["assets/nested/deep/name-abcdefgz.js"] = "nested";
|
||||
const root = await distWith(files);
|
||||
|
||||
const manifest = await collectStaticAssets(root, "build-1", "release-1");
|
||||
|
||||
expect(manifest.assets.length).toBe(
|
||||
Object.keys(CACHEABLE_ASSET_CONTENT_TYPES).length + 1,
|
||||
);
|
||||
expect(decodeStaticAssetManifest(manifest)).toMatchObject({ ok: true });
|
||||
for (const asset of manifest.assets) {
|
||||
expect(isCanonicalStaticAssetUrl(asset.url)).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ label: "an at sign", name: "bad@name-abcdefgh.js" },
|
||||
{ label: "a space", name: "bad name-abcdefgh.js" },
|
||||
{ label: "a percent escape", name: "bad%20name-abcdefgh.js" },
|
||||
{ label: "a hash", name: "bad#name-abcdefgh.js" },
|
||||
])(
|
||||
"stops the build rather than emitting a path the decoder refuses ($label)",
|
||||
async ({ name }) => {
|
||||
const { collectStaticAssets } = await import(
|
||||
"../../scripts/generate-service-worker-assets.ts"
|
||||
);
|
||||
const root = await distWith({
|
||||
"assets/good-abcdefgh.js": "ok",
|
||||
[`assets/${name}`]: "bad",
|
||||
});
|
||||
|
||||
await expect(
|
||||
collectStaticAssets(root, "build-1", "release-1"),
|
||||
).rejects.toThrow(/not canonical/u);
|
||||
},
|
||||
);
|
||||
|
||||
it("agrees with the decoder on the whole path policy table", () => {
|
||||
const cases: readonly (readonly [string, boolean])[] = [
|
||||
["/assets/name-abcdefgh.js", true],
|
||||
["/assets/nested/name-abcdefgh.js", true],
|
||||
["/assets/bad@name-abcdefgh.js", false],
|
||||
["/assets/bad name-abcdefgh.js", false],
|
||||
["/assets/bad%20name-abcdefgh.js", false],
|
||||
["/assets/bad#name-abcdefgh.js", false],
|
||||
["/assets/../name-abcdefgh.js", false],
|
||||
["/assets/./name-abcdefgh.js", false],
|
||||
["assets/name-abcdefgh.js", false],
|
||||
["/assets/\\name-abcdefgh.js", false],
|
||||
["/assets/naïve-abcdefgh.js", false],
|
||||
];
|
||||
for (const [url, canonical] of cases) {
|
||||
expect(isCanonicalStaticAssetUrl(url), url).toBe(canonical);
|
||||
const decoded = decodeStaticAssetManifest({
|
||||
schemaVersion: 1,
|
||||
buildId: "build-1",
|
||||
releaseId: "release-1",
|
||||
setDigest: `sha256:${"a".repeat(64)}`,
|
||||
assets: [
|
||||
{
|
||||
url,
|
||||
sha256: `sha256:${"a".repeat(64)}`,
|
||||
bytes: 4,
|
||||
contentType: "text/javascript",
|
||||
},
|
||||
],
|
||||
});
|
||||
// The decoder still owns the digest check, so a canonical path may fail
|
||||
// for other reasons; a non-canonical one must always fail.
|
||||
if (!canonical) expect(decoded.ok, url).toBe(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user