Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 31 additions & 4 deletions packages/cli/src/commands/review/test-efficacy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,8 +183,20 @@ describe('planTestEfficacy', () => {
describe('findVitestBin', () => {
it('names the search root when vitest cannot be resolved', () => {
const worktree = mkdtempSync(join(tmpdir(), 'no-vitest-'));
// A bare tmpdir answers "not found" only when nothing up-tree happens to
// provide vitest — a node_modules above the runner's TMPDIR (observed on
// self-hosted CI) would resolve one and the throw never fires. Inject the
// MODULE_NOT_FOUND itself so the test asks the same question on every
// host instead of depending on the ambient filesystem.
const vitestNotInstalled = () => {
const err = new Error(
"Cannot find module 'vitest/package.json'",
) as NodeJS.ErrnoException;
err.code = 'MODULE_NOT_FOUND';
throw err;
};

expect(() => findVitestBin(worktree)).toThrow(
expect(() => findVitestBin(worktree, vitestNotInstalled)).toThrow(
`vitest not found searching up from ${worktree}`,
);
});
Expand Down Expand Up @@ -314,11 +326,26 @@ describe('runControlMutant', () => {
try {
const original = 'import { it } from "vitest";\nit("t", () => {});\n';
writeFileSync(join(dir, 'a.test.ts'), original);
// No vitest resolvable from this tmpdir, so the run THROWS out of the
// suite helper — which is the point: the restore lives in a `finally`
// and has to survive that path, or the control leaves an injected
// The control throws only when the vitest run never starts. A bare
// tmpdir has no vitest only when nothing up-tree provides one — a
// node_modules above the runner's TMPDIR (observed on self-hosted CI)
// would resolve vitest and the run would actually execute. Plant a
// shadow vitest whose `exports` hides its package.json: the innermost
// node_modules wins resolution on every host, so findVitestBin surfaces
// the ERR_PACKAGE_PATH_NOT_EXPORTED and the restore's `finally` has to
// survive exactly that path — or the control leaves an injected
// always-failing test behind in a file every later mutant run uses.
// (The caller's outer catch is what turns the throw into inconclusive.)
const vitestDir = join(dir, 'node_modules', 'vitest');
mkdirSync(vitestDir, { recursive: true });
writeFileSync(
join(vitestDir, 'package.json'),
JSON.stringify({
name: 'vitest',
exports: { '.': './index.js' },
}),
);
writeFileSync(join(vitestDir, 'index.js'), '');
expect(() => runControlMutant(dir, 'a.test.ts')).toThrow();
expect(readFileSync(join(dir, 'a.test.ts'), 'utf8')).toBe(original);
} finally {
Expand Down
10 changes: 7 additions & 3 deletions packages/cli/src/commands/review/test-efficacy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1258,11 +1258,15 @@ function gitOut(cwd: string, ...args: string[]): string {
* same up-tree walk Node uses for the probe's own imports, so it also survives
* non-hoisted layouts.
*/
export function findVitestBin(worktree: string): string {
const req = createRequire(join(worktree, 'noop.js'));
export function findVitestBin(
worktree: string,
resolveModule: (specifier: string) => string = createRequire(
join(worktree, 'noop.js'),
).resolve,
): string {
let pkgPath: string;
try {
pkgPath = req.resolve('vitest/package.json');
pkgPath = resolveModule('vitest/package.json');
} catch (error) {
// Only a genuine MODULE_NOT_FOUND is "vitest not found". A present vitest
// whose `exports` no longer exposes `./package.json` throws
Expand Down
Loading