Skip to content

Commit 505aca9

Browse files
committed
fix(test): reject missing explicit vitest files
1 parent 5174d97 commit 505aca9

2 files changed

Lines changed: 202 additions & 0 deletions

File tree

scripts/run-vitest.mjs

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { spawn } from "node:child_process";
2+
import fs from "node:fs";
23
import { createRequire } from "node:module";
34
import path from "node:path";
45
import { isUiTestTarget, isUnitUiTestTarget } from "../test/vitest/vitest.ui-paths.mjs";
@@ -16,6 +17,60 @@ const ANSI_CSI_SUFFIX_RE = /^[0-?]*[ -/]*[@-~]/u;
1617
const SUPPRESSED_VITEST_STDERR_PATTERNS = ["[PLUGIN_TIMINGS]"];
1718
const UI_VITEST_CONFIG = "test/vitest/vitest.ui.config.ts";
1819
const UNIT_UI_VITEST_CONFIG = "test/vitest/vitest.unit-ui.config.ts";
20+
const EXPLICIT_TEST_FILE_RE = /\.(?:test|e2e|live)\.(?:[cm]?[jt]sx?)$/u;
21+
const GLOB_PATTERN_CHARS_RE = /[*?[\]{}]/u;
22+
const VITEST_OPTIONS_WITH_VALUE = new Set([
23+
"--attachmentsDir",
24+
"--bail",
25+
"--browser",
26+
"--config",
27+
"--configLoader",
28+
"-c",
29+
"--changed",
30+
"--dir",
31+
"--environment",
32+
"--exclude",
33+
"--execArgv",
34+
"--hookTimeout",
35+
"--inspect",
36+
"--inspect-brk",
37+
"--listTags",
38+
"--maxConcurrency",
39+
"--maxWorkers",
40+
"--mergeReports",
41+
"--mode",
42+
"--outputFile",
43+
"--pool",
44+
"--project",
45+
"--reporter",
46+
"--reporters",
47+
"--retry",
48+
"--root",
49+
"-r",
50+
"--sequence.shuffle.seed",
51+
"--shard",
52+
"--silent",
53+
"--slowTestThreshold",
54+
"--tagsFilter",
55+
"--teardownTimeout",
56+
"--testNamePattern",
57+
"-t",
58+
"--testTimeout",
59+
"--update",
60+
"-u",
61+
"--vmMemoryLimit",
62+
]);
63+
const VITEST_DOTTED_OPTIONS_WITH_VALUE_PREFIXES = [
64+
"--browser.",
65+
"--coverage.",
66+
"--diff.",
67+
"--expect.",
68+
"--experimental.",
69+
"--outputFile.",
70+
"--retry.",
71+
"--sequence.",
72+
"--typecheck.",
73+
];
1974
const require = createRequire(import.meta.url);
2075

2176
function isTruthyEnvValue(value) {
@@ -99,6 +154,69 @@ function hasExplicitVitestConfigArg(argv) {
99154
return argv.some((arg) => arg === "--config" || arg === "-c" || arg.startsWith("--config="));
100155
}
101156

157+
function optionConsumesNextArg(arg) {
158+
if (arg.includes("=")) {
159+
return false;
160+
}
161+
return (
162+
VITEST_OPTIONS_WITH_VALUE.has(arg) ||
163+
VITEST_DOTTED_OPTIONS_WITH_VALUE_PREFIXES.some((prefix) => arg.startsWith(prefix))
164+
);
165+
}
166+
167+
function isExplicitTestFileArg(arg) {
168+
if (!EXPLICIT_TEST_FILE_RE.test(arg) || GLOB_PATTERN_CHARS_RE.test(arg)) {
169+
return false;
170+
}
171+
return (
172+
path.isAbsolute(arg) || arg.startsWith("./") || arg.startsWith("../") || /[/\\]/u.test(arg)
173+
);
174+
}
175+
176+
function collectExplicitTestFileArgs(argv) {
177+
const files = [];
178+
for (let index = 0; index < argv.length; index += 1) {
179+
const arg = argv[index];
180+
if (arg === "--") {
181+
break;
182+
}
183+
if (optionConsumesNextArg(arg)) {
184+
index += 1;
185+
continue;
186+
}
187+
if (arg.startsWith("-")) {
188+
continue;
189+
}
190+
if (isExplicitTestFileArg(arg)) {
191+
files.push(arg);
192+
}
193+
}
194+
return files;
195+
}
196+
197+
function hasAlternateVitestRootArg(argv) {
198+
return argv.some(
199+
(arg) =>
200+
arg === "--root" ||
201+
arg === "-r" ||
202+
arg === "--dir" ||
203+
arg.startsWith("--root=") ||
204+
arg.startsWith("--dir="),
205+
);
206+
}
207+
208+
export function resolveMissingExplicitTestFiles(argv, cwd = process.cwd(), fsImpl = fs) {
209+
if (hasExplicitVitestConfigArg(argv) || hasAlternateVitestRootArg(argv)) {
210+
return [];
211+
}
212+
return collectExplicitTestFileArgs(argv)
213+
.filter((arg) => {
214+
const filePath = path.isAbsolute(arg) ? arg : path.resolve(cwd, arg);
215+
return !fsImpl.existsSync(filePath);
216+
})
217+
.map((arg) => toRepoRelativeArg(arg, cwd));
218+
}
219+
102220
function toRepoRelativeArg(arg, cwd) {
103221
const normalized = path.isAbsolute(arg) ? path.relative(cwd, arg) : arg;
104222
return normalized.replaceAll(path.sep, "/").replace(/^\.\//u, "");
@@ -309,6 +427,17 @@ function main(argv = process.argv.slice(2), env = process.env) {
309427
process.exit(1);
310428
}
311429

430+
const missingTestFiles = resolveMissingExplicitTestFiles(argv);
431+
if (missingTestFiles.length > 0) {
432+
console.error(
433+
[
434+
"[vitest] explicit test file(s) not found:",
435+
...missingTestFiles.map((file) => ` - ${file}`),
436+
].join("\n"),
437+
);
438+
process.exit(1);
439+
}
440+
312441
const vitestArgs = resolveImplicitVitestArgs(argv);
313442
const { child, teardown } = spawnWatchedVitestProcess({
314443
pnpmArgs: [

test/scripts/run-vitest.test.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
installVitestNoOutputWatchdog,
55
resolveDirectNodeVitestArgs,
66
resolveImplicitVitestArgs,
7+
resolveMissingExplicitTestFiles,
78
resolveVitestNodeArgs,
89
resolveVitestNoOutputTimeoutMs,
910
resolveVitestSpawnParams,
@@ -52,6 +53,78 @@ describe("scripts/run-vitest", () => {
5253
expect(resolveImplicitVitestArgs(argv)).toBe(argv);
5354
});
5455

56+
it("reports missing explicit test files before Vitest can silently ignore them", () => {
57+
const fsImpl = {
58+
existsSync: (filePath: string) => filePath.endsWith("src/agents/bash-tools.test.ts"),
59+
};
60+
61+
expect(
62+
resolveMissingExplicitTestFiles(
63+
[
64+
"src/agents/bash-tools.test.ts",
65+
"test/agents/bash-tools.exec.background-abort.test.ts",
66+
],
67+
"/repo",
68+
fsImpl,
69+
),
70+
).toEqual(["test/agents/bash-tools.exec.background-abort.test.ts"]);
71+
});
72+
73+
it("does not treat option values or glob patterns as explicit missing files", () => {
74+
const fsImpl = {
75+
existsSync: () => false,
76+
};
77+
78+
expect(
79+
resolveMissingExplicitTestFiles(
80+
[
81+
"-t",
82+
"missing.test.ts",
83+
"basename-filter.test.ts",
84+
"src/**/*.test.ts",
85+
"--config",
86+
"missing.config.ts",
87+
"--exclude",
88+
"ignored.test.ts",
89+
"--bail",
90+
"1",
91+
"--mode",
92+
"test",
93+
"--mergeReports",
94+
"reports.test.ts",
95+
"--coverage.exclude",
96+
"coverage.test.ts",
97+
],
98+
"/repo",
99+
fsImpl,
100+
),
101+
).toEqual([]);
102+
});
103+
104+
it("skips missing-file preflight when Vitest controls path resolution", () => {
105+
const fsImpl = {
106+
existsSync: () => false,
107+
};
108+
109+
expect(
110+
resolveMissingExplicitTestFiles(
111+
["--config", "test/vitest/vitest.gateway.config.ts", "server/health-state.test.ts"],
112+
"/repo",
113+
fsImpl,
114+
),
115+
).toEqual([]);
116+
expect(
117+
resolveMissingExplicitTestFiles(
118+
["--root", "packages/example", "src/example.test.ts"],
119+
"/repo",
120+
fsImpl,
121+
),
122+
).toEqual([]);
123+
expect(
124+
resolveMissingExplicitTestFiles(["--dir=src", "example.test.ts"], "/repo", fsImpl),
125+
).toEqual([]);
126+
});
127+
55128
it("keeps the run subcommand first when routing unit ui tests", () => {
56129
expect(resolveImplicitVitestArgs(["run", "ui/src/ui/controllers/chat.test.ts"])).toEqual([
57130
"run",

0 commit comments

Comments
 (0)