perf(test): shard full vitest runs

This commit is contained in:
Peter Steinberger
2026-04-06 17:34:07 +01:00
parent a47cb0a3b3
commit 0335a8783c
20 changed files with 522 additions and 135 deletions

View File

@@ -46,7 +46,7 @@ Think of the suites as “increasing realism” (and increasing flakiness/cost):
### Unit / integration (default)
- Command: `pnpm test`
- Config: native Vitest `projects` via `vitest.config.ts`
- Config: five sequential shard runs (`vitest.full-*.config.ts`) over the existing scoped Vitest projects
- 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
@@ -57,11 +57,13 @@ Think of the suites as “increasing realism” (and increasing flakiness/cost):
- No real keys required
- Should be fast and stable
- Projects note:
- Untargeted `pnpm test` still uses the native Vitest root `projects` config.
- Untargeted `pnpm test` now runs five smaller shard configs (`core-unit`, `core-runtime`, `agentic`, `auto-reply`, `extensions`) instead of one giant native root-project process. This cuts peak RSS on loaded machines and avoids auto-reply/extension work starving unrelated suites.
- `pnpm test --watch` still uses the native root `vitest.config.ts` project graph, because a multi-shard watch loop is not practical.
- `pnpm test`, `pnpm test:watch`, and `pnpm test:perf:imports` route explicit file/directory targets through scoped lanes first, so `pnpm test extensions/discord/src/monitor/message-handler.preflight.test.ts` avoids paying the full root project startup tax.
- `pnpm test:changed` expands changed git paths into the same scoped lanes when the diff only touches routable source/test files; config/setup edits still fall back to the broad root-project rerun.
- Selected `plugin-sdk` and `commands` tests also route through dedicated light lanes that skip `test/setup-openclaw-runtime.ts`; stateful/runtime-heavy files stay on the existing lanes.
- Selected `plugin-sdk` and `commands` helper source files also map changed-mode runs to explicit sibling tests in those light lanes, so helper edits avoid rerunning the full heavy suite for that directory.
- `auto-reply` now has three dedicated buckets: top-level core helpers, top-level `reply.*` integration tests, and the `src/auto-reply/reply/**` subtree. This keeps the heaviest reply harness work off the cheap status/chunk/token tests.
- Embedded runner note:
- When you change message-tool discovery inputs or compaction runtime context,
keep both levels of coverage.
@@ -77,7 +79,7 @@ Think of the suites as “increasing realism” (and increasing flakiness/cost):
- Base Vitest config now defaults to `threads`.
- The shared Vitest config also fixes `isolate: false` and uses the non-isolated runner across the root projects, e2e, and live configs.
- The root UI lane keeps its `jsdom` setup and optimizer, but now runs on the shared non-isolated runner too.
- `pnpm test` inherits the same `threads` + `isolate: false` defaults from the root `vitest.config.ts` projects config.
- Each `pnpm test` shard inherits the same `threads` + `isolate: false` defaults from the shared Vitest config.
- The shared `scripts/run-vitest.mjs` launcher now also adds `--no-maglev` for Vitest child Node processes by default to reduce V8 compile churn during big local runs. Set `OPENCLAW_VITEST_ENABLE_MAGLEV=1` if you need to compare against stock V8 behavior.
- Fast-local iteration note:
- `pnpm test:changed` routes through scoped lanes when the changed paths map cleanly to a smaller suite.

View File

@@ -13,9 +13,10 @@ title: "Tests"
- `pnpm test:coverage`: Runs the unit suite with V8 coverage (via `vitest.unit.config.ts`). Global thresholds are 70% lines/branches/functions/statements. Coverage excludes integration-heavy entrypoints (CLI wiring, gateway/telegram bridges, webchat static server) to keep the target focused on unit-testable logic.
- `pnpm test:coverage:changed`: Runs unit coverage only for files changed since `origin/main`.
- `pnpm test:changed`: expands changed git paths into scoped Vitest lanes when the diff only touches routable source/test files. Config/setup changes still fall back to the native root projects run so wiring edits rerun broadly when needed.
- `pnpm test`: routes explicit file/directory targets through scoped Vitest lanes, but still falls back to the native root projects run when you do a full untargeted sweep.
- `pnpm test`: routes explicit file/directory targets through scoped Vitest lanes. Untargeted runs now execute five sequential shard configs (`vitest.full-core-unit.config.ts`, `vitest.full-core-runtime.config.ts`, `vitest.full-agentic.config.ts`, `vitest.full-auto-reply.config.ts`, `vitest.full-extensions.config.ts`) instead of one giant root-project process.
- Selected `plugin-sdk` and `commands` test files now route through dedicated light lanes that keep only `test/setup.ts`, leaving runtime-heavy cases on their existing lanes.
- Selected `plugin-sdk` and `commands` helper source files also map `pnpm test:changed` to explicit sibling tests in those light lanes, so small helper edits avoid rerunning the heavy runtime-backed suites.
- `auto-reply` now also splits into three dedicated configs (`core`, `top-level`, `reply`) so the reply harness does not dominate the lighter top-level status/token/helper tests.
- Base Vitest config now defaults to `pool: "threads"` and `isolate: false`, with the shared non-isolated runner enabled across the repo configs.
- `pnpm test:channels` runs `vitest.channels.config.ts`.
- `pnpm test:extensions` runs `vitest.extensions.config.ts`.

View File

@@ -3,6 +3,7 @@ import { acquireLocalHeavyCheckLockSync } from "./lib/local-heavy-check-runtime.
import { spawnPnpmRunner } from "./pnpm-runner.mjs";
import { resolveVitestCliEntry, resolveVitestNodeArgs } from "./run-vitest.mjs";
import {
buildFullSuiteVitestRunPlans,
createVitestRunSpecs,
parseTestProjectsArgs,
resolveChangedTargetArgs,
@@ -59,27 +60,6 @@ function runVitestSpec(spec) {
});
}
function createRootVitestRunSpec(args) {
const { forwardedArgs, watchMode } = parseTestProjectsArgs(args, process.cwd());
return {
config: "vitest.config.ts",
env: process.env,
includeFilePath: null,
includePatterns: null,
pnpmArgs: [
"exec",
"node",
...resolveVitestNodeArgs(process.env),
resolveVitestCliEntry(),
...(watchMode ? [] : ["run"]),
"--config",
"vitest.config.ts",
...forwardedArgs,
],
watchMode,
};
}
async function main() {
const args = process.argv.slice(2);
const { targetArgs } = parseTestProjectsArgs(args, process.cwd());
@@ -87,12 +67,30 @@ async function main() {
targetArgs.length === 0 ? resolveChangedTargetArgs(args, process.cwd()) : null;
const runSpecs =
targetArgs.length === 0 && changedTargetArgs === null
? [createRootVitestRunSpec(args)]
? buildFullSuiteVitestRunPlans(args, process.cwd()).map((plan) => ({
config: plan.config,
continueOnFailure: true,
env: process.env,
includeFilePath: null,
includePatterns: null,
pnpmArgs: [
"exec",
"node",
...resolveVitestNodeArgs(process.env),
resolveVitestCliEntry(),
...(plan.watchMode ? [] : ["run"]),
"--config",
plan.config,
...plan.forwardedArgs,
],
watchMode: plan.watchMode,
}))
: createVitestRunSpecs(args, {
baseEnv: process.env,
cwd: process.cwd(),
});
let exitCode = 0;
for (const spec of runSpecs) {
const result = await runVitestSpec(spec);
if (result.signal) {
@@ -101,12 +99,18 @@ async function main() {
return;
}
if (result.code !== 0) {
releaseLockOnce();
process.exit(result.code);
exitCode = exitCode || result.code;
if (spec.continueOnFailure !== true) {
releaseLockOnce();
process.exit(result.code);
}
}
}
releaseLockOnce();
if (exitCode !== 0) {
process.exit(exitCode);
}
}
main().catch((error) => {

View File

@@ -26,6 +26,7 @@ import {
isPluginSdkLightTarget,
resolvePluginSdkLightIncludePattern,
} from "../vitest.plugin-sdk-paths.mjs";
import { fullSuiteVitestShards } from "../vitest.test-shards.mjs";
import { isBoundaryTestFile, isBundledPluginDependentUnitTestFile } from "../vitest.unit-paths.mjs";
import { resolveVitestCliEntry, resolveVitestNodeArgs } from "./run-vitest.mjs";
@@ -652,6 +653,26 @@ export function buildVitestRunPlans(
return plans;
}
export function buildFullSuiteVitestRunPlans(args, cwd = process.cwd()) {
const { forwardedArgs, watchMode } = parseTestProjectsArgs(args, cwd);
if (watchMode) {
return [
{
config: "vitest.config.ts",
forwardedArgs,
includePatterns: null,
watchMode,
},
];
}
return fullSuiteVitestShards.map((shard) => ({
config: shard.config,
forwardedArgs,
includePatterns: null,
watchMode: false,
}));
}
export function createVitestRunSpecs(args, params = {}) {
const cwd = params.cwd ?? process.cwd();
const plans = buildVitestRunPlans(args, cwd);

View File

@@ -449,10 +449,10 @@ describe("runCliAgent spawn path", () => {
};
expect(input.env?.SAFE_KEEP).toBe("ok");
expect(input.env?.CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST).toBe("1");
expect(input.env?.ANTHROPIC_BASE_URL).toBeUndefined();
expect(input.env?.ANTHROPIC_BASE_URL).toBe("https://override.example.com/v1");
expect(input.env?.CLAUDE_CODE_USE_BEDROCK).toBeUndefined();
expect(input.env?.ANTHROPIC_AUTH_TOKEN).toBeUndefined();
expect(input.env?.CLAUDE_CODE_OAUTH_TOKEN).toBeUndefined();
expect(input.env?.CLAUDE_CODE_OAUTH_TOKEN).toBe("override-oauth-token");
expect(input.env?.CLAUDE_CODE_REMOTE).toBeUndefined();
expect(input.env?.ANTHROPIC_UNIX_SOCKET).toBeUndefined();
expect(input.env?.OTEL_LOGS_EXPORTER).toBeUndefined();

View File

@@ -1,7 +1,6 @@
import fs from "node:fs/promises";
import type { Mock } from "vitest";
import { beforeEach, vi } from "vitest";
import { buildAnthropicCliBackend } from "../../extensions/anthropic/test-api.js";
import type { OpenClawConfig } from "../config/config.js";
import type { requestHeartbeatNow } from "../infra/heartbeat-wake.js";
import type { enqueueSystemEvent } from "../infra/system-events.js";
@@ -145,6 +144,103 @@ function buildOpenAICodexCliBackendFixture(): CliBackendPlugin {
};
}
function buildAnthropicCliBackendFixture(): CliBackendPlugin {
return {
id: "claude-cli",
bundleMcp: true,
config: {
command: "claude",
args: [
"-p",
"--output-format",
"stream-json",
"--include-partial-messages",
"--verbose",
"--setting-sources",
"user",
"--permission-mode",
"bypassPermissions",
],
resumeArgs: [
"-p",
"--output-format",
"stream-json",
"--include-partial-messages",
"--verbose",
"--setting-sources",
"user",
"--permission-mode",
"bypassPermissions",
"--resume",
"{sessionId}",
],
output: "jsonl",
input: "stdin",
modelArg: "--model",
modelAliases: {
opus: "opus",
"claude-opus-4-6": "opus",
sonnet: "sonnet",
"claude-sonnet-4-6": "sonnet",
haiku: "haiku",
},
sessionArg: "--session-id",
sessionMode: "always",
sessionIdFields: ["session_id", "sessionId", "conversation_id", "conversationId"],
systemPromptArg: "--append-system-prompt",
systemPromptMode: "append",
systemPromptWhen: "first",
env: {
CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST: "1",
},
clearEnv: [
"ANTHROPIC_API_KEY",
"ANTHROPIC_API_KEY_OLD",
"ANTHROPIC_AUTH_TOKEN",
"ANTHROPIC_BASE_URL",
"ANTHROPIC_UNIX_SOCKET",
"CLAUDE_CONFIG_DIR",
"CLAUDE_CODE_API_KEY_FILE_DESCRIPTOR",
"CLAUDE_CODE_ENTRYPOINT",
"CLAUDE_CODE_OAUTH_REFRESH_TOKEN",
"CLAUDE_CODE_OAUTH_SCOPES",
"CLAUDE_CODE_OAUTH_TOKEN",
"CLAUDE_CODE_OAUTH_TOKEN_FILE_DESCRIPTOR",
"CLAUDE_CODE_PLUGIN_CACHE_DIR",
"CLAUDE_CODE_PLUGIN_SEED_DIR",
"CLAUDE_CODE_REMOTE",
"CLAUDE_CODE_USE_COWORK_PLUGINS",
"CLAUDE_CODE_USE_BEDROCK",
"CLAUDE_CODE_USE_FOUNDRY",
"CLAUDE_CODE_USE_VERTEX",
"OTEL_EXPORTER_OTLP_ENDPOINT",
"OTEL_EXPORTER_OTLP_HEADERS",
"OTEL_EXPORTER_OTLP_LOGS_ENDPOINT",
"OTEL_EXPORTER_OTLP_LOGS_HEADERS",
"OTEL_EXPORTER_OTLP_LOGS_PROTOCOL",
"OTEL_EXPORTER_OTLP_METRICS_ENDPOINT",
"OTEL_EXPORTER_OTLP_METRICS_HEADERS",
"OTEL_EXPORTER_OTLP_METRICS_PROTOCOL",
"OTEL_EXPORTER_OTLP_PROTOCOL",
"OTEL_EXPORTER_OTLP_TRACES_ENDPOINT",
"OTEL_EXPORTER_OTLP_TRACES_HEADERS",
"OTEL_EXPORTER_OTLP_TRACES_PROTOCOL",
"OTEL_LOGS_EXPORTER",
"OTEL_METRICS_EXPORTER",
"OTEL_SDK_DISABLED",
"OTEL_TRACES_EXPORTER",
],
reliability: {
watchdog: {
fresh: { ...CLI_FRESH_WATCHDOG_DEFAULTS },
resume: { ...CLI_RESUME_WATCHDOG_DEFAULTS },
},
},
serialize: true,
},
};
}
function buildGoogleGeminiCliBackendFixture(): CliBackendPlugin {
return {
id: "google-gemini-cli",
@@ -224,7 +320,7 @@ export async function setupCliRunnerTestModule() {
registry.cliBackends = [
{
pluginId: "anthropic",
backend: buildAnthropicCliBackend(),
backend: buildAnthropicCliBackendFixture(),
source: "test",
},
{

View File

@@ -3,6 +3,66 @@ import os from "node:os";
import path from "node:path";
import { afterAll, beforeAll, vi, type Mock } from "vitest";
export type ReplyRuntimeMocks = {
runEmbeddedPiAgent: Mock;
loadModelCatalog: Mock;
webAuthExists: Mock;
getWebAuthAgeMs: Mock;
readWebSelfId: Mock;
};
const replyRuntimeMockState = vi.hoisted(() => ({
mocks: {
runEmbeddedPiAgent: vi.fn(),
loadModelCatalog: vi.fn(),
webAuthExists: vi.fn().mockResolvedValue(true),
getWebAuthAgeMs: vi.fn().mockReturnValue(120_000),
readWebSelfId: vi.fn().mockReturnValue({ e164: "+1999" }),
} as ReplyRuntimeMocks,
}));
vi.mock("../agents/pi-embedded.js", () => ({
abortEmbeddedPiRun: vi.fn().mockReturnValue(false),
runEmbeddedPiAgent: (...args: unknown[]) =>
replyRuntimeMockState.mocks.runEmbeddedPiAgent(...args),
queueEmbeddedPiMessage: vi.fn().mockReturnValue(false),
resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`,
isEmbeddedPiRunActive: vi.fn().mockReturnValue(false),
isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false),
}));
vi.mock("../agents/model-catalog.runtime.js", () => ({
loadModelCatalog: (...args: unknown[]) => replyRuntimeMockState.mocks.loadModelCatalog(...args),
}));
vi.mock("../agents/auth-profiles/session-override.js", () => ({
clearSessionAuthProfileOverride: vi.fn(),
resolveSessionAuthProfileOverride: vi.fn().mockResolvedValue(undefined),
}));
vi.mock("../commands-registry.runtime.js", () => ({
listChatCommands: () => [],
}));
vi.mock("../skill-commands.runtime.js", () => ({
listSkillCommandsForWorkspace: () => [],
}));
vi.mock("../plugins/runtime/runtime-web-channel-plugin.js", () => ({
webAuthExists: (...args: unknown[]) => replyRuntimeMockState.mocks.webAuthExists(...args),
getWebAuthAgeMs: (...args: unknown[]) => replyRuntimeMockState.mocks.getWebAuthAgeMs(...args),
readWebSelfId: (...args: unknown[]) => replyRuntimeMockState.mocks.readWebSelfId(...args),
}));
vi.mock("../agents/pi-embedded.runtime.js", () => ({
abortEmbeddedPiRun: vi.fn().mockReturnValue(false),
isEmbeddedPiRunActive: vi.fn().mockReturnValue(false),
isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false),
resolveActiveEmbeddedRunSessionId: vi.fn().mockReturnValue(undefined),
resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`,
waitForEmbeddedPiRunEnd: vi.fn(async () => undefined),
}));
type HomeEnvSnapshot = {
HOME: string | undefined;
USERPROFILE: string | undefined;
@@ -96,14 +156,6 @@ export function makeReplyConfig(home: string) {
};
}
export type ReplyRuntimeMocks = {
runEmbeddedPiAgent: Mock;
loadModelCatalog: Mock;
webAuthExists: Mock;
getWebAuthAgeMs: Mock;
readWebSelfId: Mock;
};
export function createReplyRuntimeMocks(): ReplyRuntimeMocks {
return {
runEmbeddedPiAgent: vi.fn(),
@@ -115,37 +167,7 @@ export function createReplyRuntimeMocks(): ReplyRuntimeMocks {
}
export function installReplyRuntimeMocks(mocks: ReplyRuntimeMocks) {
vi.mock("../agents/pi-embedded.js", () => ({
abortEmbeddedPiRun: vi.fn().mockReturnValue(false),
runEmbeddedPiAgent: (...args: unknown[]) => mocks.runEmbeddedPiAgent(...args),
queueEmbeddedPiMessage: vi.fn().mockReturnValue(false),
resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`,
isEmbeddedPiRunActive: vi.fn().mockReturnValue(false),
isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false),
}));
vi.mock("../agents/model-catalog.runtime.js", () => ({
loadModelCatalog: mocks.loadModelCatalog,
}));
vi.mock("../agents/auth-profiles/session-override.js", () => ({
clearSessionAuthProfileOverride: vi.fn(),
resolveSessionAuthProfileOverride: vi.fn().mockResolvedValue(undefined),
}));
vi.mock("../commands-registry.runtime.js", () => ({
listChatCommands: () => [],
}));
vi.mock("../skill-commands.runtime.js", () => ({
listSkillCommandsForWorkspace: () => [],
}));
vi.mock("../plugins/runtime/runtime-web-channel-plugin.js", () => ({
webAuthExists: mocks.webAuthExists,
getWebAuthAgeMs: mocks.getWebAuthAgeMs,
readWebSelfId: mocks.readWebSelfId,
}));
replyRuntimeMockState.mocks = mocks;
}
export function resetReplyRuntimeMocks(mocks: ReplyRuntimeMocks) {

View File

@@ -1,67 +1,80 @@
import { vi } from "vitest";
import { createMockTypingController } from "./reply.test-helpers.js";
export function registerGetReplyCommonMocks(): void {
vi.mock("../../agents/agent-scope.js", async () => {
const actual = await vi.importActual<typeof import("../../agents/agent-scope.js")>(
"../../agents/agent-scope.js",
);
return {
...actual,
resolveAgentDir: vi.fn(() => "/tmp/agent"),
resolveAgentWorkspaceDir: vi.fn(() => "/tmp/workspace"),
resolveSessionAgentId: vi.fn(() => "main"),
resolveAgentSkillsFilter: vi.fn(() => undefined),
};
});
vi.mock("../../agents/model-selection.js", async () => {
const actual = await vi.importActual<typeof import("../../agents/model-selection.js")>(
"../../agents/model-selection.js",
);
return {
...actual,
resolveModelRefFromString: vi.fn(() => null),
};
});
vi.mock("../../agents/timeout.js", () => ({
resolveAgentTimeoutMs: vi.fn(() => 60000),
}));
vi.mock("../../agents/workspace.js", () => ({
DEFAULT_AGENT_WORKSPACE_DIR: "/tmp/workspace",
ensureAgentWorkspace: vi.fn(async () => ({ dir: "/tmp/workspace" })),
}));
vi.mock("../../channels/model-overrides.js", () => ({
resolveChannelModelOverride: vi.fn(() => undefined),
}));
vi.mock("../../config/config.js", () => ({
loadConfig: vi.fn(() => ({})),
}));
vi.mock("../../runtime.js", () => ({
defaultRuntime: { log: vi.fn(), error: vi.fn(), warn: vi.fn(), info: vi.fn() },
}));
vi.mock("../command-auth.js", () => ({
resolveCommandAuthorization: vi.fn(() => ({ isAuthorizedSender: true })),
}));
vi.mock("./directive-handling.defaults.js", () => ({
resolveDefaultModel: vi.fn(() => ({
defaultProvider: "openai",
defaultModel: "gpt-4o-mini",
aliasIndex: new Map(),
})),
}));
vi.mock("./get-reply-run.js", () => ({
runPreparedReply: vi.fn(async () => undefined),
}));
vi.mock("./inbound-context.js", () => ({
finalizeInboundContext: vi.fn((ctx: unknown) => ctx),
}));
vi.mock("./session-reset-model.runtime.js", () => ({
applyResetModelOverride: vi.fn(async () => undefined),
}));
vi.mock("./stage-sandbox-media.runtime.js", () => ({
stageSandboxMedia: vi.fn(async () => undefined),
}));
vi.mock("./typing.js", () => ({
createTypingController: vi.fn(() => createMockTypingController()),
}));
}
vi.mock("../../agents/agent-scope.js", async () => {
const actual = await vi.importActual<typeof import("../../agents/agent-scope.js")>(
"../../agents/agent-scope.js",
);
return {
...actual,
resolveAgentDir: vi.fn(() => "/tmp/agent"),
resolveAgentWorkspaceDir: vi.fn(() => "/tmp/workspace"),
resolveSessionAgentId: vi.fn(() => "main"),
resolveAgentSkillsFilter: vi.fn(() => undefined),
};
});
vi.mock("../../agents/model-selection.js", async () => {
const actual = await vi.importActual<typeof import("../../agents/model-selection.js")>(
"../../agents/model-selection.js",
);
return {
...actual,
resolveModelRefFromString: vi.fn(() => null),
};
});
vi.mock("../../agents/timeout.js", () => ({
resolveAgentTimeoutMs: vi.fn(() => 60000),
}));
vi.mock("../../agents/workspace.js", () => ({
DEFAULT_AGENT_WORKSPACE_DIR: "/tmp/workspace",
ensureAgentWorkspace: vi.fn(async () => ({ dir: "/tmp/workspace" })),
}));
vi.mock("../../channels/model-overrides.js", () => ({
resolveChannelModelOverride: vi.fn(() => undefined),
}));
vi.mock("../../config/config.js", () => ({
loadConfig: vi.fn(() => ({})),
}));
vi.mock("../../runtime.js", () => ({
defaultRuntime: { log: vi.fn(), error: vi.fn(), warn: vi.fn(), info: vi.fn() },
}));
vi.mock("../command-auth.js", () => ({
resolveCommandAuthorization: vi.fn(() => ({ isAuthorizedSender: true })),
}));
vi.mock("./directive-handling.defaults.js", () => ({
resolveDefaultModel: vi.fn(() => ({
defaultProvider: "openai",
defaultModel: "gpt-4o-mini",
aliasIndex: new Map(),
})),
}));
vi.mock("./get-reply-run.js", () => ({
runPreparedReply: vi.fn(async () => undefined),
}));
vi.mock("./inbound-context.js", () => ({
finalizeInboundContext: vi.fn((ctx: unknown) => ctx),
}));
vi.mock("./session-reset-model.runtime.js", () => ({
applyResetModelOverride: vi.fn(async () => undefined),
}));
vi.mock("./stage-sandbox-media.runtime.js", () => ({
stageSandboxMedia: vi.fn(async () => undefined),
}));
vi.mock("./typing.js", () => ({
createTypingController: vi.fn(() => createMockTypingController()),
}));
export function registerGetReplyCommonMocks(): void {}

View File

@@ -1,5 +1,6 @@
import { describe, expect, it } from "vitest";
import {
buildFullSuiteVitestRunPlans,
buildVitestRunPlans,
resolveChangedTargetArgs,
} from "../../scripts/test-projects.test-support.mjs";
@@ -158,3 +159,51 @@ describe("scripts/test-projects changed-target routing", () => {
]);
});
});
describe("scripts/test-projects full-suite sharding", () => {
it("splits untargeted runs into fixed shard configs", () => {
expect(buildFullSuiteVitestRunPlans([], process.cwd())).toEqual([
{
config: "vitest.full-core-unit.config.ts",
forwardedArgs: [],
includePatterns: null,
watchMode: false,
},
{
config: "vitest.full-core-runtime.config.ts",
forwardedArgs: [],
includePatterns: null,
watchMode: false,
},
{
config: "vitest.full-agentic.config.ts",
forwardedArgs: [],
includePatterns: null,
watchMode: false,
},
{
config: "vitest.full-auto-reply.config.ts",
forwardedArgs: [],
includePatterns: null,
watchMode: false,
},
{
config: "vitest.full-extensions.config.ts",
forwardedArgs: [],
includePatterns: null,
watchMode: false,
},
]);
});
it("keeps untargeted watch mode on the native root config", () => {
expect(buildFullSuiteVitestRunPlans(["--watch"], process.cwd())).toEqual([
{
config: "vitest.config.ts",
forwardedArgs: [],
includePatterns: null,
watchMode: true,
},
]);
});
});

View File

@@ -4,6 +4,9 @@ import path from "node:path";
import { describe, expect, it } from "vitest";
import { createAcpVitestConfig } from "../vitest.acp.config.ts";
import { createAgentsVitestConfig } from "../vitest.agents.config.ts";
import { createAutoReplyCoreVitestConfig } from "../vitest.auto-reply-core.config.ts";
import { createAutoReplyReplyVitestConfig } from "../vitest.auto-reply-reply.config.ts";
import { createAutoReplyTopLevelVitestConfig } from "../vitest.auto-reply-top-level.config.ts";
import { createAutoReplyVitestConfig } from "../vitest.auto-reply.config.ts";
import { createChannelsVitestConfig } from "../vitest.channels.config.ts";
import { createCliVitestConfig } from "../vitest.cli.config.ts";
@@ -175,6 +178,9 @@ describe("scoped vitest configs", () => {
const defaultCommandsLightConfig = createCommandsLightVitestConfig({});
const defaultCommandsConfig = createCommandsVitestConfig({});
const defaultAutoReplyConfig = createAutoReplyVitestConfig({});
const defaultAutoReplyCoreConfig = createAutoReplyCoreVitestConfig({});
const defaultAutoReplyTopLevelConfig = createAutoReplyTopLevelVitestConfig({});
const defaultAutoReplyReplyConfig = createAutoReplyReplyVitestConfig({});
const defaultAgentsConfig = createAgentsVitestConfig({});
const defaultPluginsConfig = createPluginsVitestConfig({});
const defaultProcessConfig = createProcessVitestConfig({});
@@ -193,6 +199,9 @@ describe("scoped vitest configs", () => {
defaultExtensionProvidersConfig,
defaultInfraConfig,
defaultAutoReplyConfig,
defaultAutoReplyCoreConfig,
defaultAutoReplyTopLevelConfig,
defaultAutoReplyReplyConfig,
defaultToolingConfig,
defaultUiConfig,
]) {
@@ -216,6 +225,15 @@ describe("scoped vitest configs", () => {
]);
});
it("splits auto-reply into narrower scoped buckets", () => {
expect(defaultAutoReplyCoreConfig.test?.include).toEqual(["*.test.ts"]);
expect(defaultAutoReplyCoreConfig.test?.exclude).toEqual(
expect.arrayContaining(["reply*.test.ts"]),
);
expect(defaultAutoReplyTopLevelConfig.test?.include).toEqual(["reply*.test.ts"]);
expect(defaultAutoReplyReplyConfig.test?.include).toEqual(["reply/**/*.test.ts"]);
});
it("keeps selected plugin-sdk and commands light lanes off the openclaw runtime setup", () => {
expect(defaultPluginSdkLightConfig.test?.setupFiles).toEqual(["test/setup.ts"]);
expect(defaultCommandsLightConfig.test?.setupFiles).toEqual(["test/setup.ts"]);

View File

@@ -0,0 +1,13 @@
import { createScopedVitestConfig } from "./vitest.scoped-config.ts";
import { autoReplyCoreTestExclude, autoReplyCoreTestInclude } from "./vitest.test-shards.mjs";
export function createAutoReplyCoreVitestConfig(env?: Record<string, string | undefined>) {
return createScopedVitestConfig([...autoReplyCoreTestInclude], {
dir: "src/auto-reply",
env,
exclude: [...autoReplyCoreTestExclude],
name: "auto-reply-core",
});
}
export default createAutoReplyCoreVitestConfig();

View File

@@ -0,0 +1,12 @@
import { createScopedVitestConfig } from "./vitest.scoped-config.ts";
import { autoReplyReplySubtreeTestInclude } from "./vitest.test-shards.mjs";
export function createAutoReplyReplyVitestConfig(env?: Record<string, string | undefined>) {
return createScopedVitestConfig([...autoReplyReplySubtreeTestInclude], {
dir: "src/auto-reply",
env,
name: "auto-reply-reply",
});
}
export default createAutoReplyReplyVitestConfig();

View File

@@ -0,0 +1,12 @@
import { createScopedVitestConfig } from "./vitest.scoped-config.ts";
import { autoReplyTopLevelReplyTestInclude } from "./vitest.test-shards.mjs";
export function createAutoReplyTopLevelVitestConfig(env?: Record<string, string | undefined>) {
return createScopedVitestConfig([...autoReplyTopLevelReplyTestInclude], {
dir: "src/auto-reply",
env,
name: "auto-reply-top-level",
});
}
export default createAutoReplyTopLevelVitestConfig();

View File

@@ -0,0 +1,4 @@
import { createProjectShardVitestConfig } from "./vitest.project-shard-config.ts";
import { fullSuiteVitestShards } from "./vitest.test-shards.mjs";
export default createProjectShardVitestConfig(fullSuiteVitestShards[2].projects);

View File

@@ -0,0 +1,4 @@
import { createProjectShardVitestConfig } from "./vitest.project-shard-config.ts";
import { fullSuiteVitestShards } from "./vitest.test-shards.mjs";
export default createProjectShardVitestConfig(fullSuiteVitestShards[3].projects);

View File

@@ -0,0 +1,4 @@
import { createProjectShardVitestConfig } from "./vitest.project-shard-config.ts";
import { fullSuiteVitestShards } from "./vitest.test-shards.mjs";
export default createProjectShardVitestConfig(fullSuiteVitestShards[1].projects);

View File

@@ -0,0 +1,4 @@
import { createProjectShardVitestConfig } from "./vitest.project-shard-config.ts";
import { fullSuiteVitestShards } from "./vitest.test-shards.mjs";
export default createProjectShardVitestConfig(fullSuiteVitestShards[0].projects);

View File

@@ -0,0 +1,4 @@
import { createProjectShardVitestConfig } from "./vitest.project-shard-config.ts";
import { fullSuiteVitestShards } from "./vitest.test-shards.mjs";
export default createProjectShardVitestConfig(fullSuiteVitestShards[4].projects);

View File

@@ -0,0 +1,13 @@
import { defineConfig } from "vitest/config";
import { sharedVitestConfig } from "./vitest.shared.config.ts";
export function createProjectShardVitestConfig(projects: readonly string[]) {
return defineConfig({
...sharedVitestConfig,
test: {
...sharedVitestConfig.test,
runner: "./test/non-isolated-runner.ts",
projects: [...projects],
},
});
}

91
vitest.test-shards.mjs Normal file
View File

@@ -0,0 +1,91 @@
export const autoReplyCoreTestInclude = ["src/auto-reply/*.test.ts"];
export const autoReplyCoreTestExclude = ["src/auto-reply/reply*.test.ts"];
export const autoReplyTopLevelReplyTestInclude = ["src/auto-reply/reply*.test.ts"];
export const autoReplyReplySubtreeTestInclude = ["src/auto-reply/reply/**/*.test.ts"];
export const fullSuiteVitestShards = [
{
config: "vitest.full-core-unit.config.ts",
name: "core-unit",
projects: [
"vitest.unit.config.ts",
"vitest.boundary.config.ts",
"vitest.contracts.config.ts",
"vitest.bundled.config.ts",
"vitest.tooling.config.ts",
],
},
{
config: "vitest.full-core-runtime.config.ts",
name: "core-runtime",
projects: [
"vitest.infra.config.ts",
"vitest.hooks.config.ts",
"vitest.acp.config.ts",
"vitest.runtime-config.config.ts",
"vitest.secrets.config.ts",
"vitest.logging.config.ts",
"vitest.process.config.ts",
"vitest.cron.config.ts",
"vitest.media.config.ts",
"vitest.media-understanding.config.ts",
"vitest.shared-core.config.ts",
"vitest.tasks.config.ts",
"vitest.tui.config.ts",
"vitest.ui.config.ts",
"vitest.utils.config.ts",
"vitest.wizard.config.ts",
],
},
{
config: "vitest.full-agentic.config.ts",
name: "agentic",
projects: [
"vitest.gateway.config.ts",
"vitest.cli.config.ts",
"vitest.commands-light.config.ts",
"vitest.commands.config.ts",
"vitest.agents.config.ts",
"vitest.daemon.config.ts",
"vitest.plugin-sdk-light.config.ts",
"vitest.plugin-sdk.config.ts",
"vitest.plugins.config.ts",
"vitest.channels.config.ts",
],
},
{
config: "vitest.full-auto-reply.config.ts",
name: "auto-reply",
projects: [
"vitest.auto-reply-core.config.ts",
"vitest.auto-reply-top-level.config.ts",
"vitest.auto-reply-reply.config.ts",
],
},
{
config: "vitest.full-extensions.config.ts",
name: "extensions",
projects: [
"vitest.extension-acpx.config.ts",
"vitest.extension-bluebubbles.config.ts",
"vitest.extension-channels.config.ts",
"vitest.extension-diffs.config.ts",
"vitest.extension-feishu.config.ts",
"vitest.extension-irc.config.ts",
"vitest.extension-mattermost.config.ts",
"vitest.extension-matrix.config.ts",
"vitest.extension-memory.config.ts",
"vitest.extension-messaging.config.ts",
"vitest.extension-msteams.config.ts",
"vitest.extension-providers.config.ts",
"vitest.extension-telegram.config.ts",
"vitest.extension-voice-call.config.ts",
"vitest.extension-whatsapp.config.ts",
"vitest.extension-zalo.config.ts",
"vitest.extensions.config.ts",
],
},
];