perf(test): scope unit-fast discovery

This commit is contained in:
Peter Steinberger
2026-07-10 11:53:30 -04:00
parent 366d09bcc7
commit bd9b1b428c
7 changed files with 232 additions and 30 deletions
+96 -8
View File
@@ -1,4 +1,5 @@
// Vitest unit-fast config tests validate fast unit test project setup.
import path from "node:path";
import { beforeAll, describe, expect, it } from "vitest";
import { spawnNodeEvalSync } from "../src/test-utils/node-process.js";
import { createCommandsLightVitestConfig } from "./vitest/vitest.commands-light.config.ts";
@@ -11,6 +12,7 @@ import {
collectUnitFastTestFileAnalysis,
forcedUnitFastTestFiles,
getUnitFastTestFiles,
getUnitFastTestFilesForIncludePatterns,
getUnitFastTimerTestFiles,
isUnitFastTestFile,
isUnitFastTimerTestFile,
@@ -68,15 +70,60 @@ describe("unit-fast vitest lane", () => {
beforeAll(() => {
const script = `
import childProcess from "node:child_process";
import fs from "node:fs";
import { syncBuiltinESMExports } from "node:module";
let gitLsFilesCalls = 0;
const originalSpawnSync = childProcess.spawnSync;
childProcess.spawnSync = function patchedSpawnSync(...args) {
const [command, commandArgs] = args;
if (command === "git" && commandArgs?.[0] === "ls-files") {
gitLsFilesCalls += 1;
const stdout = [
"src/agents/agent-tools.deferred-followup-guidance.test.ts",
"src/hooks/frontmatter.test.ts",
].join("\\n") + "\\n";
return {
pid: 0,
output: [null, stdout, ""],
stdout,
stderr: "",
status: 0,
signal: null,
};
}
return originalSpawnSync.apply(this, args);
};
syncBuiltinESMExports();
let readdirSyncCalls = 0;
let hookFileReads = 0;
let outsideFileReads = 0;
const originalReaddirSync = fs.readdirSync;
const originalReadFileSync = fs.readFileSync;
fs.readdirSync = function patchedReaddirSync(...args) {
readdirSyncCalls += 1;
return originalReaddirSync.apply(this, args);
};
fs.readFileSync = function patchedReadFileSync(...args) {
const file = String(args[0]).replaceAll("\\\\", "/");
if (file.endsWith("/src/hooks/frontmatter.test.ts")) {
hookFileReads += 1;
} else if (file.endsWith("/src/agents/agent-tools.deferred-followup-guidance.test.ts")) {
outsideFileReads += 1;
}
return originalReadFileSync.apply(this, args);
};
await import("./test/vitest/vitest.hooks.config.ts?scope-probe=" + Date.now());
const scopedHookFileReads = hookFileReads;
const scopedOutsideFileReads = outsideFileReads;
await import("./test/vitest/vitest.unit-fast.config.ts?io-probe=" + Date.now());
console.log(readdirSyncCalls);
console.log(
"UNIT_FAST_IO_PROBE",
gitLsFilesCalls,
readdirSyncCalls,
scopedHookFileReads,
scopedOutsideFileReads,
);
`;
configProbeResult = spawnNodeEvalSync(script, {
env: { ...process.env, FORCE_COLOR: "0", NO_COLOR: "1" },
@@ -94,13 +141,14 @@ describe("unit-fast vitest lane", () => {
it("loads the config without recursively walking repo roots", () => {
expect(configProbeResult.status, configProbeResult.stderr).toBe(0);
const numericOutputLines = configProbeResult.stdout
.trim()
.split(/\r?\n/u)
.map((line) => Number(line.trim()))
.filter(Number.isFinite);
expect(numericOutputLines.length, configProbeResult.stdout).toBeGreaterThan(0);
expect(numericOutputLines.at(-1)).toBeLessThan(20);
const probeMatch = configProbeResult.stdout.match(
/UNIT_FAST_IO_PROBE (\d+) (\d+) (\d+) (\d+)/u,
);
expect(probeMatch, configProbeResult.stdout).not.toBeNull();
expect(Number(probeMatch?.[1])).toBe(1);
expect(Number(probeMatch?.[2])).toBeLessThan(20);
expect(Number(probeMatch?.[3])).toBe(1);
expect(Number(probeMatch?.[4])).toBe(0);
});
it("runs cache-friendly tests without the reset-heavy runner or runtime setup", () => {
@@ -225,6 +273,46 @@ describe("unit-fast vitest lane", () => {
);
});
it("keeps scoped unit-fast exclusions equivalent to the full inventory", () => {
const cases = [
{ dir: "src/hooks", patterns: ["src/hooks/**/*.test.ts"] },
{ dir: "src", patterns: ["src/agents/*/**/*.test.ts"] },
{ dir: "src/acp", patterns: ["src/acp/client.test.ts"] },
{ dir: "extensions", patterns: ["extensions/**/*.test.ts"] },
{ dir: undefined, patterns: ["test/**/*.test.ts"] },
{ dir: undefined, patterns: ["src/{hooks,infra}/**/*.test.ts"] },
{ dir: "src", patterns: [] },
];
for (const { dir, patterns } of cases) {
const prefix = dir ? `${dir}/` : "";
const expected = unitFastTestFiles.filter((file) => {
if (prefix && !file.startsWith(prefix)) {
return false;
}
return patterns.some((pattern) => path.matchesGlob(file, pattern));
});
expect(getUnitFastTestFilesForIncludePatterns(patterns, { dir })).toEqual(expected);
}
const extensionUnitFastFiles = getUnitFastTestFilesForIncludePatterns(
["extensions/**/*.test.ts"],
{ dir: "extensions" },
);
expect(getUnitFastTestFilesForIncludePatterns(["**/*.test.ts"], { dir: "extensions" })).toEqual(
extensionUnitFastFiles,
);
expect(extensionUnitFastFiles).toEqual(
expect.arrayContaining([
"extensions/canvas/src/host/server.test.ts",
"extensions/canvas/src/host/server.state-dir.test.ts",
]),
);
expect(getUnitFastTestFilesForIncludePatterns(["!src/**/*.test.ts"])).toEqual(
unitFastTestFiles,
);
});
it("excludes unit-fast files from the older light lanes so full runs do not duplicate them", () => {
const pluginSdkLight = createPluginSdkLightVitestConfig({});
const commandsLight = createCommandsLightVitestConfig({});
@@ -1,13 +1,11 @@
// Vitest commands light config wires the commands light test shard.
import { commandsLightTestFiles } from "./vitest.commands-light-paths.mjs";
import { createScopedVitestConfig } from "./vitest.scoped-config.ts";
import { getUnitFastTestFiles } from "./vitest.unit-fast-paths.mjs";
export function createCommandsLightVitestConfig(env?: Record<string, string | undefined>) {
return createScopedVitestConfig(commandsLightTestFiles, {
dir: "src/commands",
env,
exclude: getUnitFastTestFiles(),
includeOpenClawRuntimeSetup: false,
name: "commands-light",
passWithNoTests: true,
@@ -1,13 +1,11 @@
// Vitest plugin sdk light config wires the plugin sdk light test shard.
import { pluginSdkLightTestFiles } from "./vitest.plugin-sdk-paths.mjs";
import { createScopedVitestConfig } from "./vitest.scoped-config.ts";
import { getUnitFastTestFiles } from "./vitest.unit-fast-paths.mjs";
export function createPluginSdkLightVitestConfig(env?: Record<string, string | undefined>) {
return createScopedVitestConfig(pluginSdkLightTestFiles, {
dir: "src",
env,
exclude: getUnitFastTestFiles(),
includeOpenClawRuntimeSetup: false,
name: "plugin-sdk-light",
passWithNoTests: true,
+7 -3
View File
@@ -8,7 +8,7 @@ import {
resolveRepoRootPath,
sharedVitestConfig,
} from "./vitest.shared.config.ts";
import { getUnitFastTestFiles } from "./vitest.unit-fast-paths.mjs";
import { getUnitFastTestFilesForIncludePatterns } from "./vitest.unit-fast-paths.mjs";
function normalizePathPattern(value: string): string {
return value.replaceAll("\\", "/");
@@ -219,8 +219,12 @@ export function createScopedVitestConfig(
const cliInclude = narrowIncludePatternsForCli(include, options?.argv, {
scopedDir,
});
const effectiveInclude = includeFromEnv ?? cliInclude ?? include;
const scopedInclude = relativizeScopedPatterns(effectiveInclude, scopedDir);
const unitFastExcludePatterns =
options?.excludeUnitFastTests === false ? [] : getUnitFastTestFiles();
options?.excludeUnitFastTests === false
? []
: getUnitFastTestFilesForIncludePatterns(effectiveInclude, { dir: scopedDir });
const exclude = relativizeScopedPatterns(
[...(baseTest.exclude ?? []), ...unitFastExcludePatterns, ...(options?.exclude ?? [])],
scopedDir,
@@ -249,7 +253,7 @@ export function createScopedVitestConfig(
...(runner ? { runner } : { runner: undefined }),
setupFiles,
...(resolvedScopedDir ? { dir: resolvedScopedDir } : {}),
include: relativizeScopedPatterns(includeFromEnv ?? cliInclude ?? include, scopedDir),
include: scopedInclude,
exclude,
...(options?.pool ? { pool: options.pool } : {}),
...(options?.fileParallelism === undefined
-2
View File
@@ -1,12 +1,10 @@
// Vitest shared core config wires the shared core test shard.
import { createScopedVitestConfig } from "./vitest.scoped-config.ts";
import { getUnitFastTestFiles } from "./vitest.unit-fast-paths.mjs";
export function createSharedCoreVitestConfig(env?: Record<string, string | undefined>) {
return createScopedVitestConfig(["src/shared/**/*.test.ts"], {
dir: "src",
env,
exclude: getUnitFastTestFiles(),
includeOpenClawRuntimeSetup: false,
name: "shared-core",
passWithNoTests: true,
+129 -11
View File
@@ -300,13 +300,21 @@ function matchesAnyGlob(file, patterns) {
return patterns.some((pattern) => path.matchesGlob(file, pattern));
}
const unitFastCandidateFileByPath = new Map();
function isUnitFastCandidateFile(file) {
return (
const cached = unitFastCandidateFileByPath.get(file);
if (cached !== undefined) {
return cached;
}
const candidate =
forcedUnitFastTestFileSet.has(file) ||
unitFastCandidateExactFileSet.has(file) ||
(matchesAnyGlob(file, unitFastCandidateGlobs) &&
!matchesAnyGlob(file, broadUnitFastCandidateSkipGlobs))
);
!matchesAnyGlob(file, broadUnitFastCandidateSkipGlobs));
// Candidate rules are static for the process lifetime; scoped configs overlap heavily.
unitFastCandidateFileByPath.set(file, candidate);
return candidate;
}
function walkFiles(directory, files = []) {
@@ -365,6 +373,68 @@ function collectRepoTestFiles(cwd) {
return files;
}
const unitFastCandidateInventoryByCwd = new Map();
function collectUnitFastCandidateInventory(cwd) {
const cacheKey = normalizeRepoPath(cwd);
const cached = unitFastCandidateInventoryByCwd.get(cacheKey);
if (cached) {
return cached;
}
const inventory = [
...new Set([
...collectRepoTestFiles(cwd),
...unitFastCandidateExactFiles,
...forcedUnitFastTestFiles,
]),
];
// Git inventory and routing constants are stable for the lifetime of a config process.
unitFastCandidateInventoryByCwd.set(cacheKey, inventory);
return inventory;
}
function normalizeScopedDir(dir) {
const normalized = normalizeRepoPath(dir ?? "").replace(/^\.\/+|\/+$/gu, "");
return normalized === "." ? "" : normalized;
}
function hasRepoRootPrefix(value) {
return /^(?:apps|extensions|packages|src|test|ui)(?:\/|$)/u.test(value);
}
function cannotSafelyNarrowIncludePattern(value, dir) {
return (
value.startsWith("!") ||
value.startsWith("./") ||
/^(?:[A-Za-z]:\/|\/)/u.test(value) ||
/(?:^|\/)\.\.(?:\/|$)/u.test(value) ||
(Boolean(dir) && /^[{[(]/u.test(value))
);
}
function anchorScopedIncludePattern(value, dir) {
const normalized = normalizeRepoPath(value);
if (!dir || hasRepoRootPrefix(normalized)) {
return normalized;
}
return `${dir}/${normalized}`;
}
function isFileWithinScope(file, dir) {
return !dir || file.startsWith(`${dir}/`);
}
function literalGlobPrefix(pattern) {
const dynamicIndex = pattern.search(/[!?*[{(@+]/u);
return dynamicIndex < 0 ? pattern : pattern.slice(0, dynamicIndex);
}
function matchesCompiledInclude(file, compiledPatterns) {
return compiledPatterns.some(
({ pattern, prefix }) => file.startsWith(prefix) && path.matchesGlob(file, pattern),
);
}
export function classifyUnitFastTestFileContent(source) {
const reasons = [];
for (const { code, pattern } of disqualifyingPatterns) {
@@ -384,14 +454,19 @@ function collectUnitFastCandidates(cwd, scope) {
return cached;
}
const broad = scope === "broad";
const discovered = collectRepoTestFiles(cwd).filter(
(file) =>
matchesAnyGlob(file, broad ? broadUnitFastCandidateGlobs : unitFastCandidateGlobs) &&
!matchesAnyGlob(file, broadUnitFastCandidateSkipGlobs),
);
const candidates = [
...new Set([...discovered, ...unitFastCandidateExactFiles, ...forcedUnitFastTestFiles]),
].toSorted((a, b) => a.localeCompare(b));
const candidates = collectUnitFastCandidateInventory(cwd)
.filter((file) => {
if (!broad) {
return isUnitFastCandidateFile(file);
}
return (
forcedUnitFastTestFileSet.has(file) ||
unitFastCandidateExactFileSet.has(file) ||
(matchesAnyGlob(file, broadUnitFastCandidateGlobs) &&
!matchesAnyGlob(file, broadUnitFastCandidateSkipGlobs))
);
})
.toSorted((a, b) => a.localeCompare(b));
// Candidate discovery is immutable for the lifetime of a Vitest/audit process.
unitFastCandidatesByKey.set(cacheKey, candidates);
return candidates;
@@ -458,6 +533,49 @@ let cachedUnitFastTestFiles = null;
let cachedUnitFastTestFileSet = null;
let cachedUnitFastTimerTestFiles = null;
let cachedUnitFastTimerTestFileSet = null;
const scopedUnitFastTestFilesByKey = new Map();
export function getUnitFastTestFilesForIncludePatterns(includePatterns, options = {}) {
const cwd = process.cwd();
const normalizedCwd = normalizeRepoPath(cwd);
const dir = normalizeScopedDir(options.dir);
const normalizedPatterns = includePatterns.map(normalizeRepoPath);
if (normalizedPatterns.some((pattern) => cannotSafelyNarrowIncludePattern(pattern, dir))) {
// Keep the former full exclusion list when Vitest syntax cannot be safely mapped to repo paths.
return getUnitFastTestFiles();
}
const patterns = [
...new Set(normalizedPatterns.map((pattern) => anchorScopedIncludePattern(pattern, dir))),
].toSorted();
const cacheKey = JSON.stringify([normalizedCwd, dir, patterns]);
const cached = scopedUnitFastTestFilesByKey.get(cacheKey);
if (cached) {
return cached;
}
if (patterns.length === 0) {
scopedUnitFastTestFilesByKey.set(cacheKey, []);
return [];
}
const compiledPatterns = patterns.map((pattern) => ({
pattern,
prefix: literalGlobPrefix(pattern),
}));
const files = collectUnitFastCandidateInventory(cwd)
.filter((file) => {
return (
isFileWithinScope(file, dir) &&
matchesCompiledInclude(file, compiledPatterns) &&
isUnitFastCandidateFile(file)
);
})
.toSorted((a, b) => a.localeCompare(b))
.filter((file) => analyzeUnitFastTestFile(cwd, file).unitFast);
// Scoped discovery is a process-start snapshot, matching the full unit-fast inventory cache.
scopedUnitFastTestFilesByKey.set(cacheKey, files);
return files;
}
export function getUnitFastTestFiles() {
if (cachedUnitFastTestFiles !== null) {
-2
View File
@@ -1,12 +1,10 @@
// Vitest utils config wires the utils test shard.
import { createScopedVitestConfig } from "./vitest.scoped-config.ts";
import { getUnitFastTestFiles } from "./vitest.unit-fast-paths.mjs";
export function createUtilsVitestConfig(env?: Record<string, string | undefined>) {
return createScopedVitestConfig(["src/utils/**/*.test.ts"], {
dir: "src",
env,
exclude: getUnitFastTestFiles(),
includeOpenClawRuntimeSetup: false,
name: "utils",
passWithNoTests: true,