openclaw/scripts/run-vitest.mts
Peter Steinberger 9137cfcc5f
perf(ci): run qualified unit tests with native Bun (#159988)
* perf(ci): run qualified unit tests with native Bun

* test(ci): keep a V8-only cache warm fixture

* fix(ci): include shared timeout in PR wrapper inventory

* perf(test): skip Bun bytecode cache during test teardown

* test: handle worker fixture retirement notifications

Handle rejected observer returns without delaying retirement, matching the
worker pool contract, and avoid shadowing the lane fixture input helper.

* perf(ci): qualify and tune Bun test workers

Complete SQLite admission before Bun Vitest threads start and retain each
worker generation's canonical cleanup decision in lifecycle fixtures.
Keep proxy-agent fixture values out of Bun's native fetch cache and compare
large socket payloads with full byte equality.

Renew the 292-file native qualification from paired 2,518-case proof.
Apply the existing UI JIT and allocator settings to ordinary unit-fast
Bun/Vitest selections and their cache warmer, preserving Node and native
Bun settings and dual-runtime release coverage.

Proof: 208 routing/warming cases; 61 lifecycle cases on each runtime;
66 ordered proxy/fetch cases plus the canonical 11-case proxy owner on each
runtime; independent P0-P2 review. The changed-file gate was interrupted
by the local disk reserve guard after core typing and 14 core-test type
shards passed. Tuned full-cohort performance remains to be measured;
the completed untuned comparison was 4.60% slower than Node.

* perf(test): expand native Bun coverage and reduce artifact IO

* perf(test): yield between artifact verification batches

* test: qualify native Bun cases after runtime integration

* test: refresh native Bun qualification after main integration
2026-10-01 17:28:11 -07:00

1222 lines
38 KiB
TypeScript

// Runs Vitest through repo project selection, local scheduling policy, output
// watchdogs, and process-group cleanup.
import fs from "node:fs";
import { constants as osConstants } from "node:os";
import path from "node:path";
import { assertTestHomeSelection } from "../test/test-home-policy.mts";
import { agentVitestProjectOwners } from "../test/vitest/vitest.agents-paths.mjs";
import { toolingIsolatedTestFiles } from "../test/vitest/vitest.tooling-isolated-paths.mjs";
import {
isPluginControlUiPath,
isUiBrowserTestFile,
isUiTestTarget,
uiTimingTestFiles,
} from "../test/vitest/vitest.ui-paths.mjs";
import { boundaryTestFiles } from "../test/vitest/vitest.unit-paths.mjs";
import { parsePermissiveBooleanToken } from "./lib/arg-utils.mts";
import { resolveExtensionTestConfig } from "./lib/extension-test-plan.mts";
import { createGatewayServerTestTargetChunks } from "./lib/gateway-server-test-plan.mts";
import { resolveRepoRoot } from "./lib/repo-root.mjs";
import {
prepareE2eVitestRuntime,
resolveVitestCliEntry,
prepareVitestRuntime,
} from "./lib/vitest-build-prerequisites.mts";
import { createVitestCacheSlots, resolveVitestCacheRoot } from "./lib/vitest-cache-slots.mts";
import {
hasNonRunVitestSubcommand,
collectVitestFileFilters,
resolveBooleanModeFlag,
resolveExplicitVitestMode,
vitestOptionConsumesNextArg,
} from "./lib/vitest-cli-mode.mts";
import { parseVitestExecutionArgs } from "./lib/vitest-cli.mts";
import { resolveVitestHomeSelection } from "./lib/vitest-home-selection.mts";
import {
resolveVitestProcessEnv,
resolveVitestNodeArgs,
resolveRunVitestSpawnEnv,
resolveVitestNoOutputTimeoutMs,
resolveVitestNoOutputHeartbeatMs,
resolveVitestConfigArg,
normalizeVitestConfigPath,
matchesVitestConfigPath,
} from "./lib/vitest-process-env.mts";
import {
spawnOwnedVitestProcess,
runVitestCli,
type exitVitestBySignal,
} from "./lib/vitest-process.mts";
import {
resolveVitestRuntimeCliSelections,
shouldPrepareVitestCoreWorkers,
} from "./lib/vitest-runtime-selection.mts";
import {
resolveNativeBunTestCommand,
resolveVitestTestCommand,
} from "./lib/vitest-test-runtime.mts";
import {
createVitestUnhandledErrorDetector,
stripVitestAnsi,
writeVitestUnhandledErrorSummary,
} from "./lib/vitest-unhandled-errors.mts";
import { createVitestWorkerRun, type VitestWorkerRun } from "./lib/vitest-worker-run.mts";
import { createPnpmRunnerSpawnSpec, type PnpmRunnerParams } from "./pnpm-runner.mts";
import {
forwardSignalToVitestProcessGroup,
installVitestProcessGroupCleanup,
shouldUseDetachedVitestProcessGroup,
terminateVitestProcessGroupForTimeout,
} from "./vitest-process-group.mts";
type VitestPathFs = Pick<typeof fs, "existsSync" | "statSync">;
type WatchdogStream = {
on(event: string, listener: (...args: unknown[]) => void): unknown;
off(event: string, listener: (...args: unknown[]) => void): unknown;
};
type NodeSignal = keyof typeof osConstants.signals;
type VitestOutputStream = {
setEncoding(encoding: "utf8"): unknown;
on(event: "data", listener: (chunk: string) => void): unknown;
on(event: "end", listener: () => void): unknown;
};
type VitestOutputTarget = {
write(chunk: string): unknown;
};
const SUPPRESSED_VITEST_STDERR_PATTERNS = ["[PLUGIN_TIMINGS]"];
const UI_VITEST_CONFIG = "test/vitest/vitest.ui.config.ts";
const UI_BROWSER_VITEST_CONFIG = "test/vitest/vitest.ui-browser.config.ts";
const TOOLING_DOCKER_VITEST_CONFIG = "test/vitest/vitest.tooling-docker.config.ts";
const TOOLING_VITEST_CONFIG = "test/vitest/vitest.tooling.config.ts";
const GATEWAY_SERVER_VITEST_CONFIG = "test/vitest/vitest.gateway-server.config.ts";
const E2E_VITEST_CONFIG = "test/vitest/vitest.e2e.config.ts";
const E2E_TEST_PROCESS_COUNT = 4;
const TOOLING_EXCLUDED_TESTS = new Set([
...boundaryTestFiles,
"test/scripts/docker-build-helper.test.ts",
...toolingIsolatedTestFiles,
]);
const EXPLICIT_FILE_TARGET_RE = /\.(?:[cm]?[jt]sx?)$/u;
const EXPLICIT_TEST_FILE_RE = /\.(?:test|e2e|live)\.(?:[cm]?[jt]sx?)$/u;
const GLOB_PATTERN_CHARS_RE = /[*?[\]{}]/u;
const UNBOUNDED_CONFIG_ONLY_OPTIONS = [
"--changed",
"--coverage",
"--dir",
"--mergeReports",
"--outputFile",
"--project",
"--root",
"--shard",
];
function isErrorWithCode(error: unknown, code: string): error is NodeJS.ErrnoException {
return error instanceof Error && "code" in error && error.code === code;
}
function isNodeSignal(signal: string): signal is NodeSignal {
return Object.hasOwn(osConstants.signals, signal);
}
function normalizeNodeSignal(signal: string | null): NodeSignal | null {
if (!signal) {
return null;
}
const unknownSignalMessage = `child process exited with unknown signal: ${signal}`;
if (!isNodeSignal(signal)) {
throw new Error(unknownSignalMessage);
}
return signal;
}
function hasVitestOption(argv: string[], option: string): boolean {
for (const arg of argv) {
if (arg === "--") {
return false;
}
if (arg === option || arg.startsWith(option + "=") || arg.startsWith(option + ".")) {
return true;
}
}
return false;
}
function insertVitestTargets(argv: string[], targets: string[]): string[] {
const separatorIndex = argv.indexOf("--");
const insertionIndex = separatorIndex < 0 ? argv.length : separatorIndex;
return [...argv.slice(0, insertionIndex), ...targets, ...argv.slice(insertionIndex)];
}
/**
* Bounds shared-worker lifetime with fresh processes so broad non-isolated
* runs do not exhaust the worker heap.
*/
export function resolveBoundedVitestInvocations(
argv: string[],
options: {
cwd?: string;
env?: NodeJS.ProcessEnv;
gatewayServerTargetChunks?: string[][];
} = {},
): string[][] {
const config = resolveVitestConfigArg(argv);
const normalizedConfig = config === null ? "" : normalizeVitestConfigPath(config);
const env = options.env ?? process.env;
const cwd = options.cwd ?? process.cwd();
const mode = resolveExplicitVitestMode(argv);
const isE2E = matchesVitestConfigPath(normalizedConfig, E2E_VITEST_CONFIG);
if (
(!isE2E && !matchesVitestConfigPath(normalizedConfig, GATEWAY_SERVER_VITEST_CONFIG)) ||
mode === "watch" ||
hasExplicitDisabledRunFlag(argv) ||
(mode !== "run" && parsePermissiveBooleanToken(env.CI) !== true) ||
hasNonRunVitestSubcommand(argv) ||
hasAlternateVitestRootArg(argv) ||
collectExplicitProjectRouterTargetArgs(argv, cwd).length > 0 ||
[...UNBOUNDED_CONFIG_ONLY_OPTIONS, "--bail"].some((option) => hasVitestOption(argv, option))
) {
return [argv];
}
if (isE2E) {
// Let Vitest own include/exclude and shard membership. Filtered and report-producing
// calls retain one process so small selections and aggregate outputs keep their semantics.
if (
collectExplicitFileTargetArgs(argv, (arg) => arg !== "run").length > 0 ||
argv.includes("--") ||
[
"--exclude",
"--testNamePattern",
"-t",
"--tagsFilter",
"--sequence.sequencer",
"--reporter",
"--reporters",
"--listTags",
"--clearCache",
"--standalone",
"--help",
"-h",
"--version",
"-v",
].some((option) => hasVitestOption(argv, option))
) {
return [argv];
}
return Array.from({ length: E2E_TEST_PROCESS_COUNT }, (_, index) => [
...argv,
`--shard=${index + 1}/${E2E_TEST_PROCESS_COUNT}`,
]);
}
const chunks = options.gatewayServerTargetChunks ?? createGatewayServerTestTargetChunks(cwd);
return chunks.length > 1 ? chunks.map((targets) => insertVitestTargets(argv, targets)) : [argv];
}
/**
* Builds spawn options for the primary Vitest child process.
*/
export function resolveVitestSpawnParams(
env: NodeJS.ProcessEnv = process.env,
platform: NodeJS.Platform = process.platform,
): PnpmRunnerParams {
return {
env: resolveVitestProcessEnv(env),
detached: shouldUseDetachedVitestProcessGroup(platform),
stdio: ["inherit", "pipe", "pipe"],
};
}
/**
* Filters known noisy Vitest stderr lines after stripping ANSI escapes.
*/
export function shouldSuppressVitestStderrLine(line: string): boolean {
const normalizedLine = stripVitestAnsi(line);
return SUPPRESSED_VITEST_STDERR_PATTERNS.some((pattern) => normalizedLine.includes(pattern));
}
/**
* Detects pnpm exec node invocations so the wrapper can spawn Node directly.
*/
function resolveDirectNodeVitestArgs(pnpmArgs: string[]): string[] | null {
return pnpmArgs[0] === "exec" && pnpmArgs[1] === "node" ? pnpmArgs.slice(2) : null;
}
function hasExplicitVitestConfigArg(argv: string[]): boolean {
// Delegation consumes separators; keep config-bearing argv direct so its tail
// cannot become an active override in the project runner.
return argv.some(
(arg) =>
arg === "--config" || arg === "-c" || arg.startsWith("--config=") || arg.startsWith("-c="),
);
}
function isPathLikeExplicitFileArg(arg: string): boolean {
return (
path.isAbsolute(arg) || arg.startsWith("./") || arg.startsWith("../") || /[/\\]/u.test(arg)
);
}
function isExplicitFileTargetArg(arg: string): boolean {
if (!EXPLICIT_FILE_TARGET_RE.test(arg) || GLOB_PATTERN_CHARS_RE.test(arg)) {
return false;
}
return isPathLikeExplicitFileArg(arg);
}
function isExplicitTestFileArg(arg: string): boolean {
return EXPLICIT_TEST_FILE_RE.test(arg) && isExplicitFileTargetArg(arg);
}
function validateNativeBunTestFiles(files: string[], cwd: string): string[] {
if (
files.length === 0 ||
files.some((file) => {
const relative = path.relative(cwd, path.resolve(cwd, file)).replaceAll(path.sep, "/");
return !isExplicitTestFileArg(file) || file !== `./${relative}` || relative.startsWith("../");
})
) {
throw new Error("Native Bun tests require explicit ./relative.test.ts file operands");
}
const missing = resolveMissingExplicitTestFiles(files, cwd);
if (missing.length > 0) {
throw new Error(`Native Bun test files not found: ${missing.join(", ")}`);
}
for (const file of files) {
if (!fs.statSync(path.resolve(cwd, file)).isFile()) {
throw new Error(`Native Bun test operand is not a file: ${file}`);
}
}
return files;
}
function isDelegableBroadProjectRouterTarget(arg: string, cwd: string): boolean {
const relative = toRepoRelativeArg(arg, cwd).replace(/\/+$/u, "");
return (
relative === "test/scripts" ||
relative === "test/scripts/*.test.ts" ||
relative === "test/scripts/**/*.test.ts"
);
}
function isPathAtOrUnder(value: string, root: string): boolean {
return value === root || value.startsWith(`${root}/`);
}
function isOwnedAgentDirectoryTarget(arg: string, cwd: string, fsImpl: VitestPathFs): boolean {
const relative = toRepoRelativeArg(arg, cwd).replace(/\/+$/u, "");
return (
isPathAtOrUnder(relative, agentVitestProjectOwners.all.root) &&
isExplicitDirectoryTargetArg(arg, cwd, fsImpl)
);
}
function isOwnedExtensionRootTarget(arg: string, cwd: string, fsImpl: VitestPathFs): boolean {
const relative = toRepoRelativeArg(arg, cwd).replace(/\/+$/u, "");
const [root, extensionId, ...remainder] = relative.split("/");
if (
root !== "extensions" ||
!extensionId ||
remainder.length > 0 ||
!isExplicitDirectoryTargetArg(arg, cwd, fsImpl)
) {
return false;
}
// Extension roots delegate so the bounded planner owns process lifetime (#124413).
// Raw Vitest would run the workspace config as one process.
return resolveExtensionTestConfig(relative).length > 0;
}
function isExplicitProjectRouterTargetArg(
arg: string,
cwd = process.cwd(),
fsImpl: VitestPathFs = fs,
): boolean {
if (!isPathLikeExplicitFileArg(arg)) {
return false;
}
if (GLOB_PATTERN_CHARS_RE.test(arg)) {
return isDelegableBroadProjectRouterTarget(arg, cwd);
}
if (isExplicitFileTargetArg(arg)) {
return true;
}
const filePath = path.isAbsolute(arg) ? arg : path.resolve(cwd, arg);
return fsImpl.existsSync(filePath)
? isDelegableBroadProjectRouterTarget(arg, cwd) ||
isOwnedAgentDirectoryTarget(arg, cwd, fsImpl) ||
isPluginControlUiPath(toRepoRelativeArg(arg, cwd)) ||
isOwnedExtensionRootTarget(arg, cwd, fsImpl)
: path.extname(arg) === "" &&
/^(?:src|test|extensions|ui|packages|apps)\//u.test(toRepoRelativeArg(arg, cwd));
}
function collectExplicitFileTargetArgs(
argv: string[],
predicate: (arg: string) => boolean = isExplicitFileTargetArg,
): string[] {
return collectVitestFileFilters(argv).filter(predicate);
}
function collectExplicitProjectRouterTargetArgs(
argv: string[],
cwd = process.cwd(),
fsImpl: VitestPathFs = fs,
): string[] {
return collectExplicitFileTargetArgs(argv, (arg) =>
isExplicitProjectRouterTargetArg(arg, cwd, fsImpl),
);
}
function isExplicitDirectoryTargetArg(
arg: string,
cwd = process.cwd(),
fsImpl: VitestPathFs = fs,
): boolean {
if (!isPathLikeExplicitFileArg(arg) || GLOB_PATTERN_CHARS_RE.test(arg)) {
return false;
}
const targetPath = path.isAbsolute(arg) ? arg : path.resolve(cwd, arg);
try {
return fsImpl.statSync(targetPath).isDirectory();
} catch {
return false;
}
}
function collectExplicitDirectoryTargetArgs(
argv: string[],
cwd = process.cwd(),
fsImpl: VitestPathFs = fs,
): string[] {
return collectExplicitFileTargetArgs(argv, (arg) =>
isExplicitDirectoryTargetArg(arg, cwd, fsImpl),
);
}
function collectExplicitTestFileArgs(argv: string[]): string[] {
return collectExplicitFileTargetArgs(argv, isExplicitTestFileArg);
}
/**
* Forces explicit test-file targets to fail when Vitest finds no matching tests.
*/
export async function resolveExplicitTestFileNoPassArgs(argv: string[]): Promise<string[]> {
if (collectExplicitTestFileArgs(argv).length === 0) {
return argv;
}
const policyArgs: string[] = [];
const spellings = new Set(["passWithNoTests"]);
for (const [index, arg] of argv.entries()) {
if (arg === "--") {
break;
}
const spelling = /^(?:--(?!-)(?:no-)?|-+no-)([^=.]+)/u.exec(arg)?.[1];
if (
!spelling ||
spelling.replace(/([a-z])-([a-z])/gu, (_, a: string, b: string) => a + b.toUpperCase()) !==
"passWithNoTests"
) {
continue;
}
policyArgs.push(arg);
spellings.add(spelling);
const value = argv[index + 1];
if (arg.replace(/[=]$/u, "") === `--${spelling}` && (value === "true" || value === "false")) {
policyArgs.push(value);
}
}
if (policyArgs.length) {
const { parseCLI } = await import("vitest/node");
// Validate only the scalar policy can overwrite; the child owns help/version
// and unrelated errors. parseCLI mutates its input, so always give it a copy.
parseCLI(["vitest", ...policyArgs], { allowUnknownOptions: true });
}
// CAC camelcases after parsing. Negate every raw spelling so a later alias
// cannot overwrite strict policy, without erasing caller scalar errors.
return insertVitestTargets(
argv,
Array.from(spellings, (name) => `--no-${name}`),
);
}
function hasAlternateVitestRootArg(argv: string[]): boolean {
return argv.some(
(arg) =>
arg === "--root" ||
arg === "-r" ||
arg === "--dir" ||
arg.startsWith("--root=") ||
arg.startsWith("--dir="),
);
}
function hasExplicitVitestProjectArg(argv: string[]): boolean {
return argv.some((arg) => arg === "--project" || arg.startsWith("--project="));
}
function hasExplicitDisabledRunFlag(argv: string[]): boolean {
for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index];
if (arg === undefined) {
break;
}
if (arg === "--") {
break;
}
const runFlag = resolveBooleanModeFlag(argv, index, "run");
if (!runFlag) {
if (vitestOptionConsumesNextArg(arg, argv[index + 1])) {
index += 1;
}
continue;
}
if (runFlag.consumedNext) {
index += 1;
}
if (!runFlag.value) {
return true;
}
}
return false;
}
function resolveDelegatedVitestArgs(argv: string[]): string[] {
const positionalArgs: string[] = [];
const optionArgs: string[] = [];
let canRemoveRunSubcommand = true;
let passthrough = false;
for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index];
if (arg === undefined) {
break;
}
if (arg === "--") {
passthrough = true;
canRemoveRunSubcommand = false;
continue;
}
if (passthrough) {
optionArgs.push(arg);
continue;
}
if (vitestOptionConsumesNextArg(arg, argv[index + 1])) {
optionArgs.push(arg);
const optionValue = argv[index + 1];
if (optionValue !== undefined) {
optionArgs.push(optionValue);
index += 1;
}
continue;
}
if (arg.startsWith("-")) {
optionArgs.push(arg);
continue;
}
if (canRemoveRunSubcommand && arg === "run") {
canRemoveRunSubcommand = false;
continue;
}
canRemoveRunSubcommand = false;
positionalArgs.push(arg);
}
return optionArgs.length > 0 ? [...positionalArgs, "--", ...optionArgs] : positionalArgs;
}
/**
* Delegates explicit path runs to the repo test-projects runner.
*/
export function resolveTestProjectsDelegationArgs(
argv: string[],
cwd = process.cwd(),
): string[] | null {
if (
hasExplicitVitestConfigArg(argv) ||
hasAlternateVitestRootArg(argv) ||
hasExplicitVitestProjectArg(argv) ||
resolveExplicitVitestMode(argv) === "watch" ||
hasNonRunVitestSubcommand(argv) ||
hasExplicitDisabledRunFlag(argv) ||
collectExplicitProjectRouterTargetArgs(argv, cwd).length === 0
) {
return null;
}
return resolveDelegatedVitestArgs(argv);
}
/**
* Lists explicit test file targets missing from the current checkout.
*/
export function resolveMissingExplicitTestFiles(
argv: string[],
cwd = process.cwd(),
fsImpl: { existsSync(filePath: string): boolean } = fs,
): string[] {
if (hasExplicitVitestConfigArg(argv) || hasAlternateVitestRootArg(argv)) {
return [];
}
return collectExplicitFileTargetArgs(argv)
.filter((arg) => {
const filePath = path.isAbsolute(arg) ? arg : path.resolve(cwd, arg);
return !fsImpl.existsSync(filePath);
})
.map((arg) => toRepoRelativeArg(arg, cwd));
}
function toRepoRelativeArg(arg: string, cwd: string): string {
const normalized = path.isAbsolute(arg) ? path.relative(cwd, arg) : arg;
return normalized.replaceAll(path.sep, "/").replace(/^\.\//u, "");
}
function withImplicitVitestConfig(argv: string[], config: string): string[] {
if (argv[0] === "run") {
return ["run", "--config", config, ...argv.slice(1)];
}
return ["--config", config, ...argv];
}
function isToolingTestTarget(target: string): boolean {
return (
target.startsWith("test/") && target.endsWith(".test.ts") && !TOOLING_EXCLUDED_TESTS.has(target)
);
}
function isToolingDockerTestTarget(target: string): boolean {
return target === "test/scripts/docker-build-helper.test.ts";
}
/**
* Resolves config defaults and explicit-file handling for wrapper-inferred runs.
*/
export function resolveImplicitVitestArgs(argv: string[], cwd = process.cwd()): string[] {
if (hasExplicitVitestConfigArg(argv)) {
return argv;
}
const separatorIndex = argv.indexOf("--");
const optionArgs = separatorIndex < 0 ? argv : argv.slice(0, separatorIndex);
const hasExplicitIsolation = optionArgs.some(
(arg) => arg === "--isolate" || arg === "--no-isolate" || arg.startsWith("--isolate="),
);
if (!hasExplicitIsolation && collectExplicitDirectoryTargetArgs(argv, cwd).length > 1) {
// Mixed directory selectors can activate overlapping Vitest projects.
// Isolate their module caches so one project's mocks cannot poison another.
const resolved = [...argv];
resolved.splice(separatorIndex < 0 ? resolved.length : separatorIndex, 0, "--isolate");
return resolved;
}
if (collectExplicitDirectoryTargetArgs(argv, cwd).length > 0) {
return argv;
}
const testTargets = collectVitestFileFilters(argv)
.filter((arg) => !arg.startsWith("-") && arg.endsWith(".test.ts"))
.map((arg) => toRepoRelativeArg(arg, cwd));
if (testTargets.length > 0 && testTargets.every(isToolingDockerTestTarget)) {
return withImplicitVitestConfig(argv, TOOLING_DOCKER_VITEST_CONFIG);
}
if (testTargets.length > 0 && testTargets.every(isToolingTestTarget)) {
return withImplicitVitestConfig(argv, TOOLING_VITEST_CONFIG);
}
// Glob selectors can span both browser owners; the root project matrix partitions them.
if (testTargets.some((target) => /[*?[\]{}]|[@+!]\(/u.test(target))) {
return argv;
}
if (testTargets.length > 0 && testTargets.every(isUiBrowserTestFile)) {
return withImplicitVitestConfig(argv, UI_BROWSER_VITEST_CONFIG);
}
if (
testTargets.length > 0 &&
testTargets.every((target) => isUiTestTarget(target) && !isUiBrowserTestFile(target))
) {
// Mixed timing/ordinary UI selection needs the root matrix to preserve groups.
if (testTargets.some((target) => uiTimingTestFiles.includes(target))) {
return argv;
}
return withImplicitVitestConfig(argv, UI_VITEST_CONFIG);
}
return argv;
}
/**
* Installs the no-output watchdog for long-running Vitest children.
*/
export function installVitestNoOutputWatchdog(params: {
streams?: Array<WatchdogStream | null>;
timeoutMs: number | null;
heartbeatMs?: number | null;
forceKillAfterMs?: number;
log?: (message: string) => void;
onTimeout?: () => void;
onForceKill?: () => void;
setTimeoutFn?: typeof setTimeout;
clearTimeoutFn?: typeof clearTimeout;
}): { recordActivity: () => void; teardown: () => void } {
const timeoutMs = params.timeoutMs;
if (!timeoutMs || timeoutMs <= 0) {
return { recordActivity: () => {}, teardown: () => {} };
}
const setTimeoutFn = params.setTimeoutFn ?? setTimeout;
const clearTimeoutFn = params.clearTimeoutFn ?? clearTimeout;
const forceKillAfterMs = params.forceKillAfterMs ?? 5_000;
const heartbeatMs =
params.heartbeatMs && params.heartbeatMs > 0 && params.heartbeatMs < timeoutMs
? params.heartbeatMs
: null;
const streams =
params.streams?.filter((stream): stream is WatchdogStream => stream !== null) ?? [];
let active = true;
let silenceTimer: ReturnType<typeof setTimeout> | null = null;
let forceKillTimer: ReturnType<typeof setTimeout> | null = null;
let heartbeatTimer: ReturnType<typeof setTimeout> | null = null;
let silentForMs = 0;
let timedOut = false;
const clearHeartbeatTimer = () => {
if (heartbeatTimer !== null) {
clearTimeoutFn(heartbeatTimer);
heartbeatTimer = null;
}
};
const clearForceKillTimer = () => {
if (forceKillTimer !== null) {
clearTimeoutFn(forceKillTimer);
forceKillTimer = null;
}
};
const clearSilenceTimer = () => {
if (silenceTimer !== null) {
clearTimeoutFn(silenceTimer);
silenceTimer = null;
}
};
const scheduleHeartbeatTimer = () => {
if (!active || heartbeatMs === null) {
return;
}
clearHeartbeatTimer();
heartbeatTimer = setTimeoutFn(() => {
if (!active) {
return;
}
silentForMs += heartbeatMs;
params.log?.(`[vitest] still running with no output for ${silentForMs}ms.`);
if (silentForMs + heartbeatMs < timeoutMs) {
scheduleHeartbeatTimer();
}
}, heartbeatMs);
};
const resetSilenceTimer = () => {
if (!active) {
return;
}
clearSilenceTimer();
silentForMs = 0;
scheduleHeartbeatTimer();
silenceTimer = setTimeoutFn(() => {
if (!active) {
return;
}
clearHeartbeatTimer();
timedOut = true;
params.log?.(
`[vitest] no output for ${timeoutMs}ms; terminating stalled Vitest process group.`,
);
if (forceKillAfterMs > 0) {
clearForceKillTimer();
forceKillTimer = setTimeoutFn(() => {
if (!active) {
return;
}
params.log?.(
`[vitest] process group still alive after ${forceKillAfterMs}ms; sending SIGKILL.`,
);
params.onForceKill?.();
}, forceKillAfterMs);
}
params.onTimeout?.();
}, timeoutMs);
};
const handleActivity = () => {
if (timedOut) {
return;
}
clearForceKillTimer();
resetSilenceTimer();
};
const listeners = streams.map((stream) => {
const handler = () => {
handleActivity();
};
stream.on("data", handler);
return { stream, handler };
});
resetSilenceTimer();
return {
recordActivity: handleActivity,
teardown() {
if (!active) {
return;
}
active = false;
clearSilenceTimer();
clearForceKillTimer();
clearHeartbeatTimer();
for (const { stream, handler } of listeners) {
stream.off("data", handler);
}
},
};
}
/**
* Forwards child output while optionally suppressing complete stderr lines.
*/
function forwardVitestOutput(
stream: VitestOutputStream | null,
target: VitestOutputTarget,
shouldSuppressLine: (line: string) => boolean = () => false,
observeLine: (line: string) => void = () => {},
): Promise<void> {
if (!stream) {
return Promise.resolve();
}
let buffered = "";
const forwardLine = (line: string) => {
if (shouldSuppressLine(line)) {
return;
}
observeLine(line);
target.write(line);
};
stream.setEncoding("utf8");
stream.on("data", (chunk) => {
buffered += chunk;
while (true) {
const newlineIndex = buffered.indexOf("\n");
if (newlineIndex === -1) {
break;
}
const line = buffered.slice(0, newlineIndex + 1);
buffered = buffered.slice(newlineIndex + 1);
forwardLine(line);
}
});
return new Promise((resolve) => {
stream.on("end", () => {
if (buffered.length > 0) {
forwardLine(buffered);
}
resolve();
});
});
}
/**
* Joins watched Vitest processes and keeps expired deadlines failed after cooperative exits.
*/
export function spawnWatchedVitestProcess({
pnpmArgs = [],
nativeBunFiles,
spawnParams,
env,
onNoOutputTimeout,
workerRun,
homeMode = nativeBunFiles
? "hermetic"
: resolveVitestHomeSelection(pnpmArgs, { cwd: spawnParams.cwd, env }),
}: {
spawnParams: PnpmRunnerParams;
env: NodeJS.ProcessEnv;
onNoOutputTimeout?: () => void;
} & (
| {
pnpmArgs: string[];
nativeBunFiles?: never;
workerRun?: VitestWorkerRun;
homeMode?: Parameters<typeof spawnOwnedVitestProcess>[0]["homeMode"];
}
| {
nativeBunFiles: string[];
pnpmArgs?: never;
workerRun?: never;
homeMode?: "hermetic";
}
)) {
if (homeMode !== "tooling") {
assertTestHomeSelection(env, homeMode);
}
let timeoutCompletion: Promise<boolean> | null = null;
const directNodeArgs = resolveDirectNodeVitestArgs(pnpmArgs);
const testCommand = nativeBunFiles
? resolveNativeBunTestCommand(
validateNativeBunTestFiles(nativeBunFiles, spawnParams.cwd ?? process.cwd()),
)
: directNodeArgs
? resolveVitestTestCommand(directNodeArgs, env)
: undefined;
if (workerRun && testCommand) {
// Give either runtime the same owned compiled-subprocess generation.
const cliIndex = testCommand.args.findIndex((arg) => path.basename(arg) === "vitest.mjs");
if (cliIndex < 0) {
throw new Error("Compiled subprocess owner requires a native Vitest CLI spec");
}
testCommand.args.splice(
cliIndex,
0,
path.join(resolveRepoRoot(import.meta.url), "scripts/lib/vitest-worker-bootstrap.mts"),
workerRun.descriptor.directory,
);
}
const childSpawnParams: PnpmRunnerParams = workerRun
? {
...spawnParams,
stdio: [
...(Array.isArray(spawnParams.stdio)
? spawnParams.stdio
: [
spawnParams.stdio ?? "pipe",
spawnParams.stdio ?? "pipe",
spawnParams.stdio ?? "pipe",
]),
"ipc",
],
}
: spawnParams;
const { child, completion: childCompletion } = spawnOwnedVitestProcess({
...(testCommand
? {
...testCommand,
options: testCommand.envOverrides
? {
...childSpawnParams,
env: {
...(childSpawnParams.env ?? process.env),
...testCommand.envOverrides,
},
}
: childSpawnParams,
}
: createPnpmRunnerSpawnSpec({ pnpmArgs, ...childSpawnParams })),
homeMode,
});
const childCleanup = installVitestProcessGroupCleanup({
child,
forceSignal: "SIGKILL",
forceSignalDelayMs: 100,
});
const noOutputWatchdog = installVitestNoOutputWatchdog({
streams: [child.stdout, child.stderr],
timeoutMs: resolveVitestNoOutputTimeoutMs(env),
heartbeatMs: resolveVitestNoOutputHeartbeatMs(env),
log: (message) => {
console.error(message);
},
onTimeout: () => {
const termination = terminateVitestProcessGroupForTimeout({
child,
kill: process.kill.bind(process),
log: (message) => {
console.error(message);
},
onTimeout: onNoOutputTimeout,
});
timeoutCompletion = termination.diagnostics.then(() => true);
},
onForceKill: () => {
forwardSignalToVitestProcessGroup({
child,
signal: "SIGKILL",
kill: process.kill.bind(process),
});
},
});
const unhandledErrors = createVitestUnhandledErrorDetector();
const forwardedOutput = Promise.all([
forwardVitestOutput(child.stdout, process.stdout, undefined, unhandledErrors.observe),
forwardVitestOutput(
child.stderr,
process.stderr,
shouldSuppressVitestStderrLine,
unhandledErrors.observe,
),
]);
const teardown = () => {
childCleanup.teardown();
noOutputWatchdog.teardown();
};
const completion = Promise.all([childCompletion, forwardedOutput])
.then(async ([{ code: childCode, signal, groupJoined }]) => {
const code = (await timeoutCompletion) && childCode === 0 ? 1 : childCode;
const result = unhandledErrors.finish();
if (result) {
writeVitestUnhandledErrorSummary(result, env);
}
return { code, signal: normalizeNodeSignal(signal), groupJoined };
})
.finally(teardown);
return {
child,
completion: workerRun
? workerRun.borrow(child, completion, noOutputWatchdog.recordActivity)
: completion,
getForwardedSignal: childCleanup.getForwardedSignal,
teardown,
};
}
export async function runVitest(
exitBySignal: typeof exitVitestBySignal,
argv: string[] = process.argv.slice(2),
env: NodeJS.ProcessEnv = process.env,
): Promise<void> {
if (argv[0] === "--native-bun") {
if (env.OPENCLAW_NATIVE_BUN_PARENT_IPC === "1" && !process.connected) {
throw new Error("Native Bun test parent IPC disconnected before admission");
}
const spawnEnv = resolveRunVitestSpawnEnv(env, ["run"]);
let watched: ReturnType<typeof spawnWatchedVitestProcess> | undefined;
let interrupted: NodeJS.Signals | undefined;
const onSignal = (signal: NodeJS.Signals) => {
interrupted ??= signal;
};
const onParentDisconnect = () => {
interrupted ??= "SIGTERM";
process.kill(process.pid, "SIGTERM");
};
process.on("SIGINT", onSignal);
process.on("SIGTERM", onSignal);
if (process.connected) {
process.once("disconnect", onParentDisconnect);
process.channel?.unref();
}
try {
watched = spawnWatchedVitestProcess({
nativeBunFiles: argv.slice(1),
spawnParams: resolveVitestSpawnParams(spawnEnv),
env: spawnEnv,
});
const result = await watched.completion;
interrupted ??= result.signal ?? undefined;
process.exitCode = result.code ?? 1;
} finally {
const signal = interrupted ?? watched?.getForwardedSignal();
process.off("disconnect", onParentDisconnect);
process.off("SIGINT", onSignal);
process.off("SIGTERM", onSignal);
if (signal) {
await exitBySignal(signal);
}
}
return;
}
if (argv.some((arg) => arg === "--isolated-image" || arg.startsWith("--isolated-image="))) {
const { parseIsolatedVitestArgs, runIsolatedVitest } =
await import("./lib/vitest-isolated.mts");
const isolated = parseIsolatedVitestArgs(argv);
if (isolated) {
process.exitCode = await runIsolatedVitest(
resolveRepoRoot(import.meta.url),
isolated.image,
isolated.args,
env,
);
return;
}
}
if (argv.length === 0) {
console.error("usage: node scripts/run-vitest.mjs <vitest args...>");
process.exitCode = 1;
return;
}
const missingTestFiles = resolveMissingExplicitTestFiles(argv);
if (missingTestFiles.length > 0) {
console.error(
[
"[vitest] explicit test/source file(s) not found:",
...missingTestFiles.map((file) => ` - ${file}`),
].join("\n"),
);
process.exitCode = 1;
return;
}
const delegatedArgs = resolveTestProjectsDelegationArgs(argv);
if (delegatedArgs) {
const { runTestProjects } = await import("./test-projects-run.mts");
return runTestProjects(exitBySignal, delegatedArgs, env);
}
const vitestArgs = resolveImplicitVitestArgs(argv);
assertTestHomeSelection(env, resolveVitestHomeSelection(vitestArgs, { env }));
const repoRoot = resolveRepoRoot(import.meta.url);
let vitestCliEntry;
try {
// Resolution owns hydrated-module linking and missing-dependency guidance;
// it must finish before the execution parser imports Vitest.
vitestCliEntry = resolveVitestCliEntry({ baseDir: repoRoot });
} catch (error) {
if (isErrorWithCode(error, "OPENCLAW_MISSING_VITEST")) {
console.error(error.message);
process.exitCode = 1;
return;
}
throw error;
}
const { parseCLI } = await import("vitest/node");
const execution = parseVitestExecutionArgs(vitestArgs, parseCLI);
const invocations = execution
? resolveBoundedVitestInvocations(vitestArgs, { env })
: [vitestArgs];
const sourceMode =
!execution || execution.options.watch || resolveExplicitVitestMode(vitestArgs) === "watch";
const config = resolveVitestConfigArg(vitestArgs);
const relativeConfig = config
? toRepoRelativeArg(path.resolve(config), repoRoot)
: config === null && !sourceMode && process.cwd() === repoRoot
? "vitest.config.ts"
: "";
const invocationEnv =
invocations.length > 1 && relativeConfig === E2E_VITEST_CONFIG
? { ...env, ...(await prepareE2eVitestRuntime(env)) }
: env;
// Canonical configs have known project scopes. Custom roots/projects keep
// their own setup; never infer their runtime selection from a config name.
const canonicalSelection =
execution &&
!hasAlternateVitestRootArg(vitestArgs) &&
!hasExplicitVitestProjectArg(vitestArgs) &&
!hasNonRunVitestSubcommand(vitestArgs) &&
!hasExplicitDisabledRunFlag(vitestArgs);
if (canonicalSelection && relativeConfig) {
const code = await prepareVitestRuntime(
invocations.flatMap((cliArgs) =>
resolveVitestRuntimeCliSelections(relativeConfig, cliArgs, invocationEnv),
),
invocationEnv,
);
if (code !== 0) {
process.exitCode = code;
return;
}
}
const workers = sourceMode
? undefined
: createVitestWorkerRun(resolveVitestProcessEnv(invocationEnv));
const withCacheSlot = createVitestCacheSlots();
let interrupted: NodeJS.Signals | undefined;
let preparingWorkers = false;
const onSignal = (signal: NodeJS.Signals) => {
interrupted ??= signal;
if (preparingWorkers) {
// No borrower can finish admission yet; cancel through the existing owner.
void workers?.dispose().catch(() => {});
}
};
// The invocation outlives child-scoped handlers when admission or final
// verification is still reading. Retain signal ownership through disposal.
process.on("SIGINT", onSignal);
process.on("SIGTERM", onSignal);
try {
if (
workers &&
canonicalSelection &&
config &&
invocations.some((args) =>
shouldPrepareVitestCoreWorkers(relativeConfig, args, invocationEnv),
)
) {
preparingWorkers = true;
try {
await workers.prepare();
} finally {
preparingWorkers = false;
}
}
if (interrupted) {
return;
}
let failedExitCode = 0;
for (const [index, invocation] of invocations.entries()) {
const guardedVitestArgs = await resolveExplicitTestFileNoPassArgs(invocation);
const spawnEnv = resolveRunVitestSpawnEnv(invocationEnv, guardedVitestArgs);
if (invocations.length > 1) {
console.error("[vitest] bounded process " + (index + 1) + "/" + invocations.length);
}
const { code, signal } = await withCacheSlot(
{
config: config ?? "",
env: spawnEnv,
watchMode: sourceMode,
...(!sourceMode &&
config &&
!spawnEnv.OPENCLAW_VITEST_FS_MODULE_CACHE_PATH?.trim() &&
!hasVitestOption(guardedVitestArgs, "--fsModuleCachePath")
? {
cacheAssignment: {
kind: "scheduler" as const,
root: resolveVitestCacheRoot(spawnEnv),
},
}
: {}),
},
async ({ env: cacheEnv }) => {
const handle = spawnWatchedVitestProcess({
workerRun: workers,
pnpmArgs: [
"exec",
"node",
...resolveVitestNodeArgs(invocationEnv),
vitestCliEntry,
...guardedVitestArgs,
],
spawnParams: resolveVitestSpawnParams(cacheEnv),
env: cacheEnv,
});
const result = await handle.completion;
return { ...result, signal: handle.getForwardedSignal() ?? result.signal };
},
);
interrupted ??= signal ?? undefined;
const exitCode = code ?? 1;
// Ordinary test failures must not hide later files; interruptions stop
// admission and are re-raised only after the invocation has been disposed.
if (interrupted || (exitCode !== 0 && exitCode !== 1)) {
process.exitCode = exitCode;
return;
}
failedExitCode ||= exitCode;
}
process.exitCode = failedExitCode;
} finally {
try {
await workers?.dispose().catch((error: unknown) => {
process.exitCode ||= 1;
console.error(error);
});
} finally {
process.off("SIGINT", onSignal);
process.off("SIGTERM", onSignal);
if (interrupted) {
await exitBySignal(interrupted);
}
}
}
}
if (import.meta.main) {
// The project owner imports our spawn helpers; top-level await would deadlock
// its dynamic import when this module is also the native entrypoint.
void runVitestCli("vitest", runVitest);
}