test: default vitest root projects to threads

This commit is contained in:
Peter Steinberger
2026-04-04 04:30:17 +01:00
parent fb5066dfb1
commit bb1cc84d50
13 changed files with 90 additions and 14 deletions

View File

@@ -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 <path-or-filter> [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`.

View File

@@ -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.

View File

@@ -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 <path/to/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)

View File

@@ -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/**");
});

View File

@@ -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);

View File

@@ -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");
});
});

View File

@@ -5,6 +5,7 @@ export function createAgentsVitestConfig(env?: Record<string, string | undefined
dir: "src/agents",
env,
name: "agents",
pool: "forks",
});
}

View File

@@ -5,6 +5,7 @@ export function createCommandsVitestConfig(env?: Record<string, string | undefin
dir: "src/commands",
env,
name: "commands",
pool: "forks",
});
}

View File

@@ -1,7 +1,11 @@
import { defineConfig } from "vitest/config";
import { resolveLocalVitestMaxWorkers, sharedVitestConfig } from "./vitest.shared.config.ts";
import {
resolveDefaultVitestPool,
resolveLocalVitestMaxWorkers,
sharedVitestConfig,
} from "./vitest.shared.config.ts";
export { resolveLocalVitestMaxWorkers };
export { resolveDefaultVitestPool, resolveLocalVitestMaxWorkers };
export const rootVitestProjects = [
"vitest.unit.config.ts",

View File

@@ -5,6 +5,7 @@ export function createGatewayVitestConfig(env?: Record<string, string | undefine
dir: "src/gateway",
env,
name: "gateway",
pool: "forks",
});
}

View File

@@ -42,11 +42,14 @@ export function createScopedVitestConfig(
options?: {
dir?: string;
env?: Record<string, string | undefined>;
environment?: string;
exclude?: string[];
isolate?: boolean;
name?: string;
pool?: "threads" | "forks";
passWithNoTests?: boolean;
setupFiles?: string[];
useNonIsolatedRunner?: boolean;
},
) {
const base = sharedVitestConfig as Record<string, unknown>;
@@ -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 } : {}),
},
});
}

View File

@@ -21,6 +21,8 @@ type VitestHostInfo = {
totalMemoryBytes?: number;
};
export type OpenClawVitestPool = "threads" | "forks";
function detectVitestHostInfo(): Required<VitestHostInfo> {
return {
cpuCount:
@@ -68,11 +70,22 @@ export function resolveLocalVitestMaxWorkers(
return clamp(inferred, 1, 16);
}
export function resolveDefaultVitestPool(
env: Record<string, string | undefined> = 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",

View File

@@ -3,8 +3,12 @@ import { createScopedVitestConfig } from "./vitest.scoped-config.ts";
export function createUiVitestConfig(env?: Record<string, string | undefined>) {
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,
});
}