mirror of https://github.com/openclaw/openclaw.git
refactor: modernize vitest projects config
This commit is contained in:
parent
9dba944c42
commit
2b900b576c
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ export function buildVitestArgs(args) {
|
|||
"vitest",
|
||||
...(watchMode ? [] : ["run"]),
|
||||
"--config",
|
||||
"vitest.projects.config.ts",
|
||||
"vitest.config.ts",
|
||||
...forwardedArgs,
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
]);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
206
vitest.config.ts
206
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"],
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
|
@ -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 ?? [])],
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
},
|
||||
};
|
||||
|
|
@ -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: [
|
||||
|
|
|
|||
Loading…
Reference in New Issue