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>
159 lines
5 KiB
JavaScript
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/);
|
|
});
|
|
});
|