Files
openclaw/test/vitest/vitest.pattern-file.ts
Peter Steinberger 35695cb3bc fix(test): intersect scoped CLI filters with each lane's own scope (#125004)
narrowIncludePatterns returned the caller's CLI pattern whenever it overlapped
a lane's include list, instead of intersecting the two. A directory argument
therefore replaced the lane's curated scope with a broad glob:

  pnpm test src/plugins
  -> unit-fast include became ["src/plugins/**/*.test.*"] instead of its
     60 curated files
  -> contracts-plugin include became ["src/plugins/**/*.test.*"] instead of
     ["src/plugins/contracts/**/*.test.ts"]

Both lanes run isolate: false, so every re-admitted file shared a worker with
unrelated files. unit-fast re-admitted exactly the files it excludes for being
stateful (module mocking, dynamic import, filesystem state), and contracts-plugin
pulled in every sibling test outside contracts/. That produced nondeterministic
cross-file pollution: failures that moved between files and lanes run to run,
reproduced in neither isolation nor CI.

Keep the lane's own pattern when it is rooted deeper than the CLI selection,
otherwise keep the CLI pattern. Equal-depth selections are unchanged, so the
existing scoped-config expectations still hold.

Coverage is unchanged; only duplicate execution is removed. `pnpm test src/plugins`
went from 1171 file-executions (3.4x duplication over 346 files) to 344 — the two
remaining files are .e2e.test.ts, excluded by the shared config by design.
2026-08-16 23:24:11 -07:00

306 lines
8.8 KiB
TypeScript

// Vitest pattern file helper reads include and exclude patterns from files.
import fs from "node:fs";
import path from "node:path";
const VITEST_OPTION_VALUE_FLAGS = new Set([
"-c",
"-r",
"-t",
"--browser",
"--changed",
"--config",
"--coverage.all",
"--coverage.exclude",
"--coverage.extension",
"--coverage.include",
"--coverage.provider",
"--coverage.reporter",
"--coverage.reportsDirectory",
"--dir",
"--environment",
"--environmentOptions",
"--exclude",
"--hookTimeout",
"--inspect",
"--inspectBrk",
"--maxConcurrency",
"--maxWorkers",
"--minWorkers",
"--mode",
"--name",
"--outputFile",
"--pool",
"--project",
"--reporter",
"--retry",
"--root",
"--sequence",
"--shard",
"--testNamePattern",
"--testTimeout",
"--workspace",
]);
function normalizeCliPattern(value: string): string {
let normalized = value
.trim()
.replace(/^\.\/+/u, "")
.replace(/\/+$/u, "");
if (
/^(?:src|test|extensions|ui|packages|apps)(?:\/|$)/u.test(normalized) &&
!/[?*[\]{}]/u.test(normalized) &&
!/\.(?:[cm]?[jt]sx?)$/u.test(normalized)
) {
normalized = `${normalized}/**/*.test.*`;
}
return normalized;
}
function normalizeScopedDir(value: string | undefined): string {
return value?.trim().replaceAll("\\", "/").replace(/\/+$/u, "") ?? "";
}
function hasRepoRootPrefix(value: string): boolean {
return /^(?:src|test|extensions|ui|packages|apps)(?:\/|$)/u.test(value);
}
function looksLikeDirRelativePath(value: string): boolean {
return (
value.includes("/") ||
value.includes(".test.") ||
value.includes(".e2e.") ||
value.includes(".live.")
);
}
function applyScopedDir(value: string, scopedDir: string): string {
const normalizedValue = value
.trim()
.replace(/^\.\/+/u, "")
.replaceAll("\\", "/");
if (
!scopedDir ||
hasRepoRootPrefix(normalizedValue) ||
path.isAbsolute(value) ||
!looksLikeDirRelativePath(normalizedValue)
) {
return normalizedValue;
}
return `${scopedDir}/${normalizedValue}`;
}
function looksLikeCliIncludePattern(value: string): boolean {
const normalized = normalizeCliPattern(value);
return (
normalized.includes(".test.") ||
normalized.includes(".e2e.") ||
normalized.includes(".live.") ||
/^(?:src|test|extensions|ui|packages|apps)(?:\/|$)/u.test(normalized)
);
}
function literalPrefixForGlobPattern(value: string): string {
const normalized = value.replaceAll("\\", "/");
const globIndex = normalized.search(/[?*[\]{}]/u);
if (globIndex === -1) {
return normalized;
}
const slashIndex = normalized.lastIndexOf("/", globIndex);
return slashIndex === -1 ? "" : normalized.slice(0, slashIndex + 1);
}
function patternsCouldOverlap(value: string, pattern: string): boolean {
if (path.matchesGlob(value, pattern) || path.matchesGlob(pattern, value)) {
return true;
}
const valuePrefix = literalPrefixForGlobPattern(value);
const patternPrefix = literalPrefixForGlobPattern(pattern);
return (
patternPrefix === "" ||
valuePrefix === "" ||
valuePrefix.startsWith(patternPrefix) ||
patternPrefix.startsWith(valuePrefix)
);
}
function narrowIncludePatterns(
includePatterns: string[],
candidatePatterns: string[] | null,
): string[] | null {
if (!candidatePatterns) {
return null;
}
// Narrowing must intersect the CLI selection with the lane's own scope. When the lane is
// rooted deeper than the CLI selection, keep the lane: returning the caller's broader
// directory pattern re-admits files the lane never owns — `unit-fast` picks up the stateful
// files it excludes, and `contracts-*` picks up every sibling test outside `contracts/`.
// Both run `isolate: false`, so those extra files share a worker and pollute unrelated ones.
const narrowed = new Set<string>();
for (const candidate of candidatePatterns) {
const candidatePrefix = literalPrefixForGlobPattern(candidate);
for (const laneScope of includePatterns) {
if (!patternsCouldOverlap(candidate, laneScope)) {
continue;
}
const laneScopePrefix = literalPrefixForGlobPattern(laneScope);
narrowed.add(laneScopePrefix.length > candidatePrefix.length ? laneScope : candidate);
}
}
return [...narrowed];
}
function isPlainRepoRelativePath(value: string): boolean {
if (!/^[A-Za-z0-9_./-]+$/u.test(value) || path.isAbsolute(value)) {
return false;
}
return value.split("/").every((segment) => segment !== "" && segment !== "." && segment !== "..");
}
function directoryTestPatternRoot(value: string): string | null {
const normalized = value.trim().replaceAll("\\", "/").replace(/^\.\//u, "");
if (normalized === "**/*.test.ts") {
return "";
}
const suffix = "/**/*.test.ts";
if (!normalized.endsWith(suffix)) {
return null;
}
const root = normalized.slice(0, -suffix.length);
return isPlainRepoRelativePath(root) ? root : null;
}
function isAtOrUnder(value: string, root: string): boolean {
return root === "" || value === root || value.startsWith(`${root}/`);
}
function patternIsFullyUnderDirectory(pattern: string, root: string): boolean {
const normalized = pattern.trim().replaceAll("\\", "/").replace(/^\.\//u, "");
if (!normalized.endsWith(".test.ts")) {
return false;
}
const literalPrefix = literalPrefixForGlobPattern(normalized).replace(/\/+$/u, "");
return isAtOrUnder(literalPrefix, root);
}
function intersectDirectoryTestPattern(
includePatterns: string[],
candidatePattern: string,
): string[] | null {
const candidateRoot = directoryTestPatternRoot(candidatePattern);
if (candidateRoot === null) {
return null;
}
const result: string[] = [];
let hasAmbiguousOverlap = false;
for (const includePattern of includePatterns) {
const includeRoot = directoryTestPatternRoot(includePattern);
if (includeRoot !== null && isAtOrUnder(candidateRoot, includeRoot)) {
return [candidatePattern];
} else if (patternIsFullyUnderDirectory(includePattern, candidateRoot)) {
result.push(includePattern);
} else if (patternsCouldOverlap(candidatePattern, includePattern)) {
hasAmbiguousOverlap = true;
}
}
if (hasAmbiguousOverlap) {
return null;
}
return [...new Set(result)];
}
export function intersectIncludePatterns(
includePatterns: string[],
candidatePatterns: string[] | null,
): string[] | null {
if (!candidatePatterns) {
return null;
}
const result: string[] = [];
for (const candidate of candidatePatterns) {
if (!isPlainRepoRelativePath(candidate)) {
// Watch directory targets retain their glob so newly added tests appear.
// Only generated directory globs have a provable ownership intersection.
const intersection = intersectDirectoryTestPattern(includePatterns, candidate);
if (!intersection) {
throw new Error(`cannot safely intersect non-literal include path: ${candidate}`);
}
result.push(...intersection);
continue;
}
if (includePatterns.some((include) => path.matchesGlob(candidate, include))) {
result.push(candidate);
}
}
return [...new Set(result)];
}
function loadPatternListFile(filePath: string, label: string): string[] {
const parsed = JSON.parse(fs.readFileSync(filePath, "utf8")) as unknown;
if (!Array.isArray(parsed)) {
throw new TypeError(`${label} must point to a JSON array: ${filePath}`);
}
return parsed.filter((value): value is string => typeof value === "string" && value.length > 0);
}
export function loadPatternListFromEnv(
envKey: string,
env: Record<string, string | undefined> = process.env,
): string[] | null {
const filePath = env[envKey]?.trim();
if (!filePath) {
return null;
}
return loadPatternListFile(filePath, envKey);
}
function loadPatternListFromArgvForScope(
argv: string[] = process.argv,
options: { scopedDir?: string } = {},
): string[] | null {
const values: string[] = [];
let skipNext = false;
for (const value of argv.slice(2)) {
if (skipNext) {
skipNext = false;
continue;
}
if (value === "run" || value === "watch" || value === "bench") {
continue;
}
if (VITEST_OPTION_VALUE_FLAGS.has(value)) {
skipNext = true;
continue;
}
if (value.startsWith("-")) {
continue;
}
values.push(value);
}
const scopedDir = normalizeScopedDir(options.scopedDir);
const patterns = values
.map((value) => applyScopedDir(value, scopedDir))
.filter(looksLikeCliIncludePattern)
.map(normalizeCliPattern);
return patterns.length > 0 ? [...new Set(patterns)] : null;
}
export function narrowIncludePatternsForCli(
includePatterns: string[],
argv: string[] = process.argv,
options: { scopedDir?: string } = {},
): string[] | null {
const cliPatterns = loadPatternListFromArgvForScope(argv, options);
if (!cliPatterns) {
return null;
}
return narrowIncludePatterns(includePatterns, cliPatterns);
}