Tests: add unit-fast Vitest lane

This commit is contained in:
Peter Steinberger
2026-04-07 10:01:26 +01:00
parent ae12aa49c3
commit ba484d263b
20 changed files with 550 additions and 137 deletions

View File

@@ -1248,6 +1248,9 @@
"test:startup:bench:update": "node scripts/test-update-cli-startup-bench.mjs",
"test:startup:memory": "node scripts/check-cli-startup-memory.mjs",
"test:ui": "pnpm ui:i18n:check && pnpm lint:ui:no-raw-window-open && pnpm --dir ui test",
"test:unit": "pnpm test:unit:fast && node scripts/run-vitest.mjs run --config vitest.unit.config.ts",
"test:unit:fast": "node scripts/run-vitest.mjs run --config vitest.unit-fast.config.ts",
"test:unit:fast:audit": "node scripts/test-unit-fast-audit.mjs",
"test:voicecall:closedloop": "node scripts/test-voicecall-closedloop.mjs",
"test:watch": "node scripts/test-projects.mjs --watch",
"tool-display:check": "node --import tsx scripts/tool-display.ts --check",

View File

@@ -27,6 +27,7 @@ import {
resolvePluginSdkLightIncludePattern,
} from "../vitest.plugin-sdk-paths.mjs";
import { fullSuiteVitestShards } from "../vitest.test-shards.mjs";
import { resolveUnitFastTestIncludePattern } from "../vitest.unit-fast-paths.mjs";
import { isBoundaryTestFile, isBundledPluginDependentUnitTestFile } from "../vitest.unit-paths.mjs";
import { resolveVitestCliEntry, resolveVitestNodeArgs } from "./run-vitest.mjs";
@@ -70,6 +71,7 @@ const LOGGING_VITEST_CONFIG = "vitest.logging.config.ts";
const PLUGIN_SDK_LIGHT_VITEST_CONFIG = "vitest.plugin-sdk-light.config.ts";
const PLUGIN_SDK_VITEST_CONFIG = "vitest.plugin-sdk.config.ts";
const PLUGINS_VITEST_CONFIG = "vitest.plugins.config.ts";
const UNIT_FAST_VITEST_CONFIG = "vitest.unit-fast.config.ts";
const PROCESS_VITEST_CONFIG = "vitest.process.config.ts";
const RUNTIME_CONFIG_VITEST_CONFIG = "vitest.runtime-config.config.ts";
const SECRETS_VITEST_CONFIG = "vitest.secrets.config.ts";
@@ -82,6 +84,58 @@ const UTILS_VITEST_CONFIG = "vitest.utils.config.ts";
const WIZARD_VITEST_CONFIG = "vitest.wizard.config.ts";
const INCLUDE_FILE_ENV_KEY = "OPENCLAW_VITEST_INCLUDE_FILE";
const CHANGED_ARGS_PATTERN = /^--changed(?:=(.+))?$/u;
const VITEST_CONFIG_BY_KIND = {
acp: ACP_VITEST_CONFIG,
agent: AGENTS_VITEST_CONFIG,
autoReply: AUTO_REPLY_VITEST_CONFIG,
boundary: BOUNDARY_VITEST_CONFIG,
bundled: BUNDLED_VITEST_CONFIG,
channel: CHANNEL_VITEST_CONFIG,
cli: CLI_VITEST_CONFIG,
command: COMMANDS_VITEST_CONFIG,
commandLight: COMMANDS_LIGHT_VITEST_CONFIG,
contracts: CONTRACTS_VITEST_CONFIG,
cron: CRON_VITEST_CONFIG,
daemon: DAEMON_VITEST_CONFIG,
e2e: E2E_VITEST_CONFIG,
extension: EXTENSIONS_VITEST_CONFIG,
extensionAcpx: EXTENSION_ACPX_VITEST_CONFIG,
extensionBlueBubbles: EXTENSION_BLUEBUBBLES_VITEST_CONFIG,
extensionChannel: EXTENSION_CHANNELS_VITEST_CONFIG,
extensionDiffs: EXTENSION_DIFFS_VITEST_CONFIG,
extensionFeishu: EXTENSION_FEISHU_VITEST_CONFIG,
extensionIrc: EXTENSION_IRC_VITEST_CONFIG,
extensionMatrix: EXTENSION_MATRIX_VITEST_CONFIG,
extensionMattermost: EXTENSION_MATTERMOST_VITEST_CONFIG,
extensionMemory: EXTENSION_MEMORY_VITEST_CONFIG,
extensionMessaging: EXTENSION_MESSAGING_VITEST_CONFIG,
extensionMsTeams: EXTENSION_MSTEAMS_VITEST_CONFIG,
extensionProvider: EXTENSION_PROVIDERS_VITEST_CONFIG,
extensionTelegram: EXTENSION_TELEGRAM_VITEST_CONFIG,
extensionVoiceCall: EXTENSION_VOICE_CALL_VITEST_CONFIG,
extensionWhatsApp: EXTENSION_WHATSAPP_VITEST_CONFIG,
extensionZalo: EXTENSION_ZALO_VITEST_CONFIG,
gateway: GATEWAY_VITEST_CONFIG,
hooks: HOOKS_VITEST_CONFIG,
infra: INFRA_VITEST_CONFIG,
logging: LOGGING_VITEST_CONFIG,
media: MEDIA_VITEST_CONFIG,
mediaUnderstanding: MEDIA_UNDERSTANDING_VITEST_CONFIG,
plugin: PLUGINS_VITEST_CONFIG,
pluginSdk: PLUGIN_SDK_VITEST_CONFIG,
pluginSdkLight: PLUGIN_SDK_LIGHT_VITEST_CONFIG,
process: PROCESS_VITEST_CONFIG,
unitFast: UNIT_FAST_VITEST_CONFIG,
runtimeConfig: RUNTIME_CONFIG_VITEST_CONFIG,
secrets: SECRETS_VITEST_CONFIG,
sharedCore: SHARED_CORE_VITEST_CONFIG,
tasks: TASKS_VITEST_CONFIG,
tooling: TOOLING_VITEST_CONFIG,
tui: TUI_VITEST_CONFIG,
ui: UI_VITEST_CONFIG,
utils: UTILS_VITEST_CONFIG,
wizard: WIZARD_VITEST_CONFIG,
};
const BROAD_CHANGED_RERUN_PATTERNS = [
/^package\.json$/u,
/^pnpm-lock\.yaml$/u,
@@ -221,6 +275,9 @@ export function resolveChangedTargetArgs(
function classifyTarget(arg, cwd) {
const relative = toRepoRelativeTarget(arg, cwd);
if (resolveUnitFastTestIncludePattern(relative)) {
return "unitFast";
}
if (relative.endsWith(".e2e.test.ts")) {
return "e2e";
}
@@ -376,6 +433,10 @@ function classifyTarget(arg, cwd) {
function resolveLightLaneIncludePatterns(kind, targetArg, cwd) {
const relative = toRepoRelativeTarget(targetArg, cwd);
if (kind === "unitFast") {
const includePattern = resolveUnitFastTestIncludePattern(relative);
return includePattern ? [includePattern] : null;
}
if (kind === "pluginSdkLight") {
const includePattern = resolvePluginSdkLightIncludePattern(relative);
return includePattern ? [includePattern] : null;
@@ -459,6 +520,7 @@ export function buildVitestRunPlans(
const nonTargetArgs = activeForwardedArgs.filter((arg) => !activeTargetArgs.includes(arg));
const orderedKinds = [
"unitFast",
"default",
"boundary",
"tooling",
@@ -516,122 +578,7 @@ export function buildVitestRunPlans(
if (!grouped || grouped.length === 0) {
continue;
}
const config =
kind === "boundary"
? BOUNDARY_VITEST_CONFIG
: kind === "tooling"
? TOOLING_VITEST_CONFIG
: kind === "contracts"
? CONTRACTS_VITEST_CONFIG
: kind === "bundled"
? BUNDLED_VITEST_CONFIG
: kind === "gateway"
? GATEWAY_VITEST_CONFIG
: kind === "hooks"
? HOOKS_VITEST_CONFIG
: kind === "infra"
? INFRA_VITEST_CONFIG
: kind === "runtimeConfig"
? RUNTIME_CONFIG_VITEST_CONFIG
: kind === "cron"
? CRON_VITEST_CONFIG
: kind === "daemon"
? DAEMON_VITEST_CONFIG
: kind === "media"
? MEDIA_VITEST_CONFIG
: kind === "logging"
? LOGGING_VITEST_CONFIG
: kind === "pluginSdkLight"
? PLUGIN_SDK_LIGHT_VITEST_CONFIG
: kind === "pluginSdk"
? PLUGIN_SDK_VITEST_CONFIG
: kind === "process"
? PROCESS_VITEST_CONFIG
: kind === "secrets"
? SECRETS_VITEST_CONFIG
: kind === "sharedCore"
? SHARED_CORE_VITEST_CONFIG
: kind === "tasks"
? TASKS_VITEST_CONFIG
: kind === "tui"
? TUI_VITEST_CONFIG
: kind === "mediaUnderstanding"
? MEDIA_UNDERSTANDING_VITEST_CONFIG
: kind === "acp"
? ACP_VITEST_CONFIG
: kind === "cli"
? CLI_VITEST_CONFIG
: kind === "commandLight"
? COMMANDS_LIGHT_VITEST_CONFIG
: kind === "command"
? COMMANDS_VITEST_CONFIG
: kind === "autoReply"
? AUTO_REPLY_VITEST_CONFIG
: kind === "agent"
? AGENTS_VITEST_CONFIG
: kind === "plugin"
? PLUGINS_VITEST_CONFIG
: kind === "ui"
? UI_VITEST_CONFIG
: kind === "utils"
? UTILS_VITEST_CONFIG
: kind === "wizard"
? WIZARD_VITEST_CONFIG
: kind === "e2e"
? E2E_VITEST_CONFIG
: kind === "extensionAcpx"
? EXTENSION_ACPX_VITEST_CONFIG
: kind === "extensionDiffs"
? EXTENSION_DIFFS_VITEST_CONFIG
: kind ===
"extensionBlueBubbles"
? EXTENSION_BLUEBUBBLES_VITEST_CONFIG
: kind ===
"extensionFeishu"
? EXTENSION_FEISHU_VITEST_CONFIG
: kind ===
"extensionIrc"
? EXTENSION_IRC_VITEST_CONFIG
: kind ===
"extensionMattermost"
? EXTENSION_MATTERMOST_VITEST_CONFIG
: kind ===
"extensionChannel"
? EXTENSION_CHANNELS_VITEST_CONFIG
: kind ===
"extensionTelegram"
? EXTENSION_TELEGRAM_VITEST_CONFIG
: kind ===
"extensionVoiceCall"
? EXTENSION_VOICE_CALL_VITEST_CONFIG
: kind ===
"extensionWhatsApp"
? EXTENSION_WHATSAPP_VITEST_CONFIG
: kind ===
"extensionZalo"
? EXTENSION_ZALO_VITEST_CONFIG
: kind ===
"extensionMatrix"
? EXTENSION_MATRIX_VITEST_CONFIG
: kind ===
"extensionMemory"
? EXTENSION_MEMORY_VITEST_CONFIG
: kind ===
"extensionMsTeams"
? EXTENSION_MSTEAMS_VITEST_CONFIG
: kind ===
"extensionMessaging"
? EXTENSION_MESSAGING_VITEST_CONFIG
: kind ===
"extensionProvider"
? EXTENSION_PROVIDERS_VITEST_CONFIG
: kind ===
"channel"
? CHANNEL_VITEST_CONFIG
: kind ===
"extension"
? EXTENSIONS_VITEST_CONFIG
: DEFAULT_VITEST_CONFIG;
const config = VITEST_CONFIG_BY_KIND[kind] ?? DEFAULT_VITEST_CONFIG;
const useCliTargetArgs =
kind === "e2e" ||
(kind === "default" &&

View File

@@ -0,0 +1,60 @@
import {
collectBroadUnitFastTestCandidates,
collectUnitFastTestFileAnalysis,
collectUnitFastTestCandidates,
unitFastTestFiles,
} from "../vitest.unit-fast-paths.mjs";
const args = new Set(process.argv.slice(2));
const json = args.has("--json");
const scope = args.has("--broad") ? "broad" : "current";
const analysis = collectUnitFastTestFileAnalysis(process.cwd(), { scope });
const rejected = analysis.filter((entry) => !entry.unitFast);
const reasonCounts = new Map();
const candidateCount =
scope === "broad"
? collectBroadUnitFastTestCandidates(process.cwd()).length
: collectUnitFastTestCandidates(process.cwd()).length;
const unitFastCount = analysis.filter((entry) => entry.unitFast).length;
for (const entry of rejected) {
for (const reason of entry.reasons) {
reasonCounts.set(reason, (reasonCounts.get(reason) ?? 0) + 1);
}
}
if (json) {
console.log(
JSON.stringify(
{
candidates: candidateCount,
unitFast: unitFastCount,
routed: unitFastTestFiles.length,
rejected: rejected.length,
reasonCounts: Object.fromEntries(
[...reasonCounts.entries()].toSorted(([a], [b]) => a.localeCompare(b)),
),
scope,
files: analysis,
},
null,
2,
),
);
process.exit(0);
}
console.log(
[
`[test-unit-fast-audit] scope=${scope} candidates=${analysis.length} unitFast=${unitFastCount} routed=${unitFastTestFiles.length} rejected=${rejected.length}`,
scope === "broad"
? `[test-unit-fast-audit] broad unit-fast candidates are not routed automatically`
: "",
"",
"Rejected reasons:",
...[...reasonCounts.entries()]
.toSorted((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
.map(([reason, count]) => ` ${String(count).padStart(4, " ")} ${reason}`),
].join("\n"),
);

View File

@@ -226,6 +226,13 @@ describe("test scripts", () => {
expect(pkg.scripts?.["test:fast"]).toBe(
"node scripts/run-vitest.mjs run --config vitest.unit.config.ts",
);
expect(pkg.scripts?.["test:unit"]).toBe(
"pnpm test:unit:fast && node scripts/run-vitest.mjs run --config vitest.unit.config.ts",
);
expect(pkg.scripts?.["test:unit:fast"]).toBe(
"node scripts/run-vitest.mjs run --config vitest.unit-fast.config.ts",
);
expect(pkg.scripts?.["test:unit:fast:audit"]).toBe("node scripts/test-unit-fast-audit.mjs");
expect(pkg.scripts?.["test"]).toBe("node scripts/test-projects.mjs");
expect(pkg.scripts?.["test:gateway"]).toBe(
"node scripts/run-vitest.mjs run --config vitest.gateway.config.ts",

View File

@@ -163,33 +163,44 @@ describe("test-projects args", () => {
});
it("routes daemon targets to the daemon config", () => {
expect(buildVitestRunPlans(["src/daemon/constants.test.ts"])).toEqual([
expect(buildVitestRunPlans(["src/daemon/inspect.test.ts"])).toEqual([
{
config: "vitest.daemon.config.ts",
forwardedArgs: [],
includePatterns: ["src/daemon/constants.test.ts"],
includePatterns: ["src/daemon/inspect.test.ts"],
watchMode: false,
},
]);
});
it("routes media targets to the media config", () => {
expect(buildVitestRunPlans(["src/media/mime.test.ts"])).toEqual([
expect(buildVitestRunPlans(["src/media/fetch.test.ts"])).toEqual([
{
config: "vitest.media.config.ts",
forwardedArgs: [],
includePatterns: ["src/media/mime.test.ts"],
includePatterns: ["src/media/fetch.test.ts"],
watchMode: false,
},
]);
});
it("routes plugin-sdk targets to the plugin-sdk config", () => {
expect(buildVitestRunPlans(["src/plugin-sdk/provider-stream.test.ts"])).toEqual([
expect(buildVitestRunPlans(["src/plugin-sdk/anthropic-vertex-auth-presence.test.ts"])).toEqual([
{
config: "vitest.plugin-sdk.config.ts",
forwardedArgs: [],
includePatterns: ["src/plugin-sdk/provider-stream.test.ts"],
includePatterns: ["src/plugin-sdk/anthropic-vertex-auth-presence.test.ts"],
watchMode: false,
},
]);
});
it("routes unit-fast light targets to the cache-friendly unit-fast config", () => {
expect(buildVitestRunPlans(["src/plugin-sdk/provider-entry.test.ts"])).toEqual([
{
config: "vitest.unit-fast.config.ts",
forwardedArgs: [],
includePatterns: ["src/plugin-sdk/provider-entry.test.ts"],
watchMode: false,
},
]);
@@ -217,10 +228,10 @@ describe("test-projects args", () => {
]);
});
it("routes shared-core targets to the shared-core config", () => {
it("routes unit-fast shared-core targets to the unit-fast config", () => {
expect(buildVitestRunPlans(["src/shared/text-chunking.test.ts"])).toEqual([
{
config: "vitest.shared-core.config.ts",
config: "vitest.unit-fast.config.ts",
forwardedArgs: [],
includePatterns: ["src/shared/text-chunking.test.ts"],
watchMode: false,
@@ -240,11 +251,11 @@ describe("test-projects args", () => {
});
it("routes logging targets to the logging config", () => {
expect(buildVitestRunPlans(["src/logging/load-levels.test.ts"])).toEqual([
expect(buildVitestRunPlans(["src/logging/console-settings.test.ts"])).toEqual([
{
config: "vitest.logging.config.ts",
forwardedArgs: [],
includePatterns: ["src/logging/load-levels.test.ts"],
includePatterns: ["src/logging/console-settings.test.ts"],
watchMode: false,
},
]);
@@ -339,11 +350,11 @@ describe("test-projects args", () => {
});
it("routes channel targets to the channels config", () => {
expect(buildVitestRunPlans(["src/channels/ids.test.ts"])).toEqual([
expect(buildVitestRunPlans(["src/channels/session.test.ts"])).toEqual([
{
config: "vitest.channels.config.ts",
forwardedArgs: [],
includePatterns: ["src/channels/ids.test.ts"],
includePatterns: ["src/channels/session.test.ts"],
watchMode: false,
},
]);

View File

@@ -55,9 +55,9 @@ describe("scripts/test-projects changed-target routing", () => {
expect(plans).toEqual([
{
config: "vitest.shared-core.config.ts",
config: "vitest.unit-fast.config.ts",
forwardedArgs: [],
includePatterns: ["src/shared/**/*.test.ts"],
includePatterns: ["src/shared/string-normalization.test.ts"],
watchMode: false,
},
{
@@ -83,13 +83,29 @@ describe("scripts/test-projects changed-target routing", () => {
});
it("routes explicit commands light tests to the lighter commands lane", () => {
const plans = buildVitestRunPlans(["src/commands/cleanup-utils.test.ts"], process.cwd());
const plans = buildVitestRunPlans(["src/commands/status-json-runtime.test.ts"], process.cwd());
expect(plans).toEqual([
{
config: "vitest.commands-light.config.ts",
forwardedArgs: [],
includePatterns: ["src/commands/cleanup-utils.test.ts"],
includePatterns: ["src/commands/status-json-runtime.test.ts"],
watchMode: false,
},
]);
});
it("routes unit-fast light tests to the cache-friendly unit-fast lane", () => {
const plans = buildVitestRunPlans(
["src/commands/status-overview-values.test.ts"],
process.cwd(),
);
expect(plans).toEqual([
{
config: "vitest.unit-fast.config.ts",
forwardedArgs: [],
includePatterns: ["src/commands/status-overview-values.test.ts"],
watchMode: false,
},
]);
@@ -97,14 +113,14 @@ describe("scripts/test-projects changed-target routing", () => {
it("routes changed plugin-sdk source allowlist files to sibling light tests", () => {
const plans = buildVitestRunPlans(["--changed", "origin/main"], process.cwd(), () => [
"src/plugin-sdk/lazy-value.ts",
"src/plugin-sdk/provider-entry.ts",
]);
expect(plans).toEqual([
{
config: "vitest.plugin-sdk-light.config.ts",
config: "vitest.unit-fast.config.ts",
forwardedArgs: [],
includePatterns: ["src/plugin-sdk/lazy-value.test.ts"],
includePatterns: ["src/plugin-sdk/provider-entry.test.ts"],
watchMode: false,
},
]);
@@ -118,7 +134,7 @@ describe("scripts/test-projects changed-target routing", () => {
expect(plans).toEqual([
{
config: "vitest.commands-light.config.ts",
config: "vitest.unit-fast.config.ts",
forwardedArgs: [],
includePatterns: [
"src/commands/status-overview-values.test.ts",
@@ -163,6 +179,12 @@ describe("scripts/test-projects changed-target routing", () => {
describe("scripts/test-projects full-suite sharding", () => {
it("splits untargeted runs into fixed shard configs", () => {
expect(buildFullSuiteVitestRunPlans([], process.cwd())).toEqual([
{
config: "vitest.full-core-unit-fast.config.ts",
forwardedArgs: [],
includePatterns: null,
watchMode: false,
},
{
config: "vitest.full-core-unit-src.config.ts",
forwardedArgs: [],

View File

@@ -8,6 +8,7 @@ import { createContractsVitestConfig } from "../vitest.contracts.config.ts";
import { createGatewayVitestConfig } from "../vitest.gateway.config.ts";
import { createPluginSdkLightVitestConfig } from "../vitest.plugin-sdk-light.config.ts";
import { createUiVitestConfig } from "../vitest.ui.config.ts";
import { createUnitFastVitestConfig } from "../vitest.unit-fast.config.ts";
import { createUnitVitestConfig } from "../vitest.unit.config.ts";
describe("projects vitest config", () => {
@@ -21,6 +22,7 @@ describe("projects vitest config", () => {
expect(createCommandsLightVitestConfig().test.pool).toBe("threads");
expect(createCommandsVitestConfig().test.pool).toBe("threads");
expect(createPluginSdkLightVitestConfig().test.pool).toBe("threads");
expect(createUnitFastVitestConfig().test.pool).toBe("threads");
expect(createContractsVitestConfig().test.pool).toBe("threads");
});
@@ -46,6 +48,12 @@ describe("projects vitest config", () => {
expect(config.test.runner).toBe("./test/non-isolated-runner.ts");
});
it("keeps the unit-fast lane on shared workers without the reset-heavy runner", () => {
const config = createUnitFastVitestConfig();
expect(config.test.isolate).toBe(false);
expect(config.test.runner).toBeUndefined();
});
it("keeps the bundled lane on thread workers with the non-isolated runner", () => {
expect(bundledConfig.test?.pool).toBe("threads");
expect(bundledConfig.test?.isolate).toBe(false);

View File

@@ -0,0 +1,65 @@
import { describe, expect, it } from "vitest";
import { createCommandsLightVitestConfig } from "../vitest.commands-light.config.ts";
import { createPluginSdkLightVitestConfig } from "../vitest.plugin-sdk-light.config.ts";
import {
classifyUnitFastTestFileContent,
collectBroadUnitFastTestCandidates,
collectUnitFastTestCandidates,
collectUnitFastTestFileAnalysis,
isUnitFastTestFile,
unitFastTestFiles,
resolveUnitFastTestIncludePattern,
} from "../vitest.unit-fast-paths.mjs";
import { createUnitFastVitestConfig } from "../vitest.unit-fast.config.ts";
describe("unit-fast vitest lane", () => {
it("runs cache-friendly tests without the reset-heavy runner or runtime setup", () => {
const config = createUnitFastVitestConfig({});
expect(config.test?.isolate).toBe(false);
expect(config.test?.runner).toBeUndefined();
expect(config.test?.setupFiles).toEqual([]);
expect(config.test?.include).toContain("src/plugin-sdk/provider-entry.test.ts");
expect(config.test?.include).toContain("src/commands/status-overview-values.test.ts");
});
it("keeps obvious stateful files out of the unit-fast lane", () => {
expect(isUnitFastTestFile("src/plugin-sdk/temp-path.test.ts")).toBe(false);
expect(resolveUnitFastTestIncludePattern("src/plugin-sdk/temp-path.ts")).toBeNull();
expect(classifyUnitFastTestFileContent("vi.resetModules(); await import('./x.js')")).toEqual([
"module-mocking",
"vitest-mock-api",
"dynamic-import",
]);
});
it("routes unit-fast source files to their unit-fast sibling tests", () => {
expect(resolveUnitFastTestIncludePattern("src/plugin-sdk/provider-entry.ts")).toBe(
"src/plugin-sdk/provider-entry.test.ts",
);
expect(resolveUnitFastTestIncludePattern("src/commands/status-overview-values.ts")).toBe(
"src/commands/status-overview-values.test.ts",
);
});
it("keeps broad audit candidates separate from automatically routed unit-fast tests", () => {
const currentCandidates = collectUnitFastTestCandidates();
const broadCandidates = collectBroadUnitFastTestCandidates();
const broadAnalysis = collectUnitFastTestFileAnalysis(process.cwd(), { scope: "broad" });
expect(currentCandidates.length).toBeGreaterThanOrEqual(unitFastTestFiles.length);
expect(broadCandidates.length).toBeGreaterThan(currentCandidates.length);
expect(broadAnalysis.filter((entry) => entry.unitFast).length).toBeGreaterThan(
unitFastTestFiles.length,
);
});
it("excludes unit-fast files from the older light lanes so full runs do not duplicate them", () => {
const pluginSdkLight = createPluginSdkLightVitestConfig({});
const commandsLight = createCommandsLightVitestConfig({});
expect(unitFastTestFiles).toContain("src/plugin-sdk/provider-entry.test.ts");
expect(pluginSdkLight.test?.exclude).toContain("plugin-sdk/provider-entry.test.ts");
expect(commandsLight.test?.exclude).toContain("status-overview-values.test.ts");
});
});

View File

@@ -1,10 +1,12 @@
import { commandsLightTestFiles } from "./vitest.commands-light-paths.mjs";
import { createScopedVitestConfig } from "./vitest.scoped-config.ts";
import { unitFastTestFiles } from "./vitest.unit-fast-paths.mjs";
export function createCommandsLightVitestConfig(env?: Record<string, string | undefined>) {
return createScopedVitestConfig(commandsLightTestFiles, {
dir: "src/commands",
env,
exclude: unitFastTestFiles,
includeOpenClawRuntimeSetup: false,
name: "commands-light",
passWithNoTests: true,

View File

@@ -26,6 +26,7 @@ export const rootVitestProjects = [
"vitest.agents.config.ts",
"vitest.daemon.config.ts",
"vitest.media.config.ts",
"vitest.unit-fast.config.ts",
"vitest.plugin-sdk-light.config.ts",
"vitest.plugin-sdk.config.ts",
"vitest.plugins.config.ts",

View File

@@ -0,0 +1,11 @@
import { defineConfig } from "vitest/config";
import { sharedVitestConfig } from "./vitest.shared.config.ts";
export default defineConfig({
...sharedVitestConfig,
test: {
...sharedVitestConfig.test,
runner: undefined,
projects: ["vitest.unit-fast.config.ts"],
},
});

View File

@@ -1,10 +1,12 @@
import { pluginSdkLightTestFiles } from "./vitest.plugin-sdk-paths.mjs";
import { createScopedVitestConfig } from "./vitest.scoped-config.ts";
import { unitFastTestFiles } from "./vitest.unit-fast-paths.mjs";
export function createPluginSdkLightVitestConfig(env?: Record<string, string | undefined>) {
return createScopedVitestConfig(pluginSdkLightTestFiles, {
dir: "src",
env,
exclude: unitFastTestFiles,
includeOpenClawRuntimeSetup: false,
name: "plugin-sdk-light",
passWithNoTests: true,

View File

@@ -1,6 +1,7 @@
import { defineConfig } from "vitest/config";
import { loadPatternListFromEnv, narrowIncludePatternsForCli } from "./vitest.pattern-file.ts";
import { sharedVitestConfig } from "./vitest.shared.config.ts";
import { unitFastTestFiles } from "./vitest.unit-fast-paths.mjs";
function normalizePathPattern(value: string): string {
return value.replaceAll("\\", "/");
@@ -59,7 +60,7 @@ export function createScopedVitestConfig(
const includeFromEnv = loadPatternListFromEnv("OPENCLAW_VITEST_INCLUDE_FILE", env);
const cliInclude = narrowIncludePatternsForCli(include, options?.argv);
const exclude = relativizeScopedPatterns(
[...(baseTest.exclude ?? []), ...(options?.exclude ?? [])],
[...(baseTest.exclude ?? []), ...unitFastTestFiles, ...(options?.exclude ?? [])],
scopedDir,
);
const isolate = options?.isolate ?? resolveVitestIsolation(options?.env);

View File

@@ -1,9 +1,11 @@
import { createScopedVitestConfig } from "./vitest.scoped-config.ts";
import { unitFastTestFiles } from "./vitest.unit-fast-paths.mjs";
export function createSharedCoreVitestConfig(env?: Record<string, string | undefined>) {
return createScopedVitestConfig(["src/shared/**/*.test.ts"], {
dir: "src",
env,
exclude: unitFastTestFiles,
includeOpenClawRuntimeSetup: false,
name: "shared-core",
passWithNoTests: true,

View File

@@ -256,6 +256,8 @@ export const sharedVitestConfig = {
"vitest.media.config.ts",
"vitest.media-understanding.config.ts",
"vitest.performance-config.ts",
"vitest.unit-fast.config.ts",
"vitest.unit-fast-paths.mjs",
"vitest.scoped-config.ts",
"vitest.shared-core.config.ts",
"vitest.shared.config.ts",

View File

@@ -7,6 +7,11 @@ export const autoReplyTopLevelReplyTestInclude = ["src/auto-reply/reply*.test.ts
export const autoReplyReplySubtreeTestInclude = ["src/auto-reply/reply/**/*.test.ts"];
export const fullSuiteVitestShards = [
{
config: "vitest.full-core-unit-fast.config.ts",
name: "core-unit-fast",
projects: ["vitest.unit-fast.config.ts"],
},
{
config: "vitest.full-core-unit-src.config.ts",
name: "core-unit-src",

231
vitest.unit-fast-paths.mjs Normal file
View File

@@ -0,0 +1,231 @@
import fs from "node:fs";
import path from "node:path";
import {
commandsLightSourceFiles,
commandsLightTestFiles,
} from "./vitest.commands-light-paths.mjs";
import { pluginSdkLightSourceFiles, pluginSdkLightTestFiles } from "./vitest.plugin-sdk-paths.mjs";
const normalizeRepoPath = (value) => value.replaceAll("\\", "/");
const unitFastCandidateGlobs = [
"packages/memory-host-sdk/**/*.test.ts",
"packages/plugin-package-contract/**/*.test.ts",
"src/acp/**/*.test.ts",
"src/bootstrap/**/*.test.ts",
"src/channels/**/*.test.ts",
"src/cli/**/*.test.ts",
"src/config/**/*.test.ts",
"src/daemon/**/*.test.ts",
"src/i18n/**/*.test.ts",
"src/hooks/**/*.test.ts",
"src/image-generation/**/*.test.ts",
"src/infra/**/*.test.ts",
"src/interactive/**/*.test.ts",
"src/link-understanding/**/*.test.ts",
"src/logging/**/*.test.ts",
"src/markdown/**/*.test.ts",
"src/media/**/*.test.ts",
"src/media-generation/**/*.test.ts",
"src/media-understanding/**/*.test.ts",
"src/memory-host-sdk/**/*.test.ts",
"src/music-generation/**/*.test.ts",
"src/node-host/**/*.test.ts",
"src/plugin-sdk/**/*.test.ts",
"src/poll-params.test.ts",
"src/polls.test.ts",
"src/process/**/*.test.ts",
"src/routing/**/*.test.ts",
"src/sessions/**/*.test.ts",
"src/shared/**/*.test.ts",
"src/terminal/**/*.test.ts",
"src/test-utils/**/*.test.ts",
"src/tasks/**/*.test.ts",
"src/tts/**/*.test.ts",
"src/utils/**/*.test.ts",
"src/video-generation/**/*.test.ts",
"src/wizard/**/*.test.ts",
"test/**/*.test.ts",
];
const unitFastCandidateExactFiles = [...pluginSdkLightTestFiles, ...commandsLightTestFiles];
const broadUnitFastCandidateGlobs = [
"src/**/*.test.ts",
"packages/**/*.test.ts",
"test/**/*.test.ts",
];
const broadUnitFastCandidateSkipGlobs = [
"**/*.e2e.test.ts",
"**/*.live.test.ts",
"test/fixtures/**/*.test.ts",
"test/setup-home-isolation.test.ts",
"src/config/schema.base.generated.test.ts",
"src/gateway/**/*.test.ts",
"src/security/**/*.test.ts",
"src/secrets/**/*.test.ts",
"src/tasks/**/*.test.ts",
];
const disqualifyingPatterns = [
{
code: "jsdom-environment",
pattern: /@vitest-environment\s+jsdom/u,
},
{
code: "module-mocking",
pattern: /\bvi\.(?:mock|doMock|unmock|doUnmock|importActual|resetModules)\s*\(/u,
},
{
code: "vitest-mock-api",
pattern: /\bvi\b/u,
},
{
code: "dynamic-import",
pattern: /\b(?:await\s+)?import\s*\(/u,
},
{
code: "fake-timers",
pattern:
/\bvi\.(?:useFakeTimers|setSystemTime|advanceTimers|runAllTimers|runOnlyPendingTimers)\s*\(/u,
},
{
code: "env-or-global-stub",
pattern: /\bvi\.(?:stubEnv|stubGlobal|unstubAllEnvs|unstubAllGlobals)\s*\(/u,
},
{
code: "process-env-mutation",
pattern: /(?:process\.env(?:\.[A-Za-z_$][\w$]*|\[[^\]]+\])?\s*=|delete\s+process\.env)/u,
},
{
code: "global-mutation",
pattern: /(?:globalThis|global)\s*\[[^\]]+\]\s*=/u,
},
{
code: "filesystem-state",
pattern:
/\b(?:mkdtemp|rmSync|writeFileSync|appendFileSync|mkdirSync|createTemp|makeTempDir|tempDir|tmpdir|node:fs|node:os)\b/u,
},
{
code: "runtime-singleton-state",
pattern: /\b(?:setActivePluginRegistry|resetPluginRuntimeStateForTest|reset.*ForTest)\s*\(/u,
},
];
function matchesAnyGlob(file, patterns) {
return patterns.some((pattern) => path.matchesGlob(file, pattern));
}
function walkFiles(directory, files = []) {
let entries;
try {
entries = fs.readdirSync(directory, { withFileTypes: true });
} catch {
return files;
}
for (const entry of entries) {
const entryPath = path.join(directory, entry.name);
if (entry.isDirectory()) {
if (entry.name === "node_modules" || entry.name === "dist" || entry.name === "vendor") {
continue;
}
walkFiles(entryPath, files);
continue;
}
if (entry.isFile() && entry.name.endsWith(".test.ts")) {
files.push(normalizeRepoPath(entryPath));
}
}
return files;
}
export function classifyUnitFastTestFileContent(source) {
const reasons = [];
for (const { code, pattern } of disqualifyingPatterns) {
if (pattern.test(source)) {
reasons.push(code);
}
}
return reasons;
}
export function collectUnitFastTestCandidates(cwd = process.cwd()) {
const discovered = ["src", "packages", "test"]
.flatMap((directory) => walkFiles(path.join(cwd, directory)))
.map((file) => normalizeRepoPath(path.relative(cwd, file)))
.filter(
(file) =>
matchesAnyGlob(file, unitFastCandidateGlobs) &&
!matchesAnyGlob(file, broadUnitFastCandidateSkipGlobs),
);
return [...new Set([...discovered, ...unitFastCandidateExactFiles])].toSorted((a, b) =>
a.localeCompare(b),
);
}
export function collectBroadUnitFastTestCandidates(cwd = process.cwd()) {
const discovered = ["src", "packages", "test"]
.flatMap((directory) => walkFiles(path.join(cwd, directory)))
.map((file) => normalizeRepoPath(path.relative(cwd, file)))
.filter(
(file) =>
matchesAnyGlob(file, broadUnitFastCandidateGlobs) &&
!matchesAnyGlob(file, broadUnitFastCandidateSkipGlobs),
);
return [...new Set([...discovered, ...unitFastCandidateExactFiles])].toSorted((a, b) =>
a.localeCompare(b),
);
}
export function collectUnitFastTestFileAnalysis(cwd = process.cwd(), options = {}) {
const candidates =
options.scope === "broad"
? collectBroadUnitFastTestCandidates(cwd)
: collectUnitFastTestCandidates(cwd);
return candidates.map((file) => {
const absolutePath = path.join(cwd, file);
let source = "";
try {
source = fs.readFileSync(absolutePath, "utf8");
} catch {
return {
file,
unitFast: false,
reasons: ["missing-file"],
};
}
const reasons = classifyUnitFastTestFileContent(source);
return {
file,
unitFast: reasons.length === 0,
reasons,
};
});
}
export const unitFastTestFiles = collectUnitFastTestFileAnalysis()
.filter((entry) => entry.unitFast)
.map((entry) => entry.file);
const unitFastTestFileSet = new Set(unitFastTestFiles);
const sourceToUnitFastTestFile = new Map(
[...pluginSdkLightSourceFiles, ...commandsLightSourceFiles].flatMap((sourceFile) => {
const testFile = sourceFile.replace(/\.ts$/u, ".test.ts");
return unitFastTestFileSet.has(testFile) ? [[sourceFile, testFile]] : [];
}),
);
export function isUnitFastTestFile(file) {
return unitFastTestFileSet.has(normalizeRepoPath(file));
}
export function resolveUnitFastTestIncludePattern(file) {
const normalized = normalizeRepoPath(file);
if (unitFastTestFileSet.has(normalized)) {
return normalized;
}
const siblingTestFile = normalized.replace(/\.ts$/u, ".test.ts");
if (unitFastTestFileSet.has(siblingTestFile)) {
return siblingTestFile;
}
return sourceToUnitFastTestFile.get(normalized) ?? null;
}

View File

@@ -0,0 +1,29 @@
import { defineConfig } from "vitest/config";
import { loadPatternListFromEnv, narrowIncludePatternsForCli } from "./vitest.pattern-file.ts";
import { sharedVitestConfig } from "./vitest.shared.config.ts";
import { unitFastTestFiles } from "./vitest.unit-fast-paths.mjs";
export function createUnitFastVitestConfig(
env: Record<string, string | undefined> = process.env,
options: { argv?: string[] } = {},
) {
const sharedTest = sharedVitestConfig.test ?? {};
const includeFromEnv = loadPatternListFromEnv("OPENCLAW_VITEST_INCLUDE_FILE", env);
const cliInclude = narrowIncludePatternsForCli(unitFastTestFiles, options.argv);
return defineConfig({
...sharedVitestConfig,
test: {
...sharedTest,
name: "unit-fast",
isolate: false,
runner: undefined,
setupFiles: [],
include: includeFromEnv ?? cliInclude ?? unitFastTestFiles,
exclude: sharedTest.exclude ?? [],
passWithNoTests: true,
},
});
}
export default createUnitFastVitestConfig();

View File

@@ -2,6 +2,7 @@ import { defineProject } from "vitest/config";
import { loadPatternListFromEnv, narrowIncludePatternsForCli } from "./vitest.pattern-file.ts";
import { resolveVitestIsolation } from "./vitest.scoped-config.ts";
import { sharedVitestConfig } from "./vitest.shared.config.ts";
import { unitFastTestFiles } from "./vitest.unit-fast-paths.mjs";
import {
isBundledPluginDependentUnitTestFile,
unitTestAdditionalExcludePatterns,
@@ -59,6 +60,7 @@ export function createUnitVitestConfigWithOptions(
...new Set([
...exclude,
...baseExcludePatterns,
...unitFastTestFiles,
...(options.extraExcludePatterns ?? []),
...loadExtraExcludePatternsFromEnv(env),
]),

View File

@@ -1,9 +1,11 @@
import { createScopedVitestConfig } from "./vitest.scoped-config.ts";
import { unitFastTestFiles } from "./vitest.unit-fast-paths.mjs";
export function createUtilsVitestConfig(env?: Record<string, string | undefined>) {
return createScopedVitestConfig(["src/utils/**/*.test.ts"], {
dir: "src",
env,
exclude: unitFastTestFiles,
includeOpenClawRuntimeSetup: false,
name: "utils",
passWithNoTests: true,