// Goldens on disk: what the pinned Node did, with the inputs that made it do so. // scripts/golden.mjs records and checks them; the specs run each against the kernel. // tests/goldens/guests/.json a guest under tests/fixtures/guests run to exit // tests/goldens/http/.json a demo app's answers to a requests file // tests/goldens/ws/.json a demo app's WebSocket exchange with its client.js // Kept apart from tests/fixtures/guests because some guests list their own directory. import { mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs"; import { dirname, join, relative } from "node:path"; import { fileURLToPath } from "node:url"; export const GOLDENS = fileURLToPath(new URL("../tests/goldens", import.meta.url)); export interface GuestGolden { guest: string; /** Extra files the guest loads, relative to tests/fixtures/guests. */ deps?: string[]; /** Piped into the guest, then closed. */ stdin?: string; /** The Node that recorded the outputs below. */ node: string; /** null when a signal ended the process, as spawnSync reports it. */ code: number | null; signal?: string; stdout: string; stderr: string; /** Narrow, opt-in masking applied identically by scripts/golden.mjs (before writing) * and tests/guest.ts's goldenTest (to the kernel's live output before comparing) — * never applied globally. "test-durations" covers node:test's own non-determinism: * `duration_ms ` (TAP/diagnostic fields) and `(ms)` (spec/dot * reporter text) become `duration_ms N` / `(Nms)`, and stack frames inside Node's * own internals (`node:internal/...`, `node:async_hooks`) are dropped — this * runtime has no such frames to match, by construction. */ mask?: "test-durations"; } /** See GuestGolden#mask. node:test's own stack traces name the calling frame after * how the function happens to be invoked (Node: "TestContext.", via * runInAsyncScope; this runtime: "Test.eval [as fn]", via its own indirect-eval CJS * wrapper — DESIGN.md's "CJS stack lines are the guest's own"), so a frame naming * difference alone is not a real behavioral gap — only the file:line:col is. */ export function maskTestDurations(text: string): string { return text .replace(/duration_ms -?[0-9]+(\.[0-9]+)?/g, "duration_ms N") .replace(/\(-?[0-9]+(\.[0-9]+)?ms\)/g, "(Nms)") .split("\n") // Internal call frames neither side can match the other's: Node's own // (node:internal/..., node:async_hooks) and this runtime's own bundle // (.../dist/runtime/runtime.js, wherever that's served from). .filter((line) => !/\bat .*\((?:node:(?:internal\/|async_hooks)|[^)]*\/dist\/runtime\/runtime\.js)/.test(line)) // A remaining frame's function-name label (the part before the location) is a V8 // calling-convention artifact of each runtime's own CJS wrapper; only the // location (file:line:col) is a real assertion about where the error happened. .map((line) => line.replace(/^(\s*at )[^(]+(\(.*\))$/, "$1 $2")) .join("\n"); } /** An app's recorded HTTP answers. The `Exchange` shape is tests/http-golden.ts's. */ export interface HttpGolden { /** The app directory, relative to the package root (demo/apps/). */ app: string; /** The port the app listens on; it is part of the unmodified app. */ port: number; /** The requests file replayed against it, relative to the package root. */ requests: string; node: string; exchanges: unknown[]; } /** An app's WebSocket exchange: the log lines its public/client.js `exchange` returned. */ export interface WsGolden { app: string; port: number; node: string; log: string[]; } export interface Goldens { guests: GuestGolden; http: HttpGolden; ws: WsGolden; } export type Kind = keyof Goldens; export const KINDS: Kind[] = ["guests", "http", "ws"]; export const goldenPath = (kind: Kind, name: string): string => join(GOLDENS, kind, `${name}.json`); // Guest output is stored as a list of lines (the last is "" after a trailing newline), // so a re-recording diffs line by line. const LINED = ["stdout", "stderr"] as const; export function readGolden(kind: K, name: string): Goldens[K] { const f = JSON.parse(readFileSync(goldenPath(kind, name), "utf8")); if (kind === "guests") for (const k of LINED) f[k] = f[k].join("\n"); return f; } export function writeGolden(kind: K, name: string, g: Goldens[K]): void { const f: Record = { ...g }; if (kind === "guests") for (const k of LINED) f[k] = (f[k] as string).split("\n"); mkdirSync(dirname(goldenPath(kind, name)), { recursive: true }); writeFileSync(goldenPath(kind, name), JSON.stringify(f, null, 2) + "\n"); } /** Every recorded name of a kind, sorted; with a prefix, only names starting with it. */ export function listGoldens(kind: Kind, prefix = ""): string[] { const root = join(GOLDENS, kind); const out: string[] = []; const walk = (dir: string): void => { for (const e of readdirSync(dir, { withFileTypes: true })) { if (e.isDirectory()) walk(join(dir, e.name)); else if (e.name.endsWith(".json")) out.push(relative(root, join(dir, e.name)).slice(0, -".json".length)); } }; walk(root); return out.filter((g) => g.startsWith(prefix)).sort(); }