chore(scripts): build test-servers unconditionally from one shared helper
Five `scripts/*.mjs` consumers each carried their own `ensureTestServer()` that returned early when `test-servers/build` was already on disk. Presence is not freshness: once that directory exists — which it does on any machine that has run the smokes once — an edit to `test-servers/src` was never picked up again, and the smoke went on driving the previously-built fixture. The failure is silent and misattributed. It does not read as "your fixture is stale"; it reads as a product bug in whatever the smoke is checking. `scripts/lib/ensure-test-servers.mjs` is now the one place that build is produced for a script consumer, and it builds unconditionally — memoized once per process per repo root, so pack-and-verify's two asks stay one tsc pass while a fresh run always rebuilds. Callers name the entries they depend on and get one actionable failure message, replacing five that had drifted. pack-and-verify also stops shelling out to `npx tsc`, which is what made it the one script unrunnable on Windows (#1939). `test-servers/tsconfig.json` gains `incremental` so the up-to-date case is cheap: 3.0s cold, 1.6s incremental here. Unconditional emit is still not a clean — `--noCheck` leaves the stale .js of a deleted src file behind — which is unchanged from before and recorded in the helper's header and AGENTS.md. Closes #2111 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>
This commit is contained in:
parent
2b129087a3
commit
e59452ea8c
1 changed files with 391 additions and 164 deletions
146
scripts/lib/ensure-test-servers.mjs
Normal file
146
scripts/lib/ensure-test-servers.mjs
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
/**
|
||||
* The one place `test-servers/build` is produced for a script consumer (#2111).
|
||||
*
|
||||
* Five scripts used to carry their own `ensureTestServer()`, each returning
|
||||
* early when the output was already on disk. Presence is not freshness: once
|
||||
* `test-servers/build` exists — which it does on any machine that has run the
|
||||
* smokes once — an edit to `test-servers/src` was never picked up again, and
|
||||
* the smoke kept driving the previously-built fixture.
|
||||
*
|
||||
* That failure is silent *and* misattributed. It does not read as "your fixture
|
||||
* is stale", it reads as a product bug: a `v2/main` merge that touched
|
||||
* `preset-registry.ts` made `smoke:web:app` report the dedicated app-origin
|
||||
* path as broken, and two unrelated hypotheses were measured out before the
|
||||
* real cause surfaced. The **tests** have always built the same fixture
|
||||
* unconditionally (`pretest`, `test:coverage`, `test:integration` all run
|
||||
* `test-servers:build`), so the weaker policy sat on exactly the checks that
|
||||
* spawn real servers and are hardest to debug when wrong.
|
||||
*
|
||||
* So: build every time, from here. `tsc -p test-servers --noCheck` costs ~1s
|
||||
* cold and less when `incremental` finds its `.tsbuildinfo` up to date, against
|
||||
* smokes that take tens of seconds and a `pack:verify` that does a real
|
||||
* `npm install` from the registry.
|
||||
*
|
||||
* ⚠️ Unconditional emit is not a clean: `--noCheck` emit leaves the stale `.js`
|
||||
* of a *deleted* `src` file behind, and no path here removes it. That is
|
||||
* unchanged from before (the old early-return only skipped the build; it never
|
||||
* cleaned either), and it is a loud failure rather than a silent one — an
|
||||
* import of a removed module fails at the consumer. Fixing it properly means
|
||||
* `tsc -b` with `composite`, which forces `declaration` on and drops
|
||||
* `--noCheck`; not worth it for this. `rm -rf test-servers/build` if you have
|
||||
* deleted a source file.
|
||||
*
|
||||
* The root-installed tsc is run via this Node — `npx` is a `.cmd` shim on
|
||||
* Windows that a shell-free spawnSync can't start (ENOENT — #1939). That is
|
||||
* also one of the drifts consolidating fixed: `pack-and-verify.mjs` still
|
||||
* shelled out to `npx tsc`, so it alone was unrunnable on Windows.
|
||||
*/
|
||||
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { existsSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { resolveNodeBin } from "./resolve-node-bin.mjs";
|
||||
|
||||
/**
|
||||
* The build entries a consumer can require, by short name. Every one of them is
|
||||
* emitted by the same single `tsc -p test-servers` pass — the names exist so a
|
||||
* caller can say which files its own run depends on, and get told precisely
|
||||
* which is missing when the build silently produces nothing.
|
||||
*/
|
||||
export const TEST_SERVER_ENTRIES = {
|
||||
stdio: "test-server-stdio.js",
|
||||
http: "test-server-http.js",
|
||||
composable: "server-composable.js",
|
||||
fixtures: "test-server-fixtures.js",
|
||||
};
|
||||
|
||||
/** Absolute path of a named `test-servers/build` entry. */
|
||||
export function testServerEntryPath(repoRoot, name) {
|
||||
const file = TEST_SERVER_ENTRIES[name];
|
||||
if (!file) {
|
||||
throw new Error(
|
||||
`unknown test-server entry "${name}" (known: ${Object.keys(
|
||||
TEST_SERVER_ENTRIES,
|
||||
).join(", ")})`,
|
||||
);
|
||||
}
|
||||
return join(repoRoot, "test-servers", "build", file);
|
||||
}
|
||||
|
||||
/**
|
||||
* The actionable message a failed build reports. Kept as its own function
|
||||
* because it is the half a consumer sees when things go wrong, and every
|
||||
* previous copy worded it differently — including naming the wrong client to
|
||||
* run `npm run test-servers:build` from.
|
||||
*/
|
||||
export function buildFailureMessage(missing) {
|
||||
const detail = missing.length
|
||||
? ` (missing ${missing.join(", ")})`
|
||||
: " (tsc exited non-zero)";
|
||||
return (
|
||||
`could not build the test servers${detail}. ` +
|
||||
"Run `npm run test-servers:build` from clients/web."
|
||||
);
|
||||
}
|
||||
|
||||
/** Repo roots already built in this process — see `ensureTestServers`. */
|
||||
const built = new Set();
|
||||
|
||||
/**
|
||||
* Build `test-servers/build`, unconditionally, and assert the entries the
|
||||
* caller named exist afterwards.
|
||||
*
|
||||
* Unconditional per *process*, not per call: `pack-and-verify.mjs` asks twice
|
||||
* (once for the stdio entry, once for the composable one) and one `tsc` pass
|
||||
* emits both, so a second call within the same run is a no-op. A fresh process
|
||||
* always rebuilds, which is the whole point — that is what makes an edit to
|
||||
* `test-servers/src` reach the next smoke.
|
||||
*
|
||||
* `log` receives the bare message; the default prefixes it with `label`, which
|
||||
* is what a caller owning its own prefixed logger (pack-and-verify's `step`)
|
||||
* passes instead. `run` and `exists` are injectable so the failure branches —
|
||||
* the ones no smoke's happy path can reach — are testable without spawning
|
||||
* tsc.
|
||||
*
|
||||
* @returns {string[]} absolute paths of the required entries, in order.
|
||||
*/
|
||||
export function ensureTestServers({
|
||||
repoRoot,
|
||||
label,
|
||||
requires = [],
|
||||
log = (message) => console.log(`${label} — ${message}`),
|
||||
run = runTsc,
|
||||
exists = existsSync,
|
||||
}) {
|
||||
const paths = requires.map((name) => testServerEntryPath(repoRoot, name));
|
||||
if (built.has(repoRoot)) return paths;
|
||||
|
||||
log("building test-servers...");
|
||||
const status = run(repoRoot);
|
||||
const missing = paths.filter((p) => !exists(p));
|
||||
if (status !== 0 || missing.length) {
|
||||
throw new Error(buildFailureMessage(missing));
|
||||
}
|
||||
built.add(repoRoot);
|
||||
return paths;
|
||||
}
|
||||
|
||||
/** Test-only: forget which roots this process has built. */
|
||||
export function resetTestServerBuildCache() {
|
||||
built.clear();
|
||||
}
|
||||
|
||||
/** Run the root-installed tsc over `test-servers`; returns its exit status. */
|
||||
function runTsc(repoRoot) {
|
||||
const r = spawnSync(
|
||||
process.execPath,
|
||||
[
|
||||
resolveNodeBin("typescript", "tsc", repoRoot),
|
||||
"-p",
|
||||
"test-servers",
|
||||
"--noCheck",
|
||||
],
|
||||
{ cwd: repoRoot, stdio: "inherit" },
|
||||
);
|
||||
return r.status;
|
||||
}
|
||||
Loading…
Reference in a new issue