refactor: modernize vitest projects config

This commit is contained in:
Peter Steinberger 2026-04-03 17:18:53 +01:00
parent 9dba944c42
commit 2b900b576c
No known key found for this signature in database
12 changed files with 320 additions and 324 deletions

View File

@ -45,7 +45,7 @@ Think of the suites as “increasing realism” (and increasing flakiness/cost):
### Unit / integration (default)
- Command: `pnpm test`
- Config: native Vitest `projects` via `vitest.projects.config.ts` (`unit` + `boundary`)
- Config: native Vitest `projects` via `vitest.config.ts` (`unit` + `boundary`)
- Files: core/unit inventories under `src/**/*.test.ts`, `packages/**/*.test.ts`, `test/**/*.test.ts`, and the whitelisted `ui` node tests covered by `vitest.unit.config.ts`
- Scope:
- Pure unit tests
@ -74,7 +74,7 @@ Think of the suites as “increasing realism” (and increasing flakiness/cost):
- Unit and boundary projects stay on `forks`.
- Channel, extension, and gateway configs also stay on `forks`.
- Unit, channel, and extension configs default to `isolate: false` for faster file startup.
- `pnpm test` inherits the isolation defaults from `vitest.projects.config.ts`.
- `pnpm test` inherits the isolation defaults from the root `vitest.config.ts` projects config.
- Opt back into unit-file isolation with `OPENCLAW_TEST_ISOLATE=1 pnpm test`.
- `OPENCLAW_TEST_NO_ISOLATE=0` or `OPENCLAW_TEST_NO_ISOLATE=false` also force isolated runs.
- Fast-local iteration note:

View File

@ -23,7 +23,7 @@ export function buildVitestArgs(args) {
"vitest",
...(watchMode ? [] : ["run"]),
"--config",
"vitest.projects.config.ts",
"vitest.config.ts",
...forwardedArgs,
];
}

View File

@ -17,7 +17,7 @@ describe("test-projects args", () => {
"exec",
"vitest",
"--config",
"vitest.projects.config.ts",
"vitest.config.ts",
"src/foo.test.ts",
]);
});
@ -28,7 +28,7 @@ describe("test-projects args", () => {
"vitest",
"run",
"--config",
"vitest.projects.config.ts",
"vitest.config.ts",
"src/foo.test.ts",
]);
});

View File

@ -6,35 +6,30 @@ import type {
} from "../src/channels/plugins/types.js";
import type { OpenClawConfig } from "../src/config/config.js";
import type { OutboundSendDeps } from "../src/infra/outbound/deliver.js";
import { resetPluginRuntimeStateForTest, setActivePluginRegistry } from "../src/plugins/runtime.js";
import type { PluginRegistry } from "../src/plugins/registry.js";
import { resetPluginRuntimeStateForTest, setActivePluginRegistry } from "../src/plugins/runtime.js";
import { installSharedTestSetup } from "./setup.shared.js";
const testEnv = installSharedTestSetup();
const [
{ resetContextWindowCacheForTest },
{ resetModelsJsonReadyCacheForTest },
{ drainSessionWriteLockStateForTest, resetSessionWriteLockStateForTest },
{ createTopLevelChannelReplyToModeResolver },
{ createTestRegistry },
{ cleanupSessionStateForTest },
] = await Promise.all([
import("../src/agents/context.js"),
import("../src/agents/models-config.js"),
import("../src/agents/session-write-lock.js"),
import("../src/channels/plugins/threading-helpers.js"),
import("../src/test-utils/channel-plugins.js"),
import("../src/test-utils/session-state-cleanup.js"),
]);
const WORKER_RUNTIME_STATE = Symbol.for("openclaw.testSetupRuntimeState");
const WORKER_RUNTIME_DEPS = Symbol.for("openclaw.testSetupRuntimeDeps");
type WorkerRuntimeState = {
defaultPluginRegistry: PluginRegistry | null;
materializedDefaultPluginRegistry: PluginRegistry | null;
};
type WorkerRuntimeDeps = {
resetContextWindowCacheForTest: typeof import("../src/agents/context.js").resetContextWindowCacheForTest;
resetModelsJsonReadyCacheForTest: typeof import("../src/agents/models-config.js").resetModelsJsonReadyCacheForTest;
drainSessionWriteLockStateForTest: typeof import("../src/agents/session-write-lock.js").drainSessionWriteLockStateForTest;
resetSessionWriteLockStateForTest: typeof import("../src/agents/session-write-lock.js").resetSessionWriteLockStateForTest;
createTopLevelChannelReplyToModeResolver: typeof import("../src/channels/plugins/threading-helpers.js").createTopLevelChannelReplyToModeResolver;
createTestRegistry: typeof import("../src/test-utils/channel-plugins.js").createTestRegistry;
cleanupSessionStateForTest: typeof import("../src/test-utils/session-state-cleanup.js").cleanupSessionStateForTest;
};
const workerRuntimeState = (() => {
const globalState = globalThis as typeof globalThis & {
[WORKER_RUNTIME_STATE]?: WorkerRuntimeState;
@ -48,6 +43,52 @@ const workerRuntimeState = (() => {
return globalState[WORKER_RUNTIME_STATE];
})();
async function loadWorkerRuntimeDeps(): Promise<WorkerRuntimeDeps> {
const [
{ resetContextWindowCacheForTest },
{ resetModelsJsonReadyCacheForTest },
{ drainSessionWriteLockStateForTest, resetSessionWriteLockStateForTest },
{ createTopLevelChannelReplyToModeResolver },
{ createTestRegistry },
{ cleanupSessionStateForTest },
] = await Promise.all([
import("../src/agents/context.js"),
import("../src/agents/models-config.js"),
import("../src/agents/session-write-lock.js"),
import("../src/channels/plugins/threading-helpers.js"),
import("../src/test-utils/channel-plugins.js"),
import("../src/test-utils/session-state-cleanup.js"),
]);
return {
resetContextWindowCacheForTest,
resetModelsJsonReadyCacheForTest,
drainSessionWriteLockStateForTest,
resetSessionWriteLockStateForTest,
createTopLevelChannelReplyToModeResolver,
createTestRegistry,
cleanupSessionStateForTest,
};
}
const workerRuntimeDeps = await (() => {
const globalState = globalThis as typeof globalThis & {
[WORKER_RUNTIME_DEPS]?: Promise<WorkerRuntimeDeps>;
};
globalState[WORKER_RUNTIME_DEPS] ??= loadWorkerRuntimeDeps();
return globalState[WORKER_RUNTIME_DEPS];
})();
const {
resetContextWindowCacheForTest,
resetModelsJsonReadyCacheForTest,
drainSessionWriteLockStateForTest,
resetSessionWriteLockStateForTest,
createTopLevelChannelReplyToModeResolver,
createTestRegistry,
cleanupSessionStateForTest,
} = workerRuntimeDeps;
const pickSendFn = (id: ChannelId, deps?: OutboundSendDeps) => {
return deps?.[id] as ((...args: unknown[]) => Promise<unknown>) | undefined;
};

View File

@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest";
import baseConfig, { resolveLocalVitestMaxWorkers } from "../vitest.config.ts";
describe("resolveLocalVitestMaxWorkers", () => {
it("defaults local runs to a single worker even on larger hosts", () => {
it("uses a moderate local worker cap on larger hosts", () => {
expect(
resolveLocalVitestMaxWorkers(
{
@ -13,7 +13,7 @@ describe("resolveLocalVitestMaxWorkers", () => {
totalMemoryBytes: 64 * 1024 ** 3,
},
),
).toBe(1);
).toBe(4);
});
it("lets OPENCLAW_VITEST_MAX_WORKERS override the inferred cap", () => {
@ -45,7 +45,7 @@ describe("resolveLocalVitestMaxWorkers", () => {
).toBe(3);
});
it("keeps memory-constrained hosts on the same single-worker default", () => {
it("keeps memory-constrained hosts conservative", () => {
expect(
resolveLocalVitestMaxWorkers(
{},
@ -54,10 +54,10 @@ describe("resolveLocalVitestMaxWorkers", () => {
totalMemoryBytes: 16 * 1024 ** 3,
},
),
).toBe(1);
).toBe(2);
});
it("keeps roomy hosts on the same single-worker default", () => {
it("lets roomy hosts use more local parallelism", () => {
expect(
resolveLocalVitestMaxWorkers(
{},
@ -66,7 +66,7 @@ describe("resolveLocalVitestMaxWorkers", () => {
totalMemoryBytes: 128 * 1024 ** 3,
},
),
).toBe(1);
).toBe(6);
});
});

View File

@ -1,12 +1,11 @@
import { describe, expect, it } from "vitest";
import projectsConfig from "../vitest.projects.config.ts";
import baseConfig from "../vitest.config.ts";
describe("projects vitest config", () => {
it("defines named unit and boundary projects", () => {
expect(projectsConfig.test?.projects).toHaveLength(2);
expect(projectsConfig.test?.projects?.map((project) => project.test?.name)).toEqual([
"unit",
"boundary",
it("defines unit and boundary project config files at the root", () => {
expect(baseConfig.test?.projects).toEqual([
"vitest.unit.config.ts",
"vitest.boundary.config.ts",
]);
});
});

View File

@ -1,18 +1,8 @@
import { defineConfig } from "vitest/config";
import baseConfig from "./vitest.config.ts";
import { defineProject } from "vitest/config";
import { loadPatternListFromEnv } from "./vitest.pattern-file.ts";
import { sharedVitestConfig } from "./vitest.shared.config.ts";
import { boundaryTestFiles } from "./vitest.unit-paths.mjs";
const base = baseConfig as unknown as Record<string, unknown>;
const baseTest =
(
baseConfig as {
test?: {
exclude?: string[];
};
}
).test ?? {};
export function loadBoundaryIncludePatternsFromEnv(
env: Record<string, string | undefined> = process.env,
): string[] | null {
@ -20,10 +10,11 @@ export function loadBoundaryIncludePatternsFromEnv(
}
export function createBoundaryVitestConfig(env: Record<string, string | undefined> = process.env) {
return defineConfig({
...base,
return defineProject({
...sharedVitestConfig,
test: {
...baseTest,
...sharedVitestConfig.test,
name: "boundary",
isolate: false,
runner: "./test/non-isolated-runner.ts",
include: loadBoundaryIncludePatternsFromEnv(env) ?? boundaryTestFiles,

View File

@ -1,208 +1,12 @@
import path from "node:path";
import { fileURLToPath } from "node:url";
import { defineConfig } from "vitest/config";
import {
BUNDLED_PLUGIN_ROOT_DIR,
BUNDLED_PLUGIN_TEST_GLOB,
} from "./scripts/lib/bundled-plugin-paths.mjs";
import { pluginSdkSubpaths } from "./scripts/lib/plugin-sdk-entries.mjs";
import { loadVitestExperimentalConfig } from "./vitest.performance-config.ts";
import { resolveLocalVitestMaxWorkers, sharedVitestConfig } from "./vitest.shared.config.ts";
const clamp = (value, min, max) => Math.max(min, Math.min(max, value));
export { resolveLocalVitestMaxWorkers };
function parsePositiveInt(value) {
const parsed = Number.parseInt(value ?? "", 10);
return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
}
export function resolveLocalVitestMaxWorkers(env = process.env, _system = undefined) {
const override = parsePositiveInt(env.OPENCLAW_VITEST_MAX_WORKERS ?? env.OPENCLAW_TEST_WORKERS);
if (override !== null) {
return clamp(override, 1, 16);
}
return 1;
}
const repoRoot = path.dirname(fileURLToPath(import.meta.url));
const isCI = process.env.CI === "true" || process.env.GITHUB_ACTIONS === "true";
const isWindows = process.platform === "win32";
const localWorkers = resolveLocalVitestMaxWorkers();
const ciWorkers = isWindows ? 2 : 3;
export default defineConfig({
resolve: {
// Keep this ordered: the base `openclaw/plugin-sdk` alias is a prefix match.
alias: [
{
find: "openclaw/extension-api",
replacement: path.join(repoRoot, "src", "extensionAPI.ts"),
},
...pluginSdkSubpaths.map((subpath) => ({
find: `openclaw/plugin-sdk/${subpath}`,
replacement: path.join(repoRoot, "src", "plugin-sdk", `${subpath}.ts`),
})),
{
find: "openclaw/plugin-sdk",
replacement: path.join(repoRoot, "src", "plugin-sdk", "index.ts"),
},
],
},
...sharedVitestConfig,
test: {
testTimeout: 120_000,
hookTimeout: isWindows ? 180_000 : 120_000,
// Many suites rely on `vi.stubEnv(...)` and expect it to be scoped to the test.
// Keep env restoration automatic so shared-worker runs do not leak state.
unstubEnvs: true,
// Same rationale as unstubEnvs: avoid cross-test pollution from shared globals.
unstubGlobals: true,
pool: "forks",
maxWorkers: isCI ? ciWorkers : localWorkers,
forceRerunTriggers: [
"package.json",
"pnpm-lock.yaml",
"test/setup.ts",
"test/setup.shared.ts",
"test/setup.extensions.ts",
"scripts/test-projects.mjs",
"vitest.channel-paths.mjs",
"vitest.channels.config.ts",
"vitest.bundled.config.ts",
"vitest.config.ts",
"vitest.contracts.config.ts",
"vitest.e2e.config.ts",
"vitest.extensions.config.ts",
"vitest.gateway.config.ts",
"vitest.live.config.ts",
"vitest.performance-config.ts",
"vitest.projects.config.ts",
"vitest.scoped-config.ts",
"vitest.unit.config.ts",
"vitest.unit-paths.mjs",
],
include: [
"src/**/*.test.ts",
BUNDLED_PLUGIN_TEST_GLOB,
"packages/**/*.test.ts",
"test/**/*.test.ts",
"ui/src/ui/app-chat.test.ts",
"ui/src/ui/chat/**/*.test.ts",
"ui/src/ui/views/agents-utils.test.ts",
"ui/src/ui/views/channels.test.ts",
"ui/src/ui/views/chat.test.ts",
"ui/src/ui/views/nodes.devices.test.ts",
"ui/src/ui/views/skills.test.ts",
"ui/src/ui/views/usage-render-details.test.ts",
"ui/src/ui/controllers/agents.test.ts",
"ui/src/ui/controllers/chat.test.ts",
"ui/src/ui/controllers/skills.test.ts",
"ui/src/ui/controllers/sessions.test.ts",
"ui/src/ui/views/sessions.test.ts",
"ui/src/ui/app-tool-stream.node.test.ts",
"ui/src/ui/app-gateway.sessions.node.test.ts",
"ui/src/ui/chat/slash-command-executor.node.test.ts",
],
setupFiles: ["test/setup.ts"],
exclude: [
"dist/**",
"test/fixtures/**",
"apps/macos/**",
"apps/macos/.build/**",
"**/node_modules/**",
"**/vendor/**",
"dist/OpenClaw.app/**",
"**/*.live.test.ts",
"**/*.e2e.test.ts",
],
coverage: {
provider: "v8",
reporter: ["text", "lcov"],
// Keep coverage stable without an ever-growing exclude list:
// only count files actually exercised by the test suite.
all: false,
thresholds: {
lines: 70,
functions: 70,
branches: 55,
statements: 70,
},
// Anchor to repo-root `src/` only. Without this, coverage globs can
// unintentionally match nested `*/src/**` folders (extensions, apps, etc).
include: ["./src/**/*.ts"],
exclude: [
// Never count workspace packages/apps toward core coverage thresholds.
`${BUNDLED_PLUGIN_ROOT_DIR}/**`,
"apps/**",
"ui/**",
"test/**",
"src/**/*.test.ts",
// Entrypoints and wiring (covered by CI smoke + manual/e2e flows).
"src/entry.ts",
"src/index.ts",
"src/runtime.ts",
"src/channel-web.ts",
"src/logging.ts",
"src/cli/**",
"src/commands/**",
"src/daemon/**",
"src/hooks/**",
"src/macos/**",
// Large integration surfaces; validated via e2e/manual/contract tests.
"src/acp/**",
"src/agents/**",
"src/channels/**",
"src/gateway/**",
"src/line/**",
"src/media-understanding/**",
"src/node-host/**",
"src/plugins/**",
"src/providers/**",
// Some agent integrations are intentionally validated via manual/e2e runs.
"src/agents/model-scan.ts",
"src/agents/pi-embedded-runner.ts",
"src/agents/sandbox-paths.ts",
"src/agents/sandbox.ts",
"src/agents/skills-install.ts",
"src/agents/pi-tool-definition-adapter.ts",
"src/agents/tools/discord-actions*.ts",
"src/agents/tools/slack-actions.ts",
// Hard-to-unit-test modules; exercised indirectly by integration tests.
"src/infra/state-migrations.ts",
"src/infra/skills-remote.ts",
"src/infra/update-check.ts",
"src/infra/ports-inspect.ts",
"src/infra/outbound/outbound-session.ts",
"src/memory/batch-gemini.ts",
// Gateway server integration surfaces are intentionally validated via manual/e2e runs.
"src/gateway/control-ui.ts",
"src/gateway/server-bridge.ts",
"src/gateway/server-channels.ts",
"src/gateway/server-methods/config.ts",
"src/gateway/server-methods/send.ts",
"src/gateway/server-methods/skills.ts",
"src/gateway/server-methods/talk.ts",
"src/gateway/server-methods/web.ts",
"src/gateway/server-methods/wizard.ts",
// Process bridges are hard to unit-test in isolation.
"src/gateway/call.ts",
"src/process/tau-rpc.ts",
"src/process/exec.ts",
// Interactive UIs/flows are intentionally validated via manual/e2e runs.
"src/tui/**",
"src/wizard/**",
// Channel surfaces are largely integration-tested (or manually validated).
"src/browser/**",
"src/channels/web/**",
"src/webchat/**",
"src/gateway/server.ts",
"src/gateway/client.ts",
"src/gateway/protocol/**",
"src/infra/tailscale.ts",
],
},
...loadVitestExperimentalConfig(),
...sharedVitestConfig.test,
projects: ["vitest.unit.config.ts", "vitest.boundary.config.ts"],
},
});

View File

@ -1,49 +0,0 @@
import { defineConfig } from "vitest/config";
import { createBoundaryVitestConfig } from "./vitest.boundary.config.ts";
import baseConfig from "./vitest.config.ts";
import { createUnitVitestConfig } from "./vitest.unit.config.ts";
const base = baseConfig as unknown as Record<string, unknown>;
const baseTest =
(
baseConfig as {
test?: {
include?: string[];
exclude?: string[];
setupFiles?: string[];
};
}
).test ?? {};
const unitTest = createUnitVitestConfig({}).test ?? {};
const boundaryTest = createBoundaryVitestConfig({}).test ?? {};
export default defineConfig({
...base,
test: {
...baseTest,
projects: [
{
extends: true,
test: {
name: "unit",
include: unitTest.include,
exclude: unitTest.exclude,
isolate: unitTest.isolate,
runner: unitTest.runner,
setupFiles: unitTest.setupFiles,
},
},
{
extends: true,
test: {
name: "boundary",
include: boundaryTest.include,
exclude: boundaryTest.exclude,
isolate: boundaryTest.isolate,
runner: boundaryTest.runner,
setupFiles: boundaryTest.setupFiles,
},
},
],
},
});

View File

@ -1,5 +1,5 @@
import { defineConfig } from "vitest/config";
import baseConfig from "./vitest.config.ts";
import { sharedVitestConfig } from "./vitest.shared.config.ts";
function normalizePathPattern(value: string): string {
return value.replaceAll("\\", "/");
@ -48,19 +48,8 @@ export function createScopedVitestConfig(
setupFiles?: string[];
},
) {
const base = baseConfig as unknown as Record<string, unknown>;
const baseTest =
(
baseConfig as {
test?: {
dir?: string;
exclude?: string[];
pool?: "threads" | "forks";
passWithNoTests?: boolean;
setupFiles?: string[];
};
}
).test ?? {};
const base = sharedVitestConfig as Record<string, unknown>;
const baseTest = sharedVitestConfig.test ?? {};
const scopedDir = options?.dir;
const exclude = relativizeScopedPatterns(
[...(baseTest.exclude ?? []), ...(options?.exclude ?? [])],

221
vitest.shared.config.ts Normal file
View File

@ -0,0 +1,221 @@
import os from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
import {
BUNDLED_PLUGIN_ROOT_DIR,
BUNDLED_PLUGIN_TEST_GLOB,
} from "./scripts/lib/bundled-plugin-paths.mjs";
import { pluginSdkSubpaths } from "./scripts/lib/plugin-sdk-entries.mjs";
import { loadVitestExperimentalConfig } from "./vitest.performance-config.ts";
const clamp = (value: number, min: number, max: number) => Math.max(min, Math.min(max, value));
function parsePositiveInt(value: string | undefined): number | null {
const parsed = Number.parseInt(value ?? "", 10);
return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
}
type VitestHostInfo = {
cpuCount?: number;
totalMemoryBytes?: number;
};
function detectVitestHostInfo(): Required<VitestHostInfo> {
return {
cpuCount:
typeof os.availableParallelism === "function" ? os.availableParallelism() : os.cpus().length,
totalMemoryBytes: os.totalmem(),
};
}
export function resolveLocalVitestMaxWorkers(
env: Record<string, string | undefined> = process.env,
system: VitestHostInfo = detectVitestHostInfo(),
): number {
const override = parsePositiveInt(env.OPENCLAW_VITEST_MAX_WORKERS ?? env.OPENCLAW_TEST_WORKERS);
if (override !== null) {
return clamp(override, 1, 16);
}
const cpuCount = Math.max(1, system.cpuCount ?? 1);
const totalMemoryGb = (system.totalMemoryBytes ?? 0) / 1024 ** 3;
let inferred = cpuCount <= 4 ? cpuCount - 1 : Math.floor(cpuCount / 2);
inferred = clamp(inferred, 1, 8);
if (totalMemoryGb <= 16) {
return Math.min(inferred, 2);
}
if (totalMemoryGb <= 32) {
return Math.min(inferred, 3);
}
if (totalMemoryGb <= 64) {
return Math.min(inferred, 4);
}
return Math.min(inferred, 6);
}
const repoRoot = path.dirname(fileURLToPath(import.meta.url));
const isCI = process.env.CI === "true" || process.env.GITHUB_ACTIONS === "true";
const isWindows = process.platform === "win32";
const localWorkers = resolveLocalVitestMaxWorkers();
const ciWorkers = isWindows ? 2 : 3;
export const sharedVitestConfig = {
resolve: {
alias: [
{
find: "openclaw/extension-api",
replacement: path.join(repoRoot, "src", "extensionAPI.ts"),
},
...pluginSdkSubpaths.map((subpath) => ({
find: `openclaw/plugin-sdk/${subpath}`,
replacement: path.join(repoRoot, "src", "plugin-sdk", `${subpath}.ts`),
})),
{
find: "openclaw/plugin-sdk",
replacement: path.join(repoRoot, "src", "plugin-sdk", "index.ts"),
},
],
},
test: {
testTimeout: 120_000,
hookTimeout: isWindows ? 180_000 : 120_000,
unstubEnvs: true,
unstubGlobals: true,
pool: "forks" as const,
maxWorkers: isCI ? ciWorkers : localWorkers,
forceRerunTriggers: [
"package.json",
"pnpm-lock.yaml",
"test/setup.ts",
"test/setup.shared.ts",
"test/setup.extensions.ts",
"test/setup-openclaw-runtime.ts",
"scripts/test-projects.mjs",
"vitest.channel-paths.mjs",
"vitest.channels.config.ts",
"vitest.boundary.config.ts",
"vitest.bundled.config.ts",
"vitest.config.ts",
"vitest.contracts.config.ts",
"vitest.e2e.config.ts",
"vitest.extensions.config.ts",
"vitest.gateway.config.ts",
"vitest.live.config.ts",
"vitest.performance-config.ts",
"vitest.scoped-config.ts",
"vitest.shared.config.ts",
"vitest.unit.config.ts",
"vitest.unit-paths.mjs",
],
include: [
"src/**/*.test.ts",
BUNDLED_PLUGIN_TEST_GLOB,
"packages/**/*.test.ts",
"test/**/*.test.ts",
"ui/src/ui/app-chat.test.ts",
"ui/src/ui/chat/**/*.test.ts",
"ui/src/ui/views/agents-utils.test.ts",
"ui/src/ui/views/channels.test.ts",
"ui/src/ui/views/chat.test.ts",
"ui/src/ui/views/nodes.devices.test.ts",
"ui/src/ui/views/skills.test.ts",
"ui/src/ui/views/usage-render-details.test.ts",
"ui/src/ui/controllers/agents.test.ts",
"ui/src/ui/controllers/chat.test.ts",
"ui/src/ui/controllers/skills.test.ts",
"ui/src/ui/controllers/sessions.test.ts",
"ui/src/ui/views/sessions.test.ts",
"ui/src/ui/app-tool-stream.node.test.ts",
"ui/src/ui/app-gateway.sessions.node.test.ts",
"ui/src/ui/chat/slash-command-executor.node.test.ts",
],
setupFiles: ["test/setup.ts"],
exclude: [
"dist/**",
"test/fixtures/**",
"apps/macos/**",
"apps/macos/.build/**",
"**/node_modules/**",
"**/vendor/**",
"dist/OpenClaw.app/**",
"**/*.live.test.ts",
"**/*.e2e.test.ts",
],
coverage: {
provider: "v8" as const,
reporter: ["text", "lcov"],
all: false,
thresholds: {
lines: 70,
functions: 70,
branches: 55,
statements: 70,
},
include: ["./src/**/*.ts"],
exclude: [
`${BUNDLED_PLUGIN_ROOT_DIR}/**`,
"apps/**",
"ui/**",
"test/**",
"src/**/*.test.ts",
"src/entry.ts",
"src/index.ts",
"src/runtime.ts",
"src/channel-web.ts",
"src/logging.ts",
"src/cli/**",
"src/commands/**",
"src/daemon/**",
"src/hooks/**",
"src/macos/**",
"src/acp/**",
"src/agents/**",
"src/channels/**",
"src/gateway/**",
"src/line/**",
"src/media-understanding/**",
"src/node-host/**",
"src/plugins/**",
"src/providers/**",
"src/agents/model-scan.ts",
"src/agents/pi-embedded-runner.ts",
"src/agents/sandbox-paths.ts",
"src/agents/sandbox.ts",
"src/agents/skills-install.ts",
"src/agents/pi-tool-definition-adapter.ts",
"src/agents/tools/discord-actions*.ts",
"src/agents/tools/slack-actions.ts",
"src/infra/state-migrations.ts",
"src/infra/skills-remote.ts",
"src/infra/update-check.ts",
"src/infra/ports-inspect.ts",
"src/infra/outbound/outbound-session.ts",
"src/memory/batch-gemini.ts",
"src/gateway/control-ui.ts",
"src/gateway/server-bridge.ts",
"src/gateway/server-channels.ts",
"src/gateway/server-methods/config.ts",
"src/gateway/server-methods/send.ts",
"src/gateway/server-methods/skills.ts",
"src/gateway/server-methods/talk.ts",
"src/gateway/server-methods/web.ts",
"src/gateway/server-methods/wizard.ts",
"src/gateway/call.ts",
"src/process/tau-rpc.ts",
"src/process/exec.ts",
"src/tui/**",
"src/wizard/**",
"src/browser/**",
"src/channels/web/**",
"src/webchat/**",
"src/gateway/server.ts",
"src/gateway/client.ts",
"src/gateway/protocol/**",
"src/infra/tailscale.ts",
],
},
...loadVitestExperimentalConfig(),
},
};

View File

@ -1,17 +1,14 @@
import { defineConfig } from "vitest/config";
import baseConfig from "./vitest.config.ts";
import { defineProject } from "vitest/config";
import { loadPatternListFromEnv } from "./vitest.pattern-file.ts";
import { resolveVitestIsolation } from "./vitest.scoped-config.ts";
import { sharedVitestConfig } from "./vitest.shared.config.ts";
import {
unitTestAdditionalExcludePatterns,
unitTestIncludePatterns,
} from "./vitest.unit-paths.mjs";
const base = baseConfig as unknown as Record<string, unknown>;
const baseTest =
(baseConfig as { test?: { include?: string[]; exclude?: string[]; setupFiles?: string[] } })
.test ?? {};
const exclude = baseTest.exclude ?? [];
const sharedTest = sharedVitestConfig.test ?? {};
const exclude = sharedTest.exclude ?? [];
export function loadIncludePatternsFromEnv(
env: Record<string, string | undefined> = process.env,
@ -32,13 +29,16 @@ export function createUnitVitestConfigWithOptions(
extraExcludePatterns?: string[];
} = {},
) {
return defineConfig({
...base,
return defineProject({
...sharedVitestConfig,
test: {
...baseTest,
...sharedTest,
name: "unit",
isolate: resolveVitestIsolation(env),
runner: "./test/non-isolated-runner.ts",
setupFiles: [...new Set([...(baseTest.setupFiles ?? []), "test/setup-openclaw-runtime.ts"])],
setupFiles: [
...new Set([...(sharedTest.setupFiles ?? []), "test/setup-openclaw-runtime.ts"]),
],
include:
loadIncludePatternsFromEnv(env) ?? options.includePatterns ?? unitTestIncludePatterns,
exclude: [