mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-19 13:11:40 +00:00
perf(test): shard full vitest runs
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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"]);
|
||||
|
||||
13
vitest.auto-reply-core.config.ts
Normal file
13
vitest.auto-reply-core.config.ts
Normal 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();
|
||||
12
vitest.auto-reply-reply.config.ts
Normal file
12
vitest.auto-reply-reply.config.ts
Normal 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();
|
||||
12
vitest.auto-reply-top-level.config.ts
Normal file
12
vitest.auto-reply-top-level.config.ts
Normal 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();
|
||||
4
vitest.full-agentic.config.ts
Normal file
4
vitest.full-agentic.config.ts
Normal 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);
|
||||
4
vitest.full-auto-reply.config.ts
Normal file
4
vitest.full-auto-reply.config.ts
Normal 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);
|
||||
4
vitest.full-core-runtime.config.ts
Normal file
4
vitest.full-core-runtime.config.ts
Normal 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);
|
||||
4
vitest.full-core-unit.config.ts
Normal file
4
vitest.full-core-unit.config.ts
Normal 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);
|
||||
4
vitest.full-extensions.config.ts
Normal file
4
vitest.full-extensions.config.ts
Normal 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);
|
||||
13
vitest.project-shard-config.ts
Normal file
13
vitest.project-shard-config.ts
Normal 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
91
vitest.test-shards.mjs
Normal 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",
|
||||
],
|
||||
},
|
||||
];
|
||||
Reference in New Issue
Block a user