fix: route explicit vitest files through project runner (#88127)

This commit is contained in:
Dallin Romney
2026-05-29 20:38:52 -07:00
committed by GitHub
parent 1659b26151
commit 7de025eacd
7 changed files with 250 additions and 1 deletions

View File

@@ -75,7 +75,9 @@ OPENCLAW_VITEST_MAX_WORKERS=1 pnpm test <path-or-filter>
```
Use targeted file paths whenever possible. Avoid raw `vitest`; use the repo
`pnpm test` wrapper so project routing, workers, and setup stay correct.
`pnpm test` wrapper so project routing, workers, and setup stay correct. If raw
Vitest is unavoidable, use `vitest run ...`; bare `vitest ...` starts local watch
mode and will not exit on its own.
When the checkout is a Codex worktree, prefer the direct node harness instead:
```bash

View File

@@ -93,6 +93,7 @@ Skills own workflows; root owns hard policy and routing.
- Install: `pnpm install` (keep Bun lock/patches aligned if touched).
- CLI: `pnpm openclaw ...` or `pnpm dev`; build: `pnpm build`.
- Tests in a normal source checkout: `pnpm test <path-or-filter> [vitest args...]`, `pnpm test:changed`, `pnpm test:serial`, `pnpm test:coverage`; never raw `vitest`.
- If raw Vitest is unavoidable, use `vitest run ...`; bare `vitest ...` starts local watch mode and will not exit on its own.
- Tests in a Codex worktree or linked/sparse checkout: avoid direct local `pnpm test*`; use `node scripts/run-vitest.mjs <path-or-filter>` for tiny explicit-file proof, or Crabbox/Testbox for anything broader.
- Checks in a normal source checkout: `pnpm check:changed`; lanes: `pnpm changed:lanes --json`; staged: `pnpm check:changed --staged`; full: `pnpm check`.
- Checks in a Codex worktree or linked/sparse checkout: avoid direct local `pnpm check*`; use `node scripts/crabbox-wrapper.mjs run ... --shell -- "pnpm check:changed"` so pnpm runs inside Testbox, not locally.

View File

@@ -6,6 +6,7 @@ This directory owns local tooling, script wrappers, and generated-artifact helpe
- Prefer existing wrappers over raw tool entrypoints when the repo already has a curated seam.
- For tests, prefer `scripts/run-vitest.mjs` or the root `pnpm test ...` entrypoints over raw `vitest run` calls.
- Never use bare `vitest ...` in automation; it starts local watch mode unless `run` or `--run` is explicit.
- For lint/typecheck flows, prefer `scripts/run-oxlint.mjs` and `scripts/run-tsgo.mjs` when adding or editing package scripts or CI steps that should honor repo-local runtime behavior.
- For changed-file verification, prefer `scripts/check-changed.mjs` and keep lane classification in `scripts/changed-lanes.mjs`. Do not copy path-scope rules into new hooks or ad hoc CI snippets.

View File

@@ -81,6 +81,7 @@ const VITEST_DOTTED_OPTIONS_WITH_VALUE_PREFIXES = [
];
const require = createRequire(import.meta.url);
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const testProjectsRunnerPath = path.join(repoRoot, "scripts", "test-projects.mjs");
function isTruthyEnvValue(value) {
return TRUTHY_ENV_VALUES.has(value?.trim().toLowerCase() ?? "");
@@ -348,6 +349,93 @@ function hasAlternateVitestRootArg(argv) {
);
}
function hasExplicitVitestProjectArg(argv) {
return argv.some((arg) => arg === "--project" || arg.startsWith("--project="));
}
function hasExplicitDisabledRunFlag(argv) {
for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index];
if (arg === "--") {
break;
}
const runFlag = resolveBooleanModeFlag(argv, index, "run");
if (!runFlag) {
if (optionConsumesNextArg(arg)) {
index += 1;
}
continue;
}
if (runFlag.consumedNext) {
index += 1;
}
if (!runFlag.value) {
return true;
}
}
return false;
}
function hasSeparateVitestOptionValueArg(argv) {
for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index];
if (arg === "--") {
return false;
}
if (optionConsumesNextArg(arg)) {
return true;
}
}
return false;
}
function stripRunSubcommand(argv) {
const stripped = [];
let canRemoveRunSubcommand = true;
for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index];
if (arg === "--") {
stripped.push(arg);
canRemoveRunSubcommand = false;
continue;
}
if (canRemoveRunSubcommand && optionConsumesNextArg(arg)) {
stripped.push(arg);
if (index + 1 < argv.length) {
index += 1;
stripped.push(argv[index]);
}
continue;
}
if (canRemoveRunSubcommand && arg.startsWith("-")) {
stripped.push(arg);
continue;
}
if (canRemoveRunSubcommand && arg === "run") {
canRemoveRunSubcommand = false;
continue;
}
canRemoveRunSubcommand = false;
stripped.push(arg);
}
return stripped;
}
export function resolveTestProjectsDelegationArgs(argv) {
if (
hasExplicitVitestConfigArg(argv) ||
hasAlternateVitestRootArg(argv) ||
hasExplicitVitestProjectArg(argv) ||
resolveExplicitVitestMode(argv) === "watch" ||
hasExplicitDisabledRunFlag(argv) ||
hasSeparateVitestOptionValueArg(argv) ||
collectExplicitTestFileArgs(argv).length === 0
) {
return null;
}
return stripRunSubcommand(argv);
}
export function resolveMissingExplicitTestFiles(argv, cwd = process.cwd(), fsImpl = fs) {
if (hasExplicitVitestConfigArg(argv) || hasAlternateVitestRootArg(argv)) {
return [];
@@ -573,6 +661,17 @@ export function spawnWatchedVitestProcess({
};
}
export function resolveTestProjectsRunnerEnv(env) {
return resolveVitestSpawnEnv(env);
}
function spawnTestProjectsRunner(argv, env) {
return spawn(process.execPath, [testProjectsRunnerPath, ...argv], {
env: resolveTestProjectsRunnerEnv(env),
stdio: "inherit",
});
}
function main(argv = process.argv.slice(2), env = process.env) {
if (argv.length === 0) {
console.error("usage: node scripts/run-vitest.mjs <vitest args...>");
@@ -590,6 +689,23 @@ function main(argv = process.argv.slice(2), env = process.env) {
process.exit(1);
}
const delegatedArgs = resolveTestProjectsDelegationArgs(argv);
if (delegatedArgs) {
const child = spawnTestProjectsRunner(delegatedArgs, env);
child.on("exit", (code, signal) => {
if (signal) {
process.kill(process.pid, signal);
return;
}
process.exit(code ?? 1);
});
child.on("error", (error) => {
console.error(error);
process.exit(1);
});
return;
}
const vitestArgs = resolveImplicitVitestArgs(argv);
const guardedVitestArgs = resolveExplicitTestFileNoPassArgs(vitestArgs);
const spawnEnv = resolveRunVitestSpawnEnv(env, guardedVitestArgs);

View File

@@ -1740,9 +1740,20 @@ export function parseTestProjectsArgs(args, cwd = process.cwd()) {
const forwardedArgs = [];
const targetArgs = [];
let watchMode = false;
let passthrough = false;
for (const arg of args) {
if (arg === "--") {
if (targetArgs.length > 0) {
passthrough = true;
}
continue;
}
if (passthrough) {
if (arg === "--watch") {
watchMode = true;
}
forwardedArgs.push(arg);
continue;
}
if (arg === "--watch") {
@@ -1874,7 +1885,9 @@ export function buildVitestRunPlans(
"utils",
"wizard",
"e2e",
"extensionActiveMemory",
"extensionAcpx",
"extensionCodex",
"extensionDiffs",
"extensionBrowser",
"extensionDiscord",

View File

@@ -8,6 +8,8 @@ import {
resolveMissingVitestDependencyMessage,
resolveMissingExplicitTestFiles,
resolveRunVitestSpawnEnv,
resolveTestProjectsDelegationArgs,
resolveTestProjectsRunnerEnv,
resolveVitestCliEntry,
resolveVitestNodeArgs,
resolveVitestNoOutputTimeoutMs,
@@ -134,6 +136,56 @@ describe("scripts/run-vitest", () => {
expect(resolveExplicitTestFileNoPassArgs(argv)).toBe(argv);
});
it("delegates bare explicit test files to the project router", () => {
const file = "test/scripts/run-vitest.test.ts";
for (const [argv, expected] of [
[[file], [file]],
[["run", file], [file]],
[
["run", file, "--reporter=verbose"],
[file, "--reporter=verbose"],
],
[
["--reporter=verbose", "run", file],
["--reporter=verbose", file],
],
[
["run", file, "--", "--watch"],
[file, "--", "--watch"],
],
[
["run", file, "--", "--reporter=verbose"],
[file, "--", "--reporter=verbose"],
],
] as const) {
expect(resolveTestProjectsDelegationArgs([...argv])).toEqual(expected);
}
});
it("keeps direct Vitest runs when project routing could change option semantics", () => {
const directArgvCases = [
[
"run",
"--config",
"test/vitest/vitest.tooling.config.ts",
"test/scripts/run-vitest.test.ts",
],
["--root", "packages/example", "src/example.test.ts"],
["--project", "tooling", "test/scripts/run-vitest.test.ts"],
["watch", "test/scripts/run-vitest.test.ts"],
["dev", "test/scripts/run-vitest.test.ts"],
["--watch", "test/scripts/run-vitest.test.ts"],
["--run=false", "test/scripts/run-vitest.test.ts"],
["--no-run", "test/scripts/run-vitest.test.ts"],
["--run", "false", "test/scripts/run-vitest.test.ts"],
["--testNamePattern", "run", "test/scripts/run-vitest.test.ts"],
["run", "test/scripts/run-vitest.test.ts", "-t", "src"],
];
for (const argv of directArgvCases) {
expect(resolveTestProjectsDelegationArgs(argv)).toBeNull();
}
});
it("reports missing explicit test files before Vitest can silently ignore them", () => {
const fsImpl = {
existsSync: (filePath: string) =>
@@ -324,6 +376,21 @@ describe("scripts/run-vitest", () => {
});
});
it("does not force the stall watchdog into delegated runner environments", () => {
expect(resolveTestProjectsRunnerEnv({ PATH: "/usr/bin" })).toEqual({
PATH: "/usr/bin",
});
expect(
resolveTestProjectsRunnerEnv({
OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS: "2500",
PATH: "/usr/bin",
}),
).toEqual({
OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS: "2500",
PATH: "/usr/bin",
});
});
it("spawns vitest in a detached process group on Unix hosts", () => {
expect(resolveVitestSpawnParams({ PATH: "/usr/bin" }, "darwin")).toEqual({
env: { PATH: "/usr/bin" },

View File

@@ -384,6 +384,33 @@ describe("scripts/test-projects changed-target routing", () => {
]);
});
it("preserves post-separator Vitest args without parsing them as targets", () => {
for (const [arg, watchMode] of [
["--reporter=verbose", false],
["--watch", true],
] as const) {
expect(buildVitestRunPlans(["test/scripts/run-vitest.test.ts", "--", arg])).toEqual([
{
config: "test/vitest/vitest.tooling.config.ts",
forwardedArgs: [arg],
includePatterns: ["test/scripts/run-vitest.test.ts"],
watchMode,
},
]);
}
});
it("keeps pnpm-style leading separators out of target routing", () => {
expect(buildVitestRunPlans(["--", "test/scripts/run-vitest.test.ts"])).toEqual([
{
config: "test/vitest/vitest.tooling.config.ts",
forwardedArgs: [],
includePatterns: ["test/scripts/run-vitest.test.ts"],
watchMode: false,
},
]);
});
it("allows explicit split Vitest config targets without treating them as unmatched tests", () => {
expect(
findUnmatchedExplicitTestTargets(
@@ -640,6 +667,28 @@ describe("scripts/test-projects changed-target routing", () => {
]);
});
it("routes explicit active-memory and Codex extension tests to their shards", () => {
expect(
buildVitestRunPlans([
"extensions/active-memory/index.test.ts",
"extensions/codex/index.test.ts",
]),
).toEqual([
{
config: "test/vitest/vitest.extension-active-memory.config.ts",
forwardedArgs: [],
includePatterns: ["extensions/active-memory/index.test.ts"],
watchMode: false,
},
{
config: "test/vitest/vitest.extension-codex.config.ts",
forwardedArgs: [],
includePatterns: ["extensions/codex/index.test.ts"],
watchMode: false,
},
]);
});
it("routes the top-level extensions target to every extension shard", () => {
expect(buildVitestRunPlans(["extensions"], process.cwd())).toEqual(
listFullExtensionVitestProjectConfigs().map((config) => ({