Three of the six Vitest projects ran on Vitest's own testTimeout: 5000 and five on its hookTimeout/teardownTimeout: 10000, and all 788 Testing Library waits in the web unit project ran on asyncUtilTimeout: 1000. None of those numbers was chosen here, and all of them were sized for an idle machine rather than for three or four concurrent worktree sessions each running a full local:gate on eight cores. #2292, #2278, #2250, #1942 and #1742 were each that class, diagnosed one site at a time. This is the complement of #1596, not a retreat from it: every fix that issue produced stands, and no race is hidden here. What changes is that a correct, deterministic test is no longer cut off by a budget nobody picked. - vitest.shared.mts exports frozen TIMEOUTS / INTEGRATION_TIMEOUTS; all six projects spread one of them, clients/launcher importing the shared module for the first time to do so. - Both web projects configure Testing Library's asyncUtilTimeout. The storybook one needs its own setup file, deliberately outside .storybook/ and free of setProjectAnnotations so the automatic preview-annotation provisioning (#1898) is untouched. - 28 per-suite restatements of the integration project's own budget are gone, so raising the shared value now moves every site under it. The handful that genuinely differ — cross-process lock contention, a full OAuth round trip — keep their budget as a named constant that says why. - AppRenderer's waitFor no longer asks for exactly its enclosing budget, which it could never win (#2292's shape); cli's e2e child timer drops below the project budget so the bound carrying the diagnostic is the one that fires. - The transition auto-settle's slack term is named RAF_SLACK_MS and raised; the term derived from HEADER_ANIM_MS is untouched. The two answer to different things and must not be scaled as one number. - The 19 Playwright budgets scattered across three web smokes become four named constants in scripts/lib/browser-timeouts.mjs. scripts/verify-test-timeouts.mjs resolves each project through Vitest itself and asserts the budgets, the asyncUtilTimeout sites, and that retry stays unset. It was observed red against the pre-change config: unit/tui/launcher at testTimeout 5000, five projects at hookTimeout 10000, both asyncUtilTimeout sites unmet. Closes #2323 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PsmUAZ6aLUAFqenjJKSSRp Signed-off-by: cliffhall <cliff@futurescale.com>
81 lines
3 KiB
TypeScript
81 lines
3 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
import { spawn } from "node:child_process";
|
|
import { resolve, dirname } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { getTestMcpServerCommand } from "@modelcontextprotocol/inspector-test-server";
|
|
|
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
const BIN = resolve(here, "../build/index.js");
|
|
|
|
/**
|
|
* In-test bound on the spawned child, deliberately BELOW the cli project's own
|
|
* `testTimeout` (15000, from `vitest.shared.mts`). The two used to be equal, so
|
|
* which one fired was arbitrary — and they do different things: this one kills
|
|
* the child's process group and rejects with a message naming the CLI, while
|
|
* the project budget just fails the test with a generic timeout and leaves a
|
|
* detached `node` behind. The useful diagnostic is here, so this is the bound
|
|
* that must win (#2323).
|
|
*/
|
|
const CHILD_TIMEOUT_MS = 12_000;
|
|
|
|
interface SpawnResult {
|
|
exitCode: number | null;
|
|
stdout: string;
|
|
stderr: string;
|
|
}
|
|
|
|
/**
|
|
* Spawn the **built** CLI binary as a real subprocess. This is the deliberately
|
|
* thin out-of-process layer that the in-process suite (cli-runner.ts) cannot
|
|
* reach: the shebang, `index.ts`'s `isMain` bootstrap, and the actual
|
|
* `process.exit` codes. Functional behavior is covered in-process under the
|
|
* coverage gate; this only asserts the binary boots and exits correctly. The
|
|
* binary is built by the `pretest` / `test:coverage` scripts before tests run.
|
|
*/
|
|
function spawnCli(args: string[]): Promise<SpawnResult> {
|
|
return new Promise((resolvePromise, reject) => {
|
|
const child = spawn("node", [BIN, ...args], {
|
|
stdio: ["pipe", "pipe", "pipe"],
|
|
detached: process.platform !== "win32",
|
|
});
|
|
let stdout = "";
|
|
let stderr = "";
|
|
const timer = setTimeout(() => {
|
|
if (process.platform !== "win32" && child.pid != null) {
|
|
process.kill(-child.pid, "SIGTERM");
|
|
} else {
|
|
child.kill("SIGTERM");
|
|
}
|
|
reject(new Error("E2E CLI timed out"));
|
|
}, CHILD_TIMEOUT_MS);
|
|
child.stdout.on("data", (d) => (stdout += d.toString()));
|
|
child.stderr.on("data", (d) => (stderr += d.toString()));
|
|
child.on("error", (err) => {
|
|
clearTimeout(timer);
|
|
reject(err);
|
|
});
|
|
child.on("close", (exitCode) => {
|
|
clearTimeout(timer);
|
|
resolvePromise({ exitCode, stdout, stderr });
|
|
});
|
|
});
|
|
}
|
|
|
|
describe("CLI binary (out-of-process E2E)", () => {
|
|
const { command, args } = getTestMcpServerCommand();
|
|
|
|
it("exits 0 and prints tools JSON on a successful run", async () => {
|
|
const result = await spawnCli([command, ...args, "--method", "tools/list"]);
|
|
|
|
expect(result.exitCode).toBe(0);
|
|
const json = JSON.parse(result.stdout);
|
|
expect(Array.isArray(json.tools)).toBe(true);
|
|
});
|
|
|
|
it("exits non-zero when required --method is missing", async () => {
|
|
const result = await spawnCli([command, ...args]);
|
|
|
|
expect(result.exitCode).not.toBe(0);
|
|
expect(result.stderr).toContain("Method is required");
|
|
});
|
|
});
|