perf(test): split light vitest lanes and restore hooks

This commit is contained in:
Peter Steinberger
2026-04-06 15:50:02 +01:00
parent 5765c4cb2a
commit a010ce462f
14 changed files with 242 additions and 74 deletions

View File

@@ -60,6 +60,7 @@ Think of the suites as “increasing realism” (and increasing flakiness/cost):
- Untargeted `pnpm test` still uses the native Vitest root `projects` config.
- `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.
- Embedded runner note:
- When you change message-tool discovery inputs or compaction runtime context,
keep both levels of coverage.

View File

@@ -14,6 +14,7 @@ title: "Tests"
- `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.
- 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.
- 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`.

55
extensions/acpx/src/acpx-runtime.d.ts vendored Normal file
View File

@@ -0,0 +1,55 @@
declare module "acpx/runtime" {
export const ACPX_BACKEND_ID: string;
export type AcpRuntimeDoctorReport =
import("../../../src/acp/runtime/types.js").AcpRuntimeDoctorReport;
export type AcpRuntimeEnsureInput =
import("../../../src/acp/runtime/types.js").AcpRuntimeEnsureInput;
export type AcpRuntimeEvent = import("../../../src/acp/runtime/types.js").AcpRuntimeEvent;
export type AcpRuntimeHandle = import("../../../src/acp/runtime/types.js").AcpRuntimeHandle;
export type AcpRuntimeTurnInput = import("../../../src/acp/runtime/types.js").AcpRuntimeTurnInput;
export type AcpRuntimeStatus = import("../../../src/acp/runtime/types.js").AcpRuntimeStatus;
export type AcpRuntimeCapabilities =
import("../../../src/acp/runtime/types.js").AcpRuntimeCapabilities;
export type AcpSessionStore = {
load(sessionId: string): Promise<unknown>;
save(record: unknown): Promise<void>;
};
export type AcpAgentRegistry = {
resolve(agentId: string): string;
list(): string[];
};
export type AcpRuntimeOptions = {
cwd: string;
sessionStore: AcpSessionStore;
agentRegistry: AcpAgentRegistry;
permissionMode: string;
mcpServers?: unknown[];
nonInteractivePermissions?: unknown;
timeoutMs?: number;
};
export class AcpxRuntime {
constructor(options: AcpRuntimeOptions, testOptions?: unknown);
isHealthy(): boolean;
probeAvailability(): Promise<void>;
doctor(): Promise<AcpRuntimeDoctorReport>;
ensureSession(input: AcpRuntimeEnsureInput): Promise<AcpRuntimeHandle>;
runTurn(input: AcpRuntimeTurnInput): AsyncIterable<AcpRuntimeEvent>;
getCapabilities(input?: { handle?: AcpRuntimeHandle }): AcpRuntimeCapabilities;
getStatus(input: { handle: AcpRuntimeHandle; signal?: AbortSignal }): Promise<AcpRuntimeStatus>;
setMode(input: { handle: AcpRuntimeHandle; mode: string }): Promise<void>;
setConfigOption(input: { handle: AcpRuntimeHandle; key: string; value: string }): Promise<void>;
cancel(input: { handle: AcpRuntimeHandle; reason?: string }): Promise<void>;
close(input: { handle: AcpRuntimeHandle; reason: string }): Promise<void>;
}
export function createAcpRuntime(...args: unknown[]): unknown;
export function createAgentRegistry(...args: unknown[]): AcpAgentRegistry;
export function createFileSessionStore(...args: unknown[]): AcpSessionStore;
export function decodeAcpxRuntimeHandleState(...args: unknown[]): unknown;
export function encodeAcpxRuntimeHandleState(...args: unknown[]): unknown;
}

View File

@@ -3,6 +3,7 @@ import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { isChannelSurfaceTestFile } from "../vitest.channel-paths.mjs";
import { isCommandsLightTestFile } from "../vitest.commands-light-paths.mjs";
import { isAcpxExtensionRoot } from "../vitest.extension-acpx-paths.mjs";
import { isBlueBubblesExtensionRoot } from "../vitest.extension-bluebubbles-paths.mjs";
import { isDiffsExtensionRoot } from "../vitest.extension-diffs-paths.mjs";
@@ -18,6 +19,7 @@ import { isTelegramExtensionRoot } from "../vitest.extension-telegram-paths.mjs"
import { isVoiceCallExtensionRoot } from "../vitest.extension-voice-call-paths.mjs";
import { isWhatsAppExtensionRoot } from "../vitest.extension-whatsapp-paths.mjs";
import { isZaloExtensionRoot } from "../vitest.extension-zalo-paths.mjs";
import { isPluginSdkLightTestFile } from "../vitest.plugin-sdk-paths.mjs";
import { isBoundaryTestFile, isBundledPluginDependentUnitTestFile } from "../vitest.unit-paths.mjs";
import { resolveVitestCliEntry, resolveVitestNodeArgs } from "./run-vitest.mjs";
@@ -29,6 +31,7 @@ const BOUNDARY_VITEST_CONFIG = "vitest.boundary.config.ts";
const BUNDLED_VITEST_CONFIG = "vitest.bundled.config.ts";
const CHANNEL_VITEST_CONFIG = "vitest.channels.config.ts";
const CLI_VITEST_CONFIG = "vitest.cli.config.ts";
const COMMANDS_LIGHT_VITEST_CONFIG = "vitest.commands-light.config.ts";
const COMMANDS_VITEST_CONFIG = "vitest.commands.config.ts";
const CONTRACTS_VITEST_CONFIG = "vitest.contracts.config.ts";
const CRON_VITEST_CONFIG = "vitest.cron.config.ts";
@@ -57,6 +60,7 @@ const INFRA_VITEST_CONFIG = "vitest.infra.config.ts";
const MEDIA_VITEST_CONFIG = "vitest.media.config.ts";
const MEDIA_UNDERSTANDING_VITEST_CONFIG = "vitest.media-understanding.config.ts";
const LOGGING_VITEST_CONFIG = "vitest.logging.config.ts";
const PLUGIN_SDK_LIGHT_VITEST_CONFIG = "vitest.plugin-sdk-light.config.ts";
const PLUGIN_SDK_VITEST_CONFIG = "vitest.plugin-sdk.config.ts";
const PLUGINS_VITEST_CONFIG = "vitest.plugins.config.ts";
const PROCESS_VITEST_CONFIG = "vitest.process.config.ts";
@@ -316,7 +320,7 @@ function classifyTarget(arg, cwd) {
return "logging";
}
if (relative.startsWith("src/plugin-sdk/")) {
return "pluginSdk";
return isPluginSdkLightTestFile(relative) ? "pluginSdkLight" : "pluginSdk";
}
if (relative.startsWith("src/process/")) {
return "process";
@@ -340,7 +344,7 @@ function classifyTarget(arg, cwd) {
return "cli";
}
if (relative.startsWith("src/commands/")) {
return "command";
return isCommandsLightTestFile(relative) ? "commandLight" : "command";
}
if (relative.startsWith("src/auto-reply/")) {
return "autoReply";
@@ -448,6 +452,7 @@ export function buildVitestRunPlans(
"daemon",
"media",
"logging",
"pluginSdkLight",
"pluginSdk",
"process",
"secrets",
@@ -457,6 +462,7 @@ export function buildVitestRunPlans(
"mediaUnderstanding",
"acp",
"cli",
"commandLight",
"command",
"autoReply",
"agent",
@@ -515,91 +521,97 @@ export function buildVitestRunPlans(
? MEDIA_VITEST_CONFIG
: kind === "logging"
? LOGGING_VITEST_CONFIG
: kind === "pluginSdk"
? PLUGIN_SDK_VITEST_CONFIG
: kind === "process"
? PROCESS_VITEST_CONFIG
: kind === "secrets"
? SECRETS_VITEST_CONFIG
: kind === "sharedCore"
? SHARED_CORE_VITEST_CONFIG
: kind === "tasks"
? TASKS_VITEST_CONFIG
: kind === "tui"
? TUI_VITEST_CONFIG
: kind === "mediaUnderstanding"
? MEDIA_UNDERSTANDING_VITEST_CONFIG
: kind === "acp"
? ACP_VITEST_CONFIG
: kind === "cli"
? CLI_VITEST_CONFIG
: kind === "command"
? COMMANDS_VITEST_CONFIG
: kind === "autoReply"
? AUTO_REPLY_VITEST_CONFIG
: kind === "agent"
? AGENTS_VITEST_CONFIG
: kind === "plugin"
? PLUGINS_VITEST_CONFIG
: kind === "ui"
? UI_VITEST_CONFIG
: kind === "utils"
? UTILS_VITEST_CONFIG
: kind === "wizard"
? WIZARD_VITEST_CONFIG
: kind === "e2e"
? E2E_VITEST_CONFIG
: kind === "extensionAcpx"
? EXTENSION_ACPX_VITEST_CONFIG
: kind === "extensionDiffs"
? EXTENSION_DIFFS_VITEST_CONFIG
: kind ===
"extensionBlueBubbles"
? EXTENSION_BLUEBUBBLES_VITEST_CONFIG
: kind === "extensionFeishu"
? EXTENSION_FEISHU_VITEST_CONFIG
: kind === "extensionIrc"
? EXTENSION_IRC_VITEST_CONFIG
: kind === "pluginSdkLight"
? PLUGIN_SDK_LIGHT_VITEST_CONFIG
: kind === "pluginSdk"
? PLUGIN_SDK_VITEST_CONFIG
: kind === "process"
? PROCESS_VITEST_CONFIG
: kind === "secrets"
? SECRETS_VITEST_CONFIG
: kind === "sharedCore"
? SHARED_CORE_VITEST_CONFIG
: kind === "tasks"
? TASKS_VITEST_CONFIG
: kind === "tui"
? TUI_VITEST_CONFIG
: kind === "mediaUnderstanding"
? MEDIA_UNDERSTANDING_VITEST_CONFIG
: kind === "acp"
? ACP_VITEST_CONFIG
: kind === "cli"
? CLI_VITEST_CONFIG
: kind === "commandLight"
? COMMANDS_LIGHT_VITEST_CONFIG
: kind === "command"
? COMMANDS_VITEST_CONFIG
: kind === "autoReply"
? AUTO_REPLY_VITEST_CONFIG
: kind === "agent"
? AGENTS_VITEST_CONFIG
: kind === "plugin"
? PLUGINS_VITEST_CONFIG
: kind === "ui"
? UI_VITEST_CONFIG
: kind === "utils"
? UTILS_VITEST_CONFIG
: kind === "wizard"
? WIZARD_VITEST_CONFIG
: kind === "e2e"
? E2E_VITEST_CONFIG
: kind === "extensionAcpx"
? EXTENSION_ACPX_VITEST_CONFIG
: kind === "extensionDiffs"
? EXTENSION_DIFFS_VITEST_CONFIG
: kind ===
"extensionBlueBubbles"
? EXTENSION_BLUEBUBBLES_VITEST_CONFIG
: kind ===
"extensionMattermost"
? EXTENSION_MATTERMOST_VITEST_CONFIG
"extensionFeishu"
? EXTENSION_FEISHU_VITEST_CONFIG
: kind ===
"extensionChannel"
? EXTENSION_CHANNELS_VITEST_CONFIG
"extensionIrc"
? EXTENSION_IRC_VITEST_CONFIG
: kind ===
"extensionTelegram"
? EXTENSION_TELEGRAM_VITEST_CONFIG
"extensionMattermost"
? EXTENSION_MATTERMOST_VITEST_CONFIG
: kind ===
"extensionVoiceCall"
? EXTENSION_VOICE_CALL_VITEST_CONFIG
"extensionChannel"
? EXTENSION_CHANNELS_VITEST_CONFIG
: kind ===
"extensionWhatsApp"
? EXTENSION_WHATSAPP_VITEST_CONFIG
"extensionTelegram"
? EXTENSION_TELEGRAM_VITEST_CONFIG
: kind ===
"extensionZalo"
? EXTENSION_ZALO_VITEST_CONFIG
"extensionVoiceCall"
? EXTENSION_VOICE_CALL_VITEST_CONFIG
: kind ===
"extensionMatrix"
? EXTENSION_MATRIX_VITEST_CONFIG
"extensionWhatsApp"
? EXTENSION_WHATSAPP_VITEST_CONFIG
: kind ===
"extensionMemory"
? EXTENSION_MEMORY_VITEST_CONFIG
"extensionZalo"
? EXTENSION_ZALO_VITEST_CONFIG
: kind ===
"extensionMsTeams"
? EXTENSION_MSTEAMS_VITEST_CONFIG
"extensionMatrix"
? EXTENSION_MATRIX_VITEST_CONFIG
: kind ===
"extensionMessaging"
? EXTENSION_MESSAGING_VITEST_CONFIG
"extensionMemory"
? EXTENSION_MEMORY_VITEST_CONFIG
: kind ===
"extensionProvider"
? EXTENSION_PROVIDERS_VITEST_CONFIG
"extensionMsTeams"
? EXTENSION_MSTEAMS_VITEST_CONFIG
: kind ===
"channel"
? CHANNEL_VITEST_CONFIG
"extensionMessaging"
? EXTENSION_MESSAGING_VITEST_CONFIG
: kind ===
"extension"
? EXTENSIONS_VITEST_CONFIG
: DEFAULT_VITEST_CONFIG;
"extensionProvider"
? EXTENSION_PROVIDERS_VITEST_CONFIG
: kind ===
"channel"
? CHANNEL_VITEST_CONFIG
: kind ===
"extension"
? EXTENSIONS_VITEST_CONFIG
: DEFAULT_VITEST_CONFIG;
const useCliTargetArgs =
kind === "e2e" ||
(kind === "default" &&

View File

@@ -67,4 +67,30 @@ describe("scripts/test-projects changed-target routing", () => {
},
]);
});
it("routes explicit plugin-sdk light tests to the lighter plugin-sdk lane", () => {
const plans = buildVitestRunPlans(["src/plugin-sdk/temp-path.test.ts"], process.cwd());
expect(plans).toEqual([
{
config: "vitest.plugin-sdk-light.config.ts",
forwardedArgs: [],
includePatterns: ["src/plugin-sdk/temp-path.test.ts"],
watchMode: false,
},
]);
});
it("routes explicit commands light tests to the lighter commands lane", () => {
const plans = buildVitestRunPlans(["src/commands/cleanup-utils.test.ts"], process.cwd());
expect(plans).toEqual([
{
config: "vitest.commands-light.config.ts",
forwardedArgs: [],
includePatterns: ["src/commands/cleanup-utils.test.ts"],
watchMode: false,
},
]);
});
});

View File

@@ -1,10 +1,12 @@
import { describe, expect, it } from "vitest";
import { createAgentsVitestConfig } from "../vitest.agents.config.ts";
import bundledConfig from "../vitest.bundled.config.ts";
import { createCommandsLightVitestConfig } from "../vitest.commands-light.config.ts";
import { createCommandsVitestConfig } from "../vitest.commands.config.ts";
import baseConfig, { rootVitestProjects } from "../vitest.config.ts";
import { createContractsVitestConfig } from "../vitest.contracts.config.ts";
import { createGatewayVitestConfig } from "../vitest.gateway.config.ts";
import { createPluginSdkLightVitestConfig } from "../vitest.plugin-sdk-light.config.ts";
import { createUiVitestConfig } from "../vitest.ui.config.ts";
import { createUnitVitestConfig } from "../vitest.unit.config.ts";
@@ -16,7 +18,9 @@ describe("projects vitest config", () => {
it("keeps root projects on the shared thread-first pool by default", () => {
expect(createGatewayVitestConfig().test.pool).toBe("threads");
expect(createAgentsVitestConfig().test.pool).toBe("threads");
expect(createCommandsLightVitestConfig().test.pool).toBe("threads");
expect(createCommandsVitestConfig().test.pool).toBe("threads");
expect(createPluginSdkLightVitestConfig().test.pool).toBe("threads");
expect(createContractsVitestConfig().test.pool).toBe("threads");
});

View File

@@ -7,6 +7,7 @@ import { createAgentsVitestConfig } from "../vitest.agents.config.ts";
import { createAutoReplyVitestConfig } from "../vitest.auto-reply.config.ts";
import { createChannelsVitestConfig } from "../vitest.channels.config.ts";
import { createCliVitestConfig } from "../vitest.cli.config.ts";
import { createCommandsLightVitestConfig } from "../vitest.commands-light.config.ts";
import { createCommandsVitestConfig } from "../vitest.commands.config.ts";
import { createCronVitestConfig } from "../vitest.cron.config.ts";
import { createDaemonVitestConfig } from "../vitest.daemon.config.ts";
@@ -33,6 +34,7 @@ import { createInfraVitestConfig } from "../vitest.infra.config.ts";
import { createLoggingVitestConfig } from "../vitest.logging.config.ts";
import { createMediaUnderstandingVitestConfig } from "../vitest.media-understanding.config.ts";
import { createMediaVitestConfig } from "../vitest.media.config.ts";
import { createPluginSdkLightVitestConfig } from "../vitest.plugin-sdk-light.config.ts";
import { createPluginSdkVitestConfig } from "../vitest.plugin-sdk.config.ts";
import { createPluginsVitestConfig } from "../vitest.plugins.config.ts";
import { createProcessVitestConfig } from "../vitest.process.config.ts";
@@ -160,6 +162,7 @@ describe("scoped vitest configs", () => {
const defaultHooksConfig = createHooksVitestConfig({});
const defaultInfraConfig = createInfraVitestConfig({});
const defaultLoggingConfig = createLoggingVitestConfig({});
const defaultPluginSdkLightConfig = createPluginSdkLightVitestConfig({});
const defaultPluginSdkConfig = createPluginSdkVitestConfig({});
const defaultSecretsConfig = createSecretsVitestConfig({});
const defaultRuntimeConfig = createRuntimeConfigVitestConfig({});
@@ -169,6 +172,7 @@ describe("scoped vitest configs", () => {
const defaultMediaUnderstandingConfig = createMediaUnderstandingVitestConfig({});
const defaultSharedCoreConfig = createSharedCoreVitestConfig({});
const defaultTasksConfig = createTasksVitestConfig({});
const defaultCommandsLightConfig = createCommandsLightVitestConfig({});
const defaultCommandsConfig = createCommandsVitestConfig({});
const defaultAutoReplyConfig = createAutoReplyVitestConfig({});
const defaultAgentsConfig = createAgentsVitestConfig({});
@@ -212,6 +216,11 @@ describe("scoped vitest configs", () => {
]);
});
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"]);
});
it("defaults channel tests to threads with the non-isolated runner", () => {
expect(defaultChannelsConfig.test?.isolate).toBe(false);
expect(defaultChannelsConfig.test?.pool).toBe("threads");

View File

@@ -0,0 +1,12 @@
const normalizeRepoPath = (value) => value.replaceAll("\\", "/");
export const commandsLightTestFiles = [
"src/commands/cleanup-utils.test.ts",
"src/commands/dashboard.links.test.ts",
"src/commands/doctor-browser.test.ts",
"src/commands/doctor-gateway-auth-token.test.ts",
];
export function isCommandsLightTestFile(file) {
return commandsLightTestFiles.includes(normalizeRepoPath(file));
}

View File

@@ -0,0 +1,14 @@
import { commandsLightTestFiles } from "./vitest.commands-light-paths.mjs";
import { createScopedVitestConfig } from "./vitest.scoped-config.ts";
export function createCommandsLightVitestConfig(env?: Record<string, string | undefined>) {
return createScopedVitestConfig(commandsLightTestFiles, {
dir: "src/commands",
env,
includeOpenClawRuntimeSetup: false,
name: "commands-light",
passWithNoTests: true,
});
}
export default createCommandsLightVitestConfig();

View File

@@ -1,9 +1,11 @@
import { commandsLightTestFiles } from "./vitest.commands-light-paths.mjs";
import { createScopedVitestConfig } from "./vitest.scoped-config.ts";
export function createCommandsVitestConfig(env?: Record<string, string | undefined>) {
return createScopedVitestConfig(["src/commands/**/*.test.ts"], {
dir: "src/commands",
env,
exclude: commandsLightTestFiles,
name: "commands",
});
}

View File

@@ -20,11 +20,13 @@ export const rootVitestProjects = [
"vitest.runtime-config.config.ts",
"vitest.secrets.config.ts",
"vitest.cli.config.ts",
"vitest.commands-light.config.ts",
"vitest.commands.config.ts",
"vitest.auto-reply.config.ts",
"vitest.agents.config.ts",
"vitest.daemon.config.ts",
"vitest.media.config.ts",
"vitest.plugin-sdk-light.config.ts",
"vitest.plugin-sdk.config.ts",
"vitest.plugins.config.ts",
"vitest.logging.config.ts",

View File

@@ -0,0 +1,14 @@
import { pluginSdkLightTestFiles } from "./vitest.plugin-sdk-paths.mjs";
import { createScopedVitestConfig } from "./vitest.scoped-config.ts";
export function createPluginSdkLightVitestConfig(env?: Record<string, string | undefined>) {
return createScopedVitestConfig(pluginSdkLightTestFiles, {
dir: "src",
env,
includeOpenClawRuntimeSetup: false,
name: "plugin-sdk-light",
passWithNoTests: true,
});
}
export default createPluginSdkLightVitestConfig();

View File

@@ -0,0 +1,14 @@
const normalizeRepoPath = (value) => value.replaceAll("\\", "/");
export const pluginSdkLightTestFiles = [
"src/plugin-sdk/acp-runtime.test.ts",
"src/plugin-sdk/provider-entry.test.ts",
"src/plugin-sdk/runtime.test.ts",
"src/plugin-sdk/temp-path.test.ts",
"src/plugin-sdk/text-chunking.test.ts",
"src/plugin-sdk/webhook-targets.test.ts",
];
export function isPluginSdkLightTestFile(file) {
return pluginSdkLightTestFiles.includes(normalizeRepoPath(file));
}

View File

@@ -1,9 +1,11 @@
import { pluginSdkLightTestFiles } from "./vitest.plugin-sdk-paths.mjs";
import { createScopedVitestConfig } from "./vitest.scoped-config.ts";
export function createPluginSdkVitestConfig(env?: Record<string, string | undefined>) {
return createScopedVitestConfig(["src/plugin-sdk/**/*.test.ts"], {
dir: "src",
env,
exclude: pluginSdkLightTestFiles,
name: "plugin-sdk",
passWithNoTests: true,
});