fix(test): reject excluded symlink aliases in exact selection

A matching realpath does not make an excluded symlink spelling a discovered test. Compare differing path components, rejecting symlink aliases while preserving ordinary case and Unicode filesystem identity.

Validation: all 14 runner boundary tests pass, including excluded file/directory aliases, valid selections and no target execution.

Based on #2849.

Co-authored-by: ShuaixinHuang <1836656514@qq.com>
This commit is contained in:
morlutoandShuaixinHuang committed 2026-10-14 13:58:17 +08:00
1 parent ef153774f7
commit b782064d54
2 files changed
+75 -6

No files matched your search

+5 -6
View File
@@ -77,7 +77,7 @@ try {
throw cause;
}
if (
!(await hasCaseMismatchedSymlinkComponent(absolute, candidate)) &&
!(await hasMismatchedSymlinkComponent(absolute, candidate)) &&
(await realpath(candidate)) === canonical
)
matches = [...matches, ...specifications];
@@ -99,13 +99,14 @@ try {
if (shutdownSignal !== undefined)
process.exitCode = signalExitCodes[shutdownSignal];
async function hasCaseMismatchedSymlinkComponent(selected, candidate) {
async function hasMismatchedSymlinkComponent(selected, candidate) {
const selectedAbsolute = resolve(selected);
const candidateAbsolute = resolve(candidate);
const root = parse(selectedAbsolute).root;
if (root !== parse(candidateAbsolute).root) return true;
const selectedParts = relative(root, selectedAbsolute).split(sep);
const candidateParts = relative(root, candidateAbsolute).split(sep);
if (selectedParts.length !== candidateParts.length) return true;
let selectedPath = root;
let candidatePath = root;
for (let index = 0; index < selectedParts.length; index += 1) {
@@ -118,14 +119,12 @@ async function hasCaseMismatchedSymlinkComponent(selected, candidate) {
candidatePath = candidatePath
? resolve(candidatePath, candidatePart)
: candidatePart;
if (
selectedPart !== candidatePart &&
selectedPart.toLowerCase() === candidatePart.toLowerCase()
) {
if (selectedPart !== candidatePart) {
const [selectedEntry, candidateEntry] = await Promise.all([
lstat(selectedPath),
lstat(candidatePath),
]);
// Matching inode/realpath does not make an excluded symlink a discovered path.
if (selectedEntry.isSymbolicLink() || candidateEntry.isSymbolicLink())
return true;
}
@@ -88,6 +88,76 @@ const testCommand = (root: string, ...arguments_: string[]) =>
{ ...process.env, NO_COLOR: "1" },
);
describe("development test runner symlink aliases", () => {
it("rejects excluded file and directory aliases without running their targets", async ({
skip,
}) => {
for (const { kind, mode } of [
{ kind: "file", mode: "focused" },
{ kind: "directory", mode: "local" },
{ kind: "directory", mode: "focused" },
] as const) {
const root = await fixture();
const included =
kind === "file" ? join(root, "src") : join(root, "src", "domain");
const includedTest = join(included, "value.test.ts");
const alias =
kind === "file"
? join(root, "src", "excluded.test.ts")
: join(root, "src", "contracts");
if (kind === "directory") {
await mkdir(included);
for (const name of ["value.ts", "value.test.ts"])
await copyFile(join(root, "src", name), join(included, name));
}
try {
await symlink(
kind === "file" ? includedTest : included,
alias,
kind === "directory"
? process.platform === "win32"
? "junction"
: "dir"
: undefined,
);
} catch (cause) {
if (
process.platform === "win32" &&
cause instanceof Error &&
"code" in cause &&
(cause.code === "EPERM" || cause.code === "EACCES")
)
skip("Windows symlink creation requires unavailable privileges");
throw cause;
}
const selectedAlias =
kind === "file"
? "src/excluded.test.ts"
: "src/contracts/value.test.ts";
const includedSelection =
kind === "file" ? "src/value.test.ts" : "src/domain/value.test.ts";
await writeFile(
join(root, "vitest.config.mjs"),
`export default { test: { projects: [{ test: { name: "domain", include: ["${includedSelection}"] } }] } };\n`,
);
await expect(
testCommand(root, mode, selectedAlias),
).rejects.toMatchObject({
code: 1,
stderr: expect.stringContaining(
`Selected test files were not discovered: ${selectedAlias}`,
),
});
await expect(
readFile(join(root, "selected.txt"), "utf8"),
).rejects.toMatchObject({ code: "ENOENT" });
await testCommand(root, mode, includedSelection);
expect(await readFile(join(root, "selected.txt"), "utf8")).toBe("1");
}
});
});
describe("development test runner exact selection", () => {
it("matches selected path casing to the host filesystem", async () => {
const root = await fixture();