Something went wrong. Try again.
Monorepo for the nibling.run platform
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117// 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/<guest>.json a guest under tests/fixtures/guests run to exit// tests/goldens/http/<app>.json a demo app's answers to a requests file// tests/goldens/ws/<app>.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 <float>` (TAP/diagnostic fields) and `(<float>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.<anonymous>", 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<fn> $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/<name>). */ 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<K extends Kind>(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<K extends Kind>(kind: K, name: string, g: Goldens[K]): void { const f: Record<string, unknown> = { ...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();}