test: isolate stateful unit-fast tests (#108334)

This commit is contained in:
Peter Steinberger
2026-07-15 07:14:35 -07:00
committed by GitHub
parent dd149f01ba
commit a748634aa9
13 changed files with 242 additions and 31 deletions

View File

@@ -902,6 +902,7 @@ const SPLIT_NODE_SHARDS = new Map([
shardName: "core-unit-fast",
configs: [
"test/vitest/vitest.unit-fast.config.ts",
"test/vitest/vitest.unit-fast-isolated.config.ts",
"test/vitest/vitest.unit-fast-fake-timers.config.ts",
],
requiresDist: false,

View File

@@ -51,8 +51,10 @@ import {
toolingIsolatedTestFiles,
} from "../test/vitest/vitest.tooling-isolated-paths.mjs";
import {
getUnitFastIsolatedTestFiles,
getUnitFastTestFiles,
getUnitFastTimerTestFiles,
resolveUnitFastIsolatedTestIncludePattern,
resolveUnitFastTestIncludePattern,
resolveUnitFastTimerTestIncludePattern,
} from "../test/vitest/vitest.unit-fast-paths.mjs";
@@ -164,6 +166,7 @@ const PLUGIN_SDK_LIGHT_VITEST_CONFIG = "test/vitest/vitest.plugin-sdk-light.conf
const PLUGIN_SDK_VITEST_CONFIG = "test/vitest/vitest.plugin-sdk.config.ts";
const PLUGINS_VITEST_CONFIG = "test/vitest/vitest.plugins.config.ts";
const UNIT_FAST_VITEST_CONFIG = "test/vitest/vitest.unit-fast.config.ts";
const UNIT_FAST_ISOLATED_VITEST_CONFIG = "test/vitest/vitest.unit-fast-isolated.config.ts";
const UNIT_FAST_FAKE_TIMERS_VITEST_CONFIG = "test/vitest/vitest.unit-fast-fake-timers.config.ts";
const UNIT_SECURITY_VITEST_CONFIG = "test/vitest/vitest.unit-security.config.ts";
const UNIT_SRC_VITEST_CONFIG = "test/vitest/vitest.unit-src.config.ts";
@@ -201,6 +204,7 @@ const FULL_SUITE_CONFIG_WEIGHT = new Map([
["test/vitest/vitest.tasks.config.ts", 165],
[CHANNEL_VITEST_CONFIG, 164],
[UNIT_FAST_VITEST_CONFIG, 160],
[UNIT_FAST_ISOLATED_VITEST_CONFIG, 159],
[AUTO_REPLY_REPLY_VITEST_CONFIG, 155],
[INFRA_VITEST_CONFIG, 145],
["test/vitest/vitest.secrets.config.ts", 140],
@@ -385,6 +389,7 @@ const VITEST_CONFIG_BY_KIND = {
pluginSdkLight: PLUGIN_SDK_LIGHT_VITEST_CONFIG,
process: PROCESS_VITEST_CONFIG,
unitFast: UNIT_FAST_VITEST_CONFIG,
unitFastIsolated: UNIT_FAST_ISOLATED_VITEST_CONFIG,
unitFastFakeTimers: UNIT_FAST_FAKE_TIMERS_VITEST_CONFIG,
unitSecurity: UNIT_SECURITY_VITEST_CONFIG,
unitSrc: UNIT_SRC_VITEST_CONFIG,
@@ -2565,7 +2570,10 @@ function listToolingFullSuiteTestTargets(cwd) {
function listUnitFastFullSuiteTestTargets() {
const timerTargets = new Set(getUnitFastTimerTestFiles());
return getUnitFastTestFiles().filter((file) => !timerTargets.has(file));
const isolatedTargets = new Set(getUnitFastIsolatedTestFiles());
return getUnitFastTestFiles().filter(
(file) => !timerTargets.has(file) && !isolatedTargets.has(file),
);
}
function listAgentsCoreFullSuiteTestTargets(cwd) {
@@ -3798,6 +3806,9 @@ function classifyTarget(arg, cwd) {
if (resolveUnitFastTimerTestIncludePattern(relative)) {
return "unitFastFakeTimers";
}
if (resolveUnitFastIsolatedTestIncludePattern(relative)) {
return "unitFastIsolated";
}
if (resolveUnitFastTestIncludePattern(relative)) {
return "unitFast";
}
@@ -3991,6 +4002,10 @@ function resolveLightLaneIncludePatterns(kind, targetArg, cwd) {
const includePattern = resolveUnitFastTimerTestIncludePattern(relative);
return includePattern ? [includePattern] : null;
}
if (kind === "unitFastIsolated") {
const includePattern = resolveUnitFastIsolatedTestIncludePattern(relative);
return includePattern ? [includePattern] : null;
}
if (kind === "pluginSdkLight") {
const includePattern = resolvePluginSdkLightIncludePattern(relative);
return includePattern ? [includePattern] : null;
@@ -4170,6 +4185,7 @@ export function buildVitestRunPlans(
const orderedKinds = [
"unitFast",
"unitFastIsolated",
"unitFastFakeTimers",
"default",
"boundary",

View File

@@ -312,6 +312,7 @@ describe("scripts/lib/ci-node-test-plan.mjs", () => {
{
configs: [
"test/vitest/vitest.unit-fast.config.ts",
"test/vitest/vitest.unit-fast-isolated.config.ts",
"test/vitest/vitest.unit-fast-fake-timers.config.ts",
],
requiresDist: false,

View File

@@ -2707,12 +2707,18 @@ describe("scripts/test-projects changed-target routing", () => {
it("chunks the broad shell helper tooling shard after isolated targets", () => {
const plans = buildVitestRunPlans(["test/scripts"], process.cwd());
expect(plans.slice(0, 3)).toEqual([
expect(plans.slice(0, 4)).toEqual([
expect.objectContaining({
config: "test/vitest/vitest.unit-fast.config.ts",
includePatterns: expect.arrayContaining(["test/scripts/arg-utils.test.ts"]),
watchMode: false,
}),
{
config: "test/vitest/vitest.unit-fast-isolated.config.ts",
forwardedArgs: [],
includePatterns: ["test/scripts/android-version.test.ts"],
watchMode: false,
},
{
config: "test/vitest/vitest.tooling-docker.config.ts",
forwardedArgs: [],
@@ -2732,7 +2738,7 @@ describe("scripts/test-projects changed-target routing", () => {
]);
const e2ePlans = plans.filter((plan) => plan.config === "test/vitest/vitest.e2e.config.ts");
const toolingPlans = plans
.slice(3)
.slice(4)
.filter((plan) => plan.config === "test/vitest/vitest.tooling.config.ts");
const toolingTargets = toolingPlans.flatMap((plan) => plan.includePatterns ?? []);
@@ -2818,6 +2824,10 @@ describe("scripts/test-projects changed-target routing", () => {
config: "test/vitest/vitest.unit-fast.config.ts",
includePatterns: expect.arrayContaining(["src/plugin-sdk/access-groups.test.ts"]),
}),
expect.objectContaining({
config: "test/vitest/vitest.unit-fast-isolated.config.ts",
includePatterns: ["src/plugin-sdk/memory-host-events.test.ts"],
}),
expect.objectContaining({
config: "test/vitest/vitest.plugin-sdk-light.config.ts",
includePatterns: expect.arrayContaining(["src/plugin-sdk/acp-runtime.test.ts"]),
@@ -2855,12 +2865,18 @@ describe("scripts/test-projects changed-target routing", () => {
it("chunks broad shell helper globs after isolated targets", () => {
const plans = buildVitestRunPlans(["test/scripts/*.test.ts"], process.cwd());
expect(plans.slice(0, 3)).toEqual([
expect(plans.slice(0, 4)).toEqual([
expect.objectContaining({
config: "test/vitest/vitest.unit-fast.config.ts",
includePatterns: expect.arrayContaining(["test/scripts/arg-utils.test.ts"]),
watchMode: false,
}),
{
config: "test/vitest/vitest.unit-fast-isolated.config.ts",
forwardedArgs: [],
includePatterns: ["test/scripts/android-version.test.ts"],
watchMode: false,
},
{
config: "test/vitest/vitest.tooling-docker.config.ts",
forwardedArgs: [],
@@ -2880,7 +2896,7 @@ describe("scripts/test-projects changed-target routing", () => {
]);
const e2ePlans = plans.filter((plan) => plan.config === "test/vitest/vitest.e2e.config.ts");
const toolingPlans = plans
.slice(3)
.slice(4)
.filter((plan) => plan.config === "test/vitest/vitest.tooling.config.ts");
const toolingTargets = toolingPlans.flatMap((plan) => plan.includePatterns ?? []);
@@ -3747,6 +3763,22 @@ describe("scripts/test-projects changed-target routing", () => {
]);
});
it("routes forced stateful unit-fast tests to the isolated lane", () => {
const plans = buildVitestRunPlans(
["src/system-agent/assistant.configured.test.ts"],
process.cwd(),
);
expect(plans).toEqual([
{
config: "test/vitest/vitest.unit-fast-isolated.config.ts",
forwardedArgs: [],
includePatterns: ["src/system-agent/assistant.configured.test.ts"],
watchMode: false,
},
]);
});
it("routes fake-timer unit-fast tests to the serial fake-timer lane", () => {
const plans = buildVitestRunPlans(["src/acp/control-plane/manager.test.ts"], process.cwd());
@@ -4457,6 +4489,7 @@ describe("scripts/test-projects full-suite sharding", () => {
}
expect(leafShardPlans.map((plan) => plan.config)).toEqual([
...unitFastPlans.map(() => unitFastConfig),
"test/vitest/vitest.unit-fast-isolated.config.ts",
"test/vitest/vitest.unit-fast-fake-timers.config.ts",
"test/vitest/vitest.unit-src.config.ts",
"test/vitest/vitest.unit-security.config.ts",
@@ -4573,7 +4606,7 @@ describe("scripts/test-projects full-suite sharding", () => {
expect(unitFastPlans.every((plan) => plan.forwardedArgs.length <= 70)).toBe(true);
expect(unitFastTargets.length).toBeGreaterThan(1_000);
expect(new Set(unitFastTargets).size).toBe(unitFastTargets.length);
expect(unitFastTargets).toContain("extensions/canvas/src/host/server.state-dir.test.ts");
expect(unitFastTargets).not.toContain("extensions/canvas/src/host/server.state-dir.test.ts");
expect(unitFastTargets).not.toContain("src/utils.test.ts");
const toolingTargets = toolingPlans.flatMap((plan) => plan.forwardedArgs);
expect(toolingPlans.length).toBeGreaterThan(1);

View File

@@ -29,6 +29,7 @@ import {
import { fullSuiteVitestShards } from "./vitest/vitest.test-shards.mjs";
import { createUiVitestConfig } from "./vitest/vitest.ui.config.ts";
import { createUnitFastFakeTimersVitestConfig } from "./vitest/vitest.unit-fast-fake-timers.config.ts";
import { createUnitFastIsolatedVitestConfig } from "./vitest/vitest.unit-fast-isolated.config.ts";
import { createUnitFastVitestConfig } from "./vitest/vitest.unit-fast.config.ts";
import { createUnitVitestConfig } from "./vitest/vitest.unit.config.ts";
@@ -237,6 +238,13 @@ describe("projects vitest config", () => {
expect(testConfig.runner).toBeUndefined();
});
it("isolates forced unit-fast files from shared module caches", () => {
const config = createUnitFastIsolatedVitestConfig();
const testConfig = requireTestConfig(config);
expect(testConfig.isolate).toBe(true);
expect(testConfig.runner).toBeUndefined();
});
it("keeps fake-timer unit-fast files serial with the non-isolated runner", () => {
const config = createUnitFastFakeTimersVitestConfig();
const testConfig = requireTestConfig(config);

View File

@@ -5,18 +5,22 @@ import { spawnNodeEvalSync } from "../src/test-utils/node-process.js";
import { createCommandsLightVitestConfig } from "./vitest/vitest.commands-light.config.ts";
import { createPluginSdkLightVitestConfig } from "./vitest/vitest.plugin-sdk-light.config.ts";
import { createUnitFastFakeTimersVitestConfig } from "./vitest/vitest.unit-fast-fake-timers.config.ts";
import { createUnitFastIsolatedVitestConfig } from "./vitest/vitest.unit-fast-isolated.config.ts";
import {
classifyUnitFastTestFileContent,
collectBroadUnitFastTestCandidates,
collectUnitFastTestCandidates,
collectUnitFastTestFileAnalysis,
forcedUnitFastTestFiles,
getUnitFastIsolatedTestFiles,
getUnitFastTestFiles,
getUnitFastTestFilesForIncludePatterns,
getUnitFastTimerTestFiles,
isUnitFastTestFile,
isUnitFastIsolatedTestFile,
isUnitFastTimerTestFile,
resolveUnitFastTestIncludePattern,
resolveUnitFastIsolatedTestIncludePattern,
resolveUnitFastTimerTestIncludePattern,
} from "./vitest/vitest.unit-fast-paths.mjs";
import { createUnitFastVitestConfig } from "./vitest/vitest.unit-fast.config.ts";
@@ -62,6 +66,7 @@ describe("unit-fast vitest lane", () => {
let configProbeResult: ReturnType<typeof spawnNodeEvalSync>;
let unitFastConfig: ReturnType<typeof createUnitFastVitestConfig>;
let unitFastTestFiles: ReturnType<typeof getUnitFastTestFiles>;
let unitFastIsolatedTestFiles: ReturnType<typeof getUnitFastIsolatedTestFiles>;
let unitFastTimerTestFiles: ReturnType<typeof getUnitFastTimerTestFiles>;
let unitFastAnalysis: ReturnType<typeof collectUnitFastTestFileAnalysis>;
let broadCandidates: ReturnType<typeof collectBroadUnitFastTestCandidates>;
@@ -137,6 +142,7 @@ describe("unit-fast vitest lane", () => {
});
unitFastConfig = createUnitFastVitestConfig({});
unitFastTestFiles = getUnitFastTestFiles();
unitFastIsolatedTestFiles = getUnitFastIsolatedTestFiles();
unitFastTimerTestFiles = getUnitFastTimerTestFiles();
unitFastAnalysis = collectUnitFastTestFileAnalysis();
currentCandidates = collectUnitFastTestCandidates();
@@ -170,30 +176,10 @@ describe("unit-fast vitest lane", () => {
expect(testConfig.include).toContain("src/acp/control-plane/runtime-cache.test.ts");
expect(testConfig.include).toContain("src/acp/runtime/registry.test.ts");
expect(testConfig.include).toContain("src/commands/status-overview-values.test.ts");
expect(testConfig.include).toContain("src/entry.version-fast-path.test.ts");
expect(testConfig.include).toContain("src/flows/doctor-startup-channel-maintenance.test.ts");
expect(testConfig.include).toContain("src/system-agent/rescue-policy.test.ts");
expect(testConfig.include).toContain("src/system-agent/assistant.configured.test.ts");
expect(testConfig.include).toContain("src/flows/search-setup.test.ts");
expect(testConfig.include).toContain("src/plugins/config-policy.test.ts");
expect(testConfig.include).toContain("src/proxy-capture/proxy-server.test.ts");
expect(testConfig.include).toContain("src/talk/agent-consult-tool.test.ts");
expect(testConfig.include).toContain("src/sessions/session-lifecycle-events.test.ts");
expect(testConfig.include).toContain("src/sessions/transcript-events.test.ts");
expect(testConfig.include).toContain("src/security/audit-channel-source-config-slack.test.ts");
expect(testConfig.include).toContain("src/security/audit-config-symlink.test.ts");
expect(testConfig.include).toContain("src/security/audit-exec-sandbox-host.test.ts");
expect(testConfig.include).toContain("src/security/audit-gateway.test.ts");
expect(testConfig.include).toContain("src/security/audit-gateway-auth-selection.test.ts");
expect(testConfig.include).toContain("src/security/audit-gateway-http-auth.test.ts");
expect(testConfig.include).toContain("src/security/audit-gateway-tools-http.test.ts");
expect(testConfig.include).toContain("src/security/audit-plugin-readonly-scope.test.ts");
expect(testConfig.include).toContain("src/security/audit-loopback-logging.test.ts");
expect(testConfig.include).toContain("src/video-generation/provider-registry.test.ts");
expect(testConfig.include).toContain("src/plugin-sdk/provider-entry.test.ts");
expect(testConfig.include).toContain("src/security/dangerous-config-flags.test.ts");
expect(testConfig.include).toContain("src/security/context-visibility.test.ts");
expect(testConfig.include).toContain("src/security/safe-regex.test.ts");
expect(testConfig.include).not.toEqual(expect.arrayContaining(unitFastIsolatedTestFiles));
});
it("does not treat moved config paths as CLI include filters", () => {
@@ -237,7 +223,7 @@ describe("unit-fast vitest lane", () => {
);
});
it("routes audited stateful-looking tests through the fast lane", () => {
it("routes audited stateful-looking tests through the isolated fast lane", () => {
const forcedFileSet = new Set(forcedUnitFastTestFiles);
const forcedAnalysisCount = countMatching(unitFastAnalysis, (entry) =>
forcedFileSet.has(entry.file),
@@ -247,9 +233,39 @@ describe("unit-fast vitest lane", () => {
for (const file of forcedUnitFastTestFiles) {
expect(unitFastTestFiles).toContain(file);
expect(isUnitFastTestFile(file)).toBe(true);
if (unitFastTimerTestFiles.includes(file)) {
expect(unitFastIsolatedTestFiles).not.toContain(file);
} else {
expect(unitFastIsolatedTestFiles).toContain(file);
expect(isUnitFastIsolatedTestFile(file)).toBe(true);
expect(resolveUnitFastTestIncludePattern(file)).toBeNull();
expect(resolveUnitFastIsolatedTestIncludePattern(file)).toBe(file);
}
}
const unroutedForcedFiles = collectUnroutedForcedFiles(unitFastAnalysis, forcedFileSet);
expect(unroutedForcedFiles).toStrictEqual([]);
const isolatedConfig = requireTestConfig(createUnitFastIsolatedVitestConfig({}));
expect(isolatedConfig.isolate).toBe(true);
expect(isolatedConfig.runner).toBeUndefined();
expect(isolatedConfig.include).toEqual(unitFastIsolatedTestFiles);
expect(isolatedConfig.setupFiles).toStrictEqual([
expect.stringMatching(ENV_ISOLATION_SETUP_PATH),
]);
});
it("isolates tests that import stateful test helpers", () => {
const files = [
"src/agents/auth-profiles/oauth-refresh-error.test.ts",
"src/auto-reply/reply/agent-runner-execution-runtime.test.ts",
];
for (const file of files) {
const analysis = unitFastAnalysis.find((entry) => entry.file === file);
expect(analysis?.reasons).toContain("stateful-test-helper");
expect(unitFastIsolatedTestFiles).toContain(file);
expect(resolveUnitFastTestIncludePattern(file)).toBeNull();
expect(resolveUnitFastIsolatedTestIncludePattern(file)).toBe(file);
}
});
it("routes fake-timer unit-fast tests through the serial fake-timer lane", () => {
@@ -265,8 +281,10 @@ describe("unit-fast vitest lane", () => {
}
const fastConfig = requireTestConfig(unitFastConfig);
const isolatedConfig = requireTestConfig(createUnitFastIsolatedVitestConfig({}));
const timerConfig = requireTestConfig(createUnitFastFakeTimersVitestConfig({}));
expect(fastConfig.include).not.toEqual(expect.arrayContaining(unitFastTimerTestFiles));
expect(isolatedConfig.include).not.toEqual(expect.arrayContaining(unitFastTimerTestFiles));
expect(timerConfig.include).toEqual(unitFastTimerTestFiles);
expect(timerConfig.fileParallelism).toBe(false);
expect(timerConfig.maxWorkers).toBe(1);

View File

@@ -40,6 +40,7 @@ export const rootVitestProjects = [
"test/vitest/vitest.daemon.config.ts",
"test/vitest/vitest.media.config.ts",
"test/vitest/vitest.unit-fast.config.ts",
"test/vitest/vitest.unit-fast-isolated.config.ts",
"test/vitest/vitest.unit-fast-fake-timers.config.ts",
"test/vitest/vitest.plugin-sdk-light.config.ts",
"test/vitest/vitest.plugin-sdk.config.ts",

View File

@@ -9,6 +9,7 @@ export default defineConfig({
runner: undefined,
projects: [
"test/vitest/vitest.unit-fast.config.ts",
"test/vitest/vitest.unit-fast-isolated.config.ts",
"test/vitest/vitest.unit-fast-fake-timers.config.ts",
],
},

View File

@@ -16,6 +16,7 @@ export const fullSuiteVitestShards = [
name: "core-unit-fast",
projects: [
"test/vitest/vitest.unit-fast.config.ts",
"test/vitest/vitest.unit-fast-isolated.config.ts",
"test/vitest/vitest.unit-fast-fake-timers.config.ts",
],
},

View File

@@ -0,0 +1,32 @@
// Vitest unit fast isolated config wires audited stateful tests out of shared module caches.
import { defineConfig } from "vitest/config";
import { loadPatternListFromEnv, narrowIncludePatternsForCli } from "./vitest.pattern-file.ts";
import { resolveRepoRootPath, sharedVitestConfig } from "./vitest.shared.config.ts";
import { getUnitFastIsolatedTestFiles } from "./vitest.unit-fast-paths.mjs";
export function createUnitFastIsolatedVitestConfig(
env: Record<string, string | undefined> = process.env,
options: { argv?: string[] } = {},
) {
const sharedTest = sharedVitestConfig.test ?? {};
const includeFromEnv = loadPatternListFromEnv("OPENCLAW_VITEST_INCLUDE_FILE", env);
const isolatedTestFiles = getUnitFastIsolatedTestFiles();
const cliInclude = narrowIncludePatternsForCli(isolatedTestFiles, options.argv);
return defineConfig({
...sharedVitestConfig,
test: {
...sharedTest,
name: "unit-fast-isolated",
// Forced stateful tests can mock modules imported by later files, so each gets a fresh graph.
isolate: true,
runner: undefined,
setupFiles: [resolveRepoRootPath("test/setup.env.ts")],
include: includeFromEnv ?? cliInclude ?? isolatedTestFiles,
exclude: sharedTest.exclude ?? [],
passWithNoTests: true,
},
});
}
export default createUnitFastIsolatedVitestConfig();

View File

@@ -22,8 +22,11 @@ export function getUnitFastTestFilesForIncludePatterns(
options?: { dir?: string },
): string[];
export function getUnitFastTestFiles(): string[];
export function getUnitFastIsolatedTestFiles(): string[];
export function getUnitFastTimerTestFiles(): string[];
export function isUnitFastTestFile(file: string): boolean;
export function isUnitFastIsolatedTestFile(file: string): boolean;
export function isUnitFastTimerTestFile(file: string): boolean;
export function resolveUnitFastTestIncludePattern(file: string): string | null;
export function resolveUnitFastIsolatedTestIncludePattern(file: string): string | null;
export function resolveUnitFastTimerTestIncludePattern(file: string): string | null;

View File

@@ -306,6 +306,42 @@ const disqualifyingPatterns = [
},
];
const statefulTestHelperImportPattern =
/\bfrom\s+["']([^"']*(?:test-support|\.harness)(?:\.js|\.ts)?)["']/gu;
const statefulTestHelperByKey = new Map();
function importsStatefulTestHelper(cwd, file, source) {
for (const match of source.matchAll(statefulTestHelperImportPattern)) {
const specifier = match[1];
if (!specifier.startsWith(".")) {
continue;
}
const helperPath = path.join(
path.dirname(file),
specifier.endsWith(".js")
? `${specifier.slice(0, -3)}.ts`
: specifier.endsWith(".ts")
? specifier
: `${specifier}.ts`,
);
const cacheKey = `${normalizeRepoPath(cwd)}\0${normalizeRepoPath(helperPath)}`;
let stateful = statefulTestHelperByKey.get(cacheKey);
if (stateful === undefined) {
try {
const helperSource = fs.readFileSync(path.join(cwd, helperPath), "utf8");
stateful = classifyUnitFastTestFileContent(helperSource).length > 0;
} catch {
stateful = false;
}
statefulTestHelperByKey.set(cacheKey, stateful);
}
if (stateful) {
return true;
}
}
return false;
}
function matchesAnyGlob(file, patterns) {
return patterns.some((pattern) => path.matchesGlob(file, pattern));
}
@@ -504,10 +540,18 @@ function analyzeUnitFastTestFile(cwd, file) {
try {
const source = fs.readFileSync(path.join(cwd, file), "utf8");
const reasons = classifyUnitFastTestFileContent(source);
if (importsStatefulTestHelper(cwd, file, source)) {
// The helper executes in the importing file's module scope, so its mocks and
// singleton mutations need the same isolation as stateful code in the test itself.
reasons.push("stateful-test-helper");
}
const forced = forcedUnitFastTestFileSet.has(file);
analysis = {
file,
unitFast: forced || reasons.length === 0,
unitFast:
forced ||
reasons.length === 0 ||
reasons.every((reason) => reason === "stateful-test-helper"),
forced,
reasons,
};
@@ -541,6 +585,8 @@ export function collectUnitFastTestFileAnalysis(cwd = process.cwd(), options = {
let cachedUnitFastTestFiles = null;
let cachedUnitFastTestFileSet = null;
let cachedUnitFastIsolatedTestFiles = null;
let cachedUnitFastIsolatedTestFileSet = null;
let cachedUnitFastTimerTestFiles = null;
let cachedUnitFastTimerTestFileSet = null;
const scopedUnitFastTestFilesByKey = new Map();
@@ -607,6 +653,22 @@ export function getUnitFastTimerTestFiles() {
return cachedUnitFastTimerTestFiles;
}
export function getUnitFastIsolatedTestFiles() {
if (cachedUnitFastIsolatedTestFiles !== null) {
return cachedUnitFastIsolatedTestFiles;
}
const timerTestFiles = new Set(getUnitFastTimerTestFiles());
cachedUnitFastIsolatedTestFiles = collectUnitFastTestFileAnalysis()
.filter(
(entry) =>
entry.unitFast &&
!timerTestFiles.has(entry.file) &&
(entry.forced || entry.reasons.includes("stateful-test-helper")),
)
.map((entry) => entry.file);
return cachedUnitFastIsolatedTestFiles;
}
function getUnitFastTestFileSet() {
if (cachedUnitFastTestFileSet !== null) {
return cachedUnitFastTestFileSet;
@@ -623,6 +685,14 @@ function getUnitFastTimerTestFileSet() {
return cachedUnitFastTimerTestFileSet;
}
function getUnitFastIsolatedTestFileSet() {
if (cachedUnitFastIsolatedTestFileSet !== null) {
return cachedUnitFastIsolatedTestFileSet;
}
cachedUnitFastIsolatedTestFileSet = new Set(getUnitFastIsolatedTestFiles());
return cachedUnitFastIsolatedTestFileSet;
}
function isUnitFastTestFileOnDemand(file, cwd = process.cwd()) {
const normalized = normalizeRepoPath(file);
if (!isUnitFastCandidateFile(normalized)) {
@@ -639,11 +709,18 @@ export function isUnitFastTimerTestFile(file) {
return getUnitFastTimerTestFileSet().has(normalizeRepoPath(file));
}
export function isUnitFastIsolatedTestFile(file) {
return getUnitFastIsolatedTestFileSet().has(normalizeRepoPath(file));
}
export function resolveUnitFastTestIncludePattern(file) {
const normalized = normalizeRepoPath(file);
if (isUnitFastTimerTestFile(normalized)) {
return null;
}
if (isUnitFastIsolatedTestFile(normalized)) {
return null;
}
if (isUnitFastTestFileOnDemand(normalized)) {
return normalized;
}
@@ -651,6 +728,9 @@ export function resolveUnitFastTestIncludePattern(file) {
if (isUnitFastTimerTestFile(siblingTestFile)) {
return null;
}
if (isUnitFastIsolatedTestFile(siblingTestFile)) {
return null;
}
if (isUnitFastTestFileOnDemand(siblingTestFile)) {
return siblingTestFile;
}
@@ -669,3 +749,12 @@ export function resolveUnitFastTimerTestIncludePattern(file) {
const siblingTestFile = normalized.replace(/\.ts$/u, ".test.ts");
return isUnitFastTimerTestFile(siblingTestFile) ? siblingTestFile : null;
}
export function resolveUnitFastIsolatedTestIncludePattern(file) {
const normalized = normalizeRepoPath(file);
if (isUnitFastIsolatedTestFile(normalized)) {
return normalized;
}
const siblingTestFile = normalized.replace(/\.ts$/u, ".test.ts");
return isUnitFastIsolatedTestFile(siblingTestFile) ? siblingTestFile : null;
}

View File

@@ -2,7 +2,11 @@
import { defineConfig } from "vitest/config";
import { loadPatternListFromEnv, narrowIncludePatternsForCli } from "./vitest.pattern-file.ts";
import { resolveRepoRootPath, sharedVitestConfig } from "./vitest.shared.config.ts";
import { getUnitFastTestFiles, getUnitFastTimerTestFiles } from "./vitest.unit-fast-paths.mjs";
import {
getUnitFastIsolatedTestFiles,
getUnitFastTestFiles,
getUnitFastTimerTestFiles,
} from "./vitest.unit-fast-paths.mjs";
export function createUnitFastVitestConfig(
env: Record<string, string | undefined> = process.env,
@@ -11,7 +15,10 @@ export function createUnitFastVitestConfig(
const sharedTest = sharedVitestConfig.test ?? {};
const includeFromEnv = loadPatternListFromEnv("OPENCLAW_VITEST_INCLUDE_FILE", env);
const timerTestFiles = new Set(getUnitFastTimerTestFiles());
const unitFastTestFiles = getUnitFastTestFiles().filter((file) => !timerTestFiles.has(file));
const isolatedTestFiles = new Set(getUnitFastIsolatedTestFiles());
const unitFastTestFiles = getUnitFastTestFiles().filter(
(file) => !timerTestFiles.has(file) && !isolatedTestFiles.has(file),
);
const cliInclude = narrowIncludePatternsForCli(unitFastTestFiles, options.argv);
return defineConfig({