inspector/scripts/lib/ensure-test-servers.test.mjs
cliffhall 8d193461e3 fix(scripts): pin the tsbuildinfo inside build/, assert on cache hits
Two findings from review round 1, both real.

The default incremental cache path derives from `rootDir`, so it landed at
`test-servers/tsconfig.tsbuildinfo` — beside the config, OUTSIDE `build/`.
That survives the `rm -rf test-servers/build` this project documents as the
remedy for a deleted source file, leaving tsc convinced its outputs are up
to date and emitting nothing. Pinned to `./build/tsconfig.tsbuildinfo`, and
verified that removing the output dir now forces a full re-emit.

The build is memoized per repo root; the existence assertion was memoized
with it. A later caller naming an entry the first did not would be handed a
path tsc never emitted, then fail with exactly the opaque
ERR_MODULE_NOT_FOUND the helper exists to replace. The assertion now runs on
every call.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JTHVxSu8AUgHRLvo1ntZ8H
Signed-off-by: cliffhall <cliff@futurescale.com>
2026-08-25 01:11:10 -04:00

159 lines
5 KiB
JavaScript

/**
* Unit tests for the shared test-server build helper (#2111).
*
* Every consumer's happy path builds successfully, so from a smoke's point of
* view each failure branch below is dead code — and the *invariant* the helper
* exists for (that it builds even when the output is already there) is one no
* consumer can assert about itself, because a smoke that drove a stale fixture
* would report a product failure rather than a staleness one. That is exactly
* the defect this replaced: an `existsSync` early return that made the check
* silently measure the previous build.
*
* `run` and `exists` are injectable precisely so this can be driven without
* spawning tsc.
*/
import assert from "node:assert/strict";
import { join } from "node:path";
import { beforeEach, describe, it } from "node:test";
import {
TEST_SERVER_ENTRIES,
buildFailureMessage,
ensureTestServers,
resetTestServerBuildCache,
testServerEntryPath,
} from "./ensure-test-servers.mjs";
const ROOT = "/repo";
/** A helper invocation with the spawn and the fs stubbed out. */
function callEnsure({
repoRoot = ROOT,
requires = ["stdio"],
status = 0,
present = () => true,
calls = [],
logs = [],
} = {}) {
return ensureTestServers({
repoRoot,
label: "test",
requires,
log: (m) => logs.push(m),
run: (root) => {
calls.push(root);
return status;
},
exists: present,
});
}
beforeEach(() => resetTestServerBuildCache());
describe("testServerEntryPath", () => {
it("resolves each known entry under test-servers/build", () => {
for (const [name, file] of Object.entries(TEST_SERVER_ENTRIES)) {
assert.equal(
testServerEntryPath(ROOT, name),
join(ROOT, "test-servers", "build", file),
);
}
});
it("throws on an unknown entry rather than resolving a bogus path", () => {
assert.throws(
() => testServerEntryPath(ROOT, "nope"),
/unknown test-server entry "nope"/,
);
});
});
describe("ensureTestServers", () => {
it("builds even when every required entry is already on disk", () => {
// The whole point of #2111: presence is not freshness.
const calls = [];
callEnsure({ present: () => true, calls });
assert.deepEqual(calls, [ROOT]);
});
it("returns the required entry paths, in the order requested", () => {
const paths = callEnsure({ requires: ["http", "stdio"] });
assert.deepEqual(paths, [
testServerEntryPath(ROOT, "http"),
testServerEntryPath(ROOT, "stdio"),
]);
});
it("builds once per process per repo root", () => {
// pack-and-verify asks twice; one tsc pass emits both entries.
const calls = [];
callEnsure({ requires: ["stdio"], calls });
const paths = callEnsure({ requires: ["composable"], calls });
assert.deepEqual(calls, [ROOT]);
// The second call still resolves its own entries.
assert.deepEqual(paths, [testServerEntryPath(ROOT, "composable")]);
});
it("still asserts existence on a cache hit that names a new entry", () => {
// The build is cached; the assertion is not. A second caller asking for an
// entry the first didn't must not be handed a path tsc never emitted.
const absent = testServerEntryPath(ROOT, "composable");
const present = (p) => p !== absent;
callEnsure({ requires: ["stdio"], present });
assert.throws(
() => callEnsure({ requires: ["composable"], present }),
(e) => e.message.includes(absent),
);
});
it("builds again for a different repo root", () => {
const calls = [];
callEnsure({ calls });
callEnsure({ repoRoot: "/other", calls });
assert.deepEqual(calls, [ROOT, "/other"]);
});
it("logs through the caller's logger, unprefixed", () => {
// pack-and-verify passes its own `step`, which adds "pack:verify — ".
const logs = [];
callEnsure({ logs });
assert.deepEqual(logs, ["building test-servers..."]);
});
it("throws naming the missing entry when tsc emits nothing", () => {
const missing = testServerEntryPath(ROOT, "composable");
assert.throws(
() =>
callEnsure({
requires: ["composable"],
present: (p) => p !== missing,
}),
(e) =>
e.message.includes(missing) &&
e.message.includes("npm run test-servers:build"),
);
});
it("throws on a non-zero tsc exit even when the entries exist", () => {
// A stale build left over from a previous run must not pass a failed tsc.
assert.throws(() => callEnsure({ status: 1 }), /tsc exited non-zero/);
});
it("does not mark a failed build as done", () => {
const calls = [];
assert.throws(() => callEnsure({ status: 1, calls }));
assert.throws(() => callEnsure({ status: 1, calls }));
assert.deepEqual(calls, [ROOT, ROOT]);
});
});
describe("buildFailureMessage", () => {
it("always points at the command that reproduces it", () => {
assert.match(buildFailureMessage([]), /npm run test-servers:build/);
});
it("lists every missing entry", () => {
const message = buildFailureMessage(["/a.js", "/b.js"]);
assert.match(message, /missing \/a\.js, \/b\.js/);
});
});