diff --git a/docs/help/testing.md b/docs/help/testing.md index ce8dc85a926..1fe0be9a511 100644 --- a/docs/help/testing.md +++ b/docs/help/testing.md @@ -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: diff --git a/scripts/test-projects.test-support.mjs b/scripts/test-projects.test-support.mjs index 66fa282e59d..e9b9ec6f0b1 100644 --- a/scripts/test-projects.test-support.mjs +++ b/scripts/test-projects.test-support.mjs @@ -23,7 +23,7 @@ export function buildVitestArgs(args) { "vitest", ...(watchMode ? [] : ["run"]), "--config", - "vitest.projects.config.ts", + "vitest.config.ts", ...forwardedArgs, ]; } diff --git a/test/scripts/test-projects.test.ts b/test/scripts/test-projects.test.ts index cf7af0ba61e..207c7bdfe0b 100644 --- a/test/scripts/test-projects.test.ts +++ b/test/scripts/test-projects.test.ts @@ -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", ]); }); diff --git a/test/setup-openclaw-runtime.ts b/test/setup-openclaw-runtime.ts index d5d60533945..5f512f659c6 100644 --- a/test/setup-openclaw-runtime.ts +++ b/test/setup-openclaw-runtime.ts @@ -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 { + 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; + }; + 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) | undefined; }; diff --git a/test/vitest-config.test.ts b/test/vitest-config.test.ts index ec41399a2c4..5f52c0166bc 100644 --- a/test/vitest-config.test.ts +++ b/test/vitest-config.test.ts @@ -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); }); }); diff --git a/test/vitest-projects-config.test.ts b/test/vitest-projects-config.test.ts index 734f99c950d..c6b6c825ec5 100644 --- a/test/vitest-projects-config.test.ts +++ b/test/vitest-projects-config.test.ts @@ -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", ]); }); }); diff --git a/vitest.boundary.config.ts b/vitest.boundary.config.ts index 502ae15b370..935787c1fe4 100644 --- a/vitest.boundary.config.ts +++ b/vitest.boundary.config.ts @@ -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; -const baseTest = - ( - baseConfig as { - test?: { - exclude?: string[]; - }; - } - ).test ?? {}; - export function loadBoundaryIncludePatternsFromEnv( env: Record = process.env, ): string[] | null { @@ -20,10 +10,11 @@ export function loadBoundaryIncludePatternsFromEnv( } export function createBoundaryVitestConfig(env: Record = 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, diff --git a/vitest.config.ts b/vitest.config.ts index 3293583112f..ea030fdc08b 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -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"], }, }); diff --git a/vitest.projects.config.ts b/vitest.projects.config.ts deleted file mode 100644 index cd9c03e8878..00000000000 --- a/vitest.projects.config.ts +++ /dev/null @@ -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; -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, - }, - }, - ], - }, -}); diff --git a/vitest.scoped-config.ts b/vitest.scoped-config.ts index 7315bc2bd9c..d98557fcac7 100644 --- a/vitest.scoped-config.ts +++ b/vitest.scoped-config.ts @@ -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; - const baseTest = - ( - baseConfig as { - test?: { - dir?: string; - exclude?: string[]; - pool?: "threads" | "forks"; - passWithNoTests?: boolean; - setupFiles?: string[]; - }; - } - ).test ?? {}; + const base = sharedVitestConfig as Record; + const baseTest = sharedVitestConfig.test ?? {}; const scopedDir = options?.dir; const exclude = relativizeScopedPatterns( [...(baseTest.exclude ?? []), ...(options?.exclude ?? [])], diff --git a/vitest.shared.config.ts b/vitest.shared.config.ts new file mode 100644 index 00000000000..f1bebf0fe47 --- /dev/null +++ b/vitest.shared.config.ts @@ -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 { + return { + cpuCount: + typeof os.availableParallelism === "function" ? os.availableParallelism() : os.cpus().length, + totalMemoryBytes: os.totalmem(), + }; +} + +export function resolveLocalVitestMaxWorkers( + env: Record = 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(), + }, +}; diff --git a/vitest.unit.config.ts b/vitest.unit.config.ts index 90321e38050..7ca1082faad 100644 --- a/vitest.unit.config.ts +++ b/vitest.unit.config.ts @@ -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; -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 = 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: [