Files
gstack/test/diagram-render-drift.test.ts
Garry Tanandcapy-ai[bot] a1487458d3 v1.91.241.0 fix(deps): bump dompurify, MCP SDK, zod, body-parser, fast-uri, ip-address to OSV-fixed releases (#3462) (#3463)
* fix(deps): bump dompurify, @modelcontextprotocol/sdk, zod, body-parser, fast-uri and ip-address to their OSV-fixed releases (#3462)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* build(diagram-render): empty excalidraw's public Firebase config literal in the bundle; declare BUILD_INFO.json's test lane

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test(diagram-render): name the bundle, BUILD_INFO and build script by repo path so the pre-gate sees their lane

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* chore(pregate): drop the BUILD_INFO.json lane declaration; the drift test names the file now

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: pre-landing review fixes

Testing specialist: the drift test asserts the bundled DOMPurify is the override and the bundle carries no Google API key; deps-smoke refuses ip-address below 10.7.4 and checks every bumped package against its OSV-fixed floor in both lockfiles. Adversarial: build.ts fails when any AIza key survives the strip. Outside voice and red team: the CHANGELOG hash and scope lines match the shipped tree.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs(diagram-render): document the Firebase-literal strip and the new drift checks

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

---------

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
2026-10-26 10:44:36 +00:00

115 lines
5.2 KiB
TypeScript

/**
* Drift guards for the committed diagram-render bundle (eng-review D2).
*
* Tier 1 (always, free, <50ms): dist/diagram-render.html must hash to exactly
* what dist/BUILD_INFO.json records, and the BUILD_INFO dependency pins must
* match package.json. Catches hand-edited dist files and "bumped the pin,
* forgot to rebuild" commits.
*
* Tier 2 (deep, CI / post-install only): rebuild from source and compare
* hashes. Skipped when lib/diagram-render/node_modules is absent (fresh
* clone without `bun install` in that dir) or when the local bun version
* differs from the one recorded at build time (minifier output is only
* guaranteed deterministic within a bun version).
*/
import { describe, expect, test } from "bun:test";
import { createHash } from "node:crypto";
import { existsSync } from "node:fs";
import path from "node:path";
// Repo-relative segment runs, so the pre-gate's import graph sees that this
// test owns the committed bundle, its BUILD_INFO and the build script.
const REPO = path.resolve(import.meta.dir, "..");
const ROOT = path.join(REPO, "lib", "diagram-render");
const DIST_HTML = path.join(REPO, "lib", "diagram-render", "dist", "diagram-render.html");
const BUILD_INFO = path.join(REPO, "lib", "diagram-render", "dist", "BUILD_INFO.json");
const BUILD_SCRIPT = path.join(REPO, "lib", "diagram-render", "scripts", "build.ts");
describe("diagram-render bundle drift", () => {
test("dist hash matches BUILD_INFO (tamper check)", async () => {
const html = await Bun.file(DIST_HTML).text();
const info = await Bun.file(BUILD_INFO).json();
const sha = createHash("sha256").update(html).digest("hex");
expect(sha).toBe(info.sha256);
expect(Buffer.byteLength(html)).toBe(info.bytes);
});
test("BUILD_INFO dependency pins match package.json", async () => {
const info = await Bun.file(BUILD_INFO).json();
const pkg = await Bun.file(path.join(ROOT, "package.json")).json();
expect(info.deps).toEqual(pkg.dependencies);
});
test("the bundled DOMPurify is the package.json override, and no other version", async () => {
const html = await Bun.file(DIST_HTML).text();
const pkg = await Bun.file(path.join(ROOT, "package.json")).json();
const versions = [...html.matchAll(/@license DOMPurify (\d+\.\d+\.\d+)/g)].map(m => m[1]);
expect(versions).toEqual([pkg.overrides.dompurify]);
expect(html).toContain(`version="${pkg.overrides.dompurify}"`);
});
test("the bundle carries no Google API key: excalidraw's Firebase config literal is emptied", async () => {
const html = await Bun.file(DIST_HTML).text();
expect(html).not.toMatch(/AIza[0-9A-Za-z_-]{35}/);
expect(html).toContain("VITE_APP_FIREBASE_CONFIG:'{}'");
});
test("BUILD_INFO srcSha256 matches src on disk (edited-src-forgot-rebuild guard)", async () => {
// The deep rebuild check below needs node_modules, which CI doesn't
// install for this nested package — this tier-1.5 fingerprint catches a
// src edit committed without a rebuild using nothing but file hashes.
const info = await Bun.file(BUILD_INFO).json();
const srcSha = createHash("sha256")
.update(await Bun.file(path.join(ROOT, "src", "entry.ts")).text())
.update(await Bun.file(BUILD_SCRIPT).text())
.digest("hex");
expect(srcSha).toBe(info.srcSha256);
});
test("bundle font stack matches print-css (text-measurement drift guard)", async () => {
const entrySrc = await Bun.file(path.join(ROOT, "src", "entry.ts")).text();
// Every family print-css composes into the body stack must appear in the
// bundle's PRINT_SANS literal — mermaid measures text with these fonts and
// the print document lays it out with print-css's; drift = overflowing
// labels (eng-review D3).
for (const family of [
"Helvetica", "Liberation Sans", "Arial",
"Hiragino Kaku Gothic ProN", "Noto Sans CJK JP", "Microsoft YaHei",
"Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji",
]) {
expect(entrySrc).toContain(family);
}
});
test("page invariants: module script, base href, escaped terminators, error trap", async () => {
const html = await Bun.file(DIST_HTML).text();
expect(html).toContain('<script type="module">');
expect(html).toContain('<base href="https://gstack-render.localhost/">');
expect(html).toContain("window.__errors = []");
// The inline module must contain no live </script> other than the page's
// own closers: head error-trap closer + module closer.
const closers = html.match(/<\/script>/g) ?? [];
expect(closers.length).toBe(2);
});
const nodeModules = path.join(ROOT, "node_modules");
let builtWithSameBun = false;
try {
const info = require(BUILD_INFO);
builtWithSameBun = info.bunVersion === Bun.version;
} catch {}
const canDeepCheck = existsSync(nodeModules) && builtWithSameBun;
test.skipIf(!canDeepCheck)(
"deep: fresh build reproduces committed dist",
async () => {
const before = await Bun.file(BUILD_INFO).json();
const proc = Bun.spawnSync(["bun", "run", "scripts/build.ts"], { cwd: ROOT, timeout: 120_000 });
expect(proc.exitCode).toBe(0);
const after = await Bun.file(BUILD_INFO).json();
expect(after.sha256).toBe(before.sha256);
},
60000,
);
});