diff --git a/AGENTS.md b/AGENTS.md index 257b73530d3..15de55245d6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -206,7 +206,7 @@ - Agents MUST NOT modify baseline, inventory, ignore, snapshot, or expected-failure files to silence failing checks without explicit approval in this chat. - For targeted/local debugging, use the native root-project entrypoint: `pnpm test [vitest args...]` (for example `pnpm test src/commands/onboard-search.test.ts -t "shows registered plugin providers"`); do not default to raw `pnpm vitest run ...` because it bypasses the repo's default config/profile/pool routing. - Do not set test workers above 16; tried already. -- Keep Vitest on `forks` only. Do not introduce or reintroduce any non-`forks` Vitest pool or alternate execution mode in configs, wrapper scripts, or default test commands without explicit approval in this chat. This includes `threads`, `vmThreads`, `vmForks`, and any future/nonstandard pool variant. +- Vitest now defaults to native root-project `threads`, with hard `forks` exceptions for `gateway`, `agents`, and `commands`. Keep new pool changes explicit and justified; use `OPENCLAW_VITEST_POOL=forks` for full local fork debugging. - If local Vitest runs cause memory pressure, the default worker budget now derives from host capabilities (CPU, memory band, current load). For a conservative explicit override during land/gate runs, use `OPENCLAW_VITEST_MAX_WORKERS=1 pnpm test`. - Live tests (real keys): `OPENCLAW_LIVE_TEST=1 pnpm test:live` (OpenClaw-only) or `LIVE=1 pnpm test:live` (includes provider live tests). Docker: `pnpm test:docker:live-models`, `pnpm test:docker:live-gateway`. Onboarding Docker E2E: `pnpm test:docker:onboard`. - `pnpm test:live` defaults quiet now. Keep `[live]` progress; suppress profile/gateway chatter. Full logs: `OPENCLAW_LIVE_TEST_QUIET=0 pnpm test:live`. diff --git a/docs/help/testing.md b/docs/help/testing.md index ac7cfa7ccd6..035f8c087dd 100644 --- a/docs/help/testing.md +++ b/docs/help/testing.md @@ -71,13 +71,14 @@ Think of the suites as “increasing realism” (and increasing flakiness/cost): through the real `run.ts` / `compact.ts` paths; helper-only tests are not a sufficient substitute for those integration paths. - Pool note: - - Base Vitest config still defaults to `forks`. - - Unit and boundary projects stay on `forks`. - - Channel, extension, and gateway configs also stay on `forks`. + - Base Vitest config now defaults to `threads`. + - Hard thread exceptions stay on `forks`: `gateway`, `agents`, and `commands`. + - The root UI lane now mirrors the dedicated UI setup more closely: `jsdom`, isolated files, and the standard Vitest runner. - Unit, channel, and extension configs default to `isolate: false` for faster file startup. - `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. + - `OPENCLAW_VITEST_POOL=forks` (or `OPENCLAW_TEST_POOL=forks`) forces a full local fork run when debugging thread-sensitive behavior. - Fast-local iteration note: - `pnpm test:changed` runs the native projects config with `--changed origin/main`. - `pnpm test:max` and `pnpm test:changed:max` keep the same native projects config, just with a higher worker cap. diff --git a/docs/reference/test.md b/docs/reference/test.md index 5e248b23bf7..99513a96825 100644 --- a/docs/reference/test.md +++ b/docs/reference/test.md @@ -14,7 +14,9 @@ title: "Tests" - `pnpm test:coverage:changed`: Runs unit coverage only for files changed since `origin/main`. - `pnpm test:changed`: runs the native Vitest projects config with `--changed origin/main`. The base config treats the projects/config files as `forceRerunTriggers` so wiring changes still rerun broadly when needed. - `pnpm test`: runs the native Vitest root projects config directly. File filters work natively across the configured projects. -- Unit, channel, and extension configs default to `pool: "forks"`. +- Base Vitest config now defaults to `pool: "threads"`. +- Hard thread exceptions stay on `pool: "forks"` for stability: `gateway`, `agents`, and `commands`. +- Use `OPENCLAW_VITEST_POOL=forks pnpm test` (or `OPENCLAW_TEST_POOL=forks pnpm test`) when you want a full local fork run for debugging. - `pnpm test:channels` runs `vitest.channels.config.ts`. - `pnpm test:extensions` runs `vitest.extensions.config.ts`. - `pnpm test:extensions`: runs extension/plugin suites. @@ -40,6 +42,7 @@ For local PR land/gate checks, run: If `pnpm test` flakes on a loaded host, rerun once before treating it as a regression, then isolate with `pnpm test `. For memory-constrained hosts, use: - `OPENCLAW_VITEST_MAX_WORKERS=1 pnpm test` +- `OPENCLAW_VITEST_POOL=forks OPENCLAW_VITEST_MAX_WORKERS=1 pnpm test` - `OPENCLAW_VITEST_FS_MODULE_CACHE_PATH=/tmp/openclaw-vitest-cache pnpm test:changed` ## Model latency bench (local keys) diff --git a/src/infra/vitest-config.test.ts b/src/infra/vitest-config.test.ts index a1073d2791b..500f9afdd38 100644 --- a/src/infra/vitest-config.test.ts +++ b/src/infra/vitest-config.test.ts @@ -1,6 +1,9 @@ import { readFileSync } from "node:fs"; import { describe, expect, it } from "vitest"; -import baseConfig, { resolveLocalVitestMaxWorkers } from "../../vitest.config.ts"; +import baseConfig, { + resolveDefaultVitestPool, + resolveLocalVitestMaxWorkers, +} from "../../vitest.config.ts"; describe("resolveLocalVitestMaxWorkers", () => { it("uses a moderate local worker cap on larger hosts", () => { @@ -89,6 +92,19 @@ describe("resolveLocalVitestMaxWorkers", () => { }); describe("base vitest config", () => { + it("defaults the base pool to threads", () => { + expect(resolveDefaultVitestPool()).toBe("threads"); + expect(baseConfig.test?.pool).toBe("threads"); + }); + + it("lets OPENCLAW_VITEST_POOL force forks for local debugging", () => { + expect( + resolveDefaultVitestPool({ + OPENCLAW_VITEST_POOL: "forks", + }), + ).toBe("forks"); + }); + it("excludes fixture trees from test collection", () => { expect(baseConfig.test?.exclude).toContain("test/fixtures/**"); }); diff --git a/test/scripts/test-extension.test.ts b/test/scripts/test-extension.test.ts index 9418fdecada..6198ae9eb48 100644 --- a/test/scripts/test-extension.test.ts +++ b/test/scripts/test-extension.test.ts @@ -146,7 +146,9 @@ describe("scripts/test-extension.mjs", () => { resolveExtensionTestPlan({ cwd: process.cwd(), targetArg: extensionId }).hasTests, ); - expect(uniqueAssigned.toSorted((left, right) => left.localeCompare(right))).toEqual(expected); + expect(uniqueAssigned.toSorted((left, right) => left.localeCompare(right))).toEqual( + expected.toSorted((left, right) => left.localeCompare(right)), + ); expect(assigned).toHaveLength(expected.length); const totals = shards.map((shard) => shard.testFileCount); diff --git a/test/vitest-projects-config.test.ts b/test/vitest-projects-config.test.ts index 846655def6b..1a513debeb6 100644 --- a/test/vitest-projects-config.test.ts +++ b/test/vitest-projects-config.test.ts @@ -1,8 +1,26 @@ import { describe, expect, it } from "vitest"; +import { createAgentsVitestConfig } from "../vitest.agents.config.ts"; +import { createCommandsVitestConfig } from "../vitest.commands.config.ts"; import baseConfig, { rootVitestProjects } from "../vitest.config.ts"; +import { createGatewayVitestConfig } from "../vitest.gateway.config.ts"; +import { createUiVitestConfig } from "../vitest.ui.config.ts"; describe("projects vitest config", () => { it("defines the native root project list for all non-live Vitest lanes", () => { expect(baseConfig.test?.projects).toEqual([...rootVitestProjects]); }); + + it("keeps hard thread exceptions on forks", () => { + expect(createGatewayVitestConfig().test.pool).toBe("forks"); + expect(createAgentsVitestConfig().test.pool).toBe("forks"); + expect(createCommandsVitestConfig().test.pool).toBe("forks"); + }); + + it("keeps the root ui lane aligned with the jsdom ui project setup", () => { + const config = createUiVitestConfig(); + expect(config.test.environment).toBe("jsdom"); + expect(config.test.isolate).toBe(true); + expect(config.test.runner).toBeUndefined(); + expect(config.test.setupFiles).toContain("ui/src/test-helpers/lit-warnings.setup.ts"); + }); }); diff --git a/vitest.agents.config.ts b/vitest.agents.config.ts index 30a867c875e..4a4a0e39427 100644 --- a/vitest.agents.config.ts +++ b/vitest.agents.config.ts @@ -5,6 +5,7 @@ export function createAgentsVitestConfig(env?: Record; + environment?: string; exclude?: string[]; + isolate?: boolean; name?: string; pool?: "threads" | "forks"; passWithNoTests?: boolean; setupFiles?: string[]; + useNonIsolatedRunner?: boolean; }, ) { const base = sharedVitestConfig as Record; @@ -56,16 +59,26 @@ export function createScopedVitestConfig( [...(baseTest.exclude ?? []), ...(options?.exclude ?? [])], scopedDir, ); - const isolate = resolveVitestIsolation(options?.env); + const isolate = options?.isolate ?? resolveVitestIsolation(options?.env); + const setupFiles = [ + ...new Set([ + ...(baseTest.setupFiles ?? []), + ...(options?.setupFiles ?? []), + "test/setup-openclaw-runtime.ts", + ]), + ]; return defineConfig({ ...base, test: { ...baseTest, ...(options?.name ? { name: options.name } : {}), + ...(options?.environment ? { environment: options.environment } : {}), isolate, - runner: "./test/non-isolated-runner.ts", - setupFiles: [...new Set([...(baseTest.setupFiles ?? []), "test/setup-openclaw-runtime.ts"])], + ...(options?.useNonIsolatedRunner === false + ? {} + : { runner: "./test/non-isolated-runner.ts" }), + setupFiles, ...(scopedDir ? { dir: scopedDir } : {}), include: relativizeScopedPatterns(include, scopedDir), exclude, @@ -73,7 +86,6 @@ export function createScopedVitestConfig( ...(options?.passWithNoTests !== undefined ? { passWithNoTests: options.passWithNoTests } : {}), - ...(options?.setupFiles ? { setupFiles: options.setupFiles } : {}), }, }); } diff --git a/vitest.shared.config.ts b/vitest.shared.config.ts index 426dea93c0e..d406be4602f 100644 --- a/vitest.shared.config.ts +++ b/vitest.shared.config.ts @@ -21,6 +21,8 @@ type VitestHostInfo = { totalMemoryBytes?: number; }; +export type OpenClawVitestPool = "threads" | "forks"; + function detectVitestHostInfo(): Required { return { cpuCount: @@ -68,11 +70,22 @@ export function resolveLocalVitestMaxWorkers( return clamp(inferred, 1, 16); } +export function resolveDefaultVitestPool( + env: Record = process.env, +): OpenClawVitestPool { + const configuredPool = (env.OPENCLAW_VITEST_POOL ?? env.OPENCLAW_TEST_POOL)?.trim(); + if (configuredPool === "threads" || configuredPool === "forks") { + return configuredPool; + } + return "threads"; +} + 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; +const defaultPool = resolveDefaultVitestPool(); export const sharedVitestConfig = { resolve: { @@ -96,7 +109,7 @@ export const sharedVitestConfig = { hookTimeout: isWindows ? 180_000 : 120_000, unstubEnvs: true, unstubGlobals: true, - pool: "forks" as const, + pool: defaultPool, maxWorkers: isCI ? ciWorkers : localWorkers, forceRerunTriggers: [ "package.json", diff --git a/vitest.ui.config.ts b/vitest.ui.config.ts index 31af2db1dd6..ade5caeb98a 100644 --- a/vitest.ui.config.ts +++ b/vitest.ui.config.ts @@ -3,8 +3,12 @@ import { createScopedVitestConfig } from "./vitest.scoped-config.ts"; export function createUiVitestConfig(env?: Record) { return createScopedVitestConfig(["ui/src/ui/**/*.test.ts"], { dir: "ui/src/ui", + environment: "jsdom", env, + isolate: true, name: "ui", + setupFiles: ["ui/src/test-helpers/lit-warnings.setup.ts"], + useNonIsolatedRunner: false, }); }