Files
siftlode/frontend/src/lib/nodeEngine.contract.test.ts
T
peter cc414de952 build: the gate is scripts/check.sh, and CI goes
The Gitea Actions gate repeated, after the merge, what a release already runs
before it, and its runner shared the git host's container: one run there
starved Gitea of memory until it stopped answering. The lanes now live in
scripts/check.sh, run the same by the release dispatcher and by hand, and the
gate-contract test holds its lines (the ruff and pip-audit checks read it
instead of the workflow). The two audits only CI ran join the gate: pip-audit
over the lock (pinned in the test image, blocking at any severity) and npm
audit of the production dependencies. The engine contracts drop the CI
containers; the register records that pushes have no server-side gate.
2026-10-04 06:20:58 +02:00

69 lines
3.5 KiB
TypeScript

import { readFileSync } from "node:fs";
import { expect, it } from "vitest";
// Four machines build or check this app, and for months they did not agree on which Node they were.
//
// On 2026-09-20 the server gate had been red for 73 of its last 90 frontend runs while the local gate
// was green, and nothing told anyone: the dev machine ran Node 24, the CI container `node:22-bookworm`,
// the prod build image `node:22-alpine`. The three failures were all engine splits, not code bugs —
// `Intl.NumberFormat({notation:"compact", maximumFractionDigits:1})` renders "$160.0M" on 22 and
// "$160M" on 24 (the minimum is the engine's to choose when you only give it a maximum), and
// `Response.blob()` under jsdom hands back node's own Blob class on 22 but jsdom's on 24, so
// `toBeInstanceOf(Blob)` asserts a realm rather than a type. Both are the engine showing through the
// test — and the first one the engine showing through to the USER, since the browser that renders the
// money string splits along the same line the build image does.
//
// So the version is a contract, not a coincidence, and this test is where it is held. `.nvmrc` is the
// single source of truth; everything below must agree with it, including the engine actually running
// this test. That last assertion is the one that pays: it turns "the local gate and the shipping image
// have drifted apart" from something discovered months later in a CI log nobody reads into a red lane
// on the very first run after the drift.
//
// To move the version: edit `.nvmrc`, then follow this test's failures. Every place it names is a place
// that would otherwise have been forgotten.
const readRoot = (rel: string) => readFileSync(new URL(`../../../${rel}`, import.meta.url), "utf8");
function majorIn(source: string, pattern: RegExp, where: string): number {
const m = pattern.exec(source);
// A rename or a moved line lands here. Re-point the test — the contract did not go away with it.
expect(m, `${pattern} not found in ${where}`).not.toBeNull();
return Number(m![1]);
}
const pinned = majorIn(readRoot(".nvmrc"), /^v?(\d+)\b/, ".nvmrc");
const pkg = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8")) as {
engines?: { node?: string };
devDependencies: Record<string, string>;
};
it("pins a plausible Node major", () => {
// Guards against a typo'd `.nvmrc` silently agreeing with nothing (`Number("v24.x") === NaN`).
expect(pinned).toBeGreaterThanOrEqual(24);
});
it("builds the shipped bundle on the pinned engine", () => {
// The image that compiles the SPA our users download.
expect(
majorIn(readRoot("Dockerfile"), /^FROM node:(\d+)-alpine AS frontend$/m, "Dockerfile"),
).toBe(pinned);
});
it("declares the pinned engine to npm and to tsc", () => {
expect(pkg.engines?.node).toBe(`${pinned}.x`);
// Types a major ahead of the runtime make tsc green over APIs the shipped engine does not have —
// the same defect as the gate split, just moved to compile time.
const types = pkg.devDependencies["@types/node"];
expect(types, "@types/node is gone from devDependencies").toBeTruthy();
expect(majorIn(types!, /^\D*(\d+)\./, "@types/node")).toBe(pinned);
});
it("is itself running on the pinned engine", () => {
const running = Number(process.versions.node.split(".")[0]);
expect(
running,
`this machine runs Node ${process.versions.node}, but the gate and the prod image ship Node ` +
`${pinned}.x. Install it (\`nvm use\` reads .nvmrc) — a green gate on another major does not ` +
`mean the build is green.`,
).toBe(pinned);
});