perf(test): expand light lane routing

This commit is contained in:
Peter Steinberger
2026-04-06 16:13:16 +01:00
parent c1c1c0f351
commit d7e3df5eaa
9 changed files with 292 additions and 61 deletions

View File

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

View File

@@ -15,6 +15,7 @@ title: "Tests"
- `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.
- 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.
- 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,7 +3,10 @@ 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 {
isCommandsLightTarget,
resolveCommandsLightIncludePattern,
} 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";
@@ -19,7 +22,10 @@ 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 {
isPluginSdkLightTarget,
resolvePluginSdkLightIncludePattern,
} from "../vitest.plugin-sdk-paths.mjs";
import { isBoundaryTestFile, isBundledPluginDependentUnitTestFile } from "../vitest.unit-paths.mjs";
import { resolveVitestCliEntry, resolveVitestNodeArgs } from "./run-vitest.mjs";
@@ -320,7 +326,7 @@ function classifyTarget(arg, cwd) {
return "logging";
}
if (relative.startsWith("src/plugin-sdk/")) {
return isPluginSdkLightTestFile(relative) ? "pluginSdkLight" : "pluginSdk";
return isPluginSdkLightTarget(relative) ? "pluginSdkLight" : "pluginSdk";
}
if (relative.startsWith("src/process/")) {
return "process";
@@ -344,7 +350,7 @@ function classifyTarget(arg, cwd) {
return "cli";
}
if (relative.startsWith("src/commands/")) {
return isCommandsLightTestFile(relative) ? "commandLight" : "command";
return isCommandsLightTarget(relative) ? "commandLight" : "command";
}
if (relative.startsWith("src/auto-reply/")) {
return "autoReply";
@@ -367,6 +373,19 @@ function classifyTarget(arg, cwd) {
return "default";
}
function resolveLightLaneIncludePatterns(kind, targetArg, cwd) {
const relative = toRepoRelativeTarget(targetArg, cwd);
if (kind === "pluginSdkLight") {
const includePattern = resolvePluginSdkLightIncludePattern(relative);
return includePattern ? [includePattern] : null;
}
if (kind === "commandLight") {
const includePattern = resolveCommandsLightIncludePattern(relative);
return includePattern ? [includePattern] : null;
}
return null;
}
function createVitestArgs(params) {
return [
"exec",
@@ -618,7 +637,10 @@ export function buildVitestRunPlans(
grouped.every((targetArg) => isFileLikeTarget(toRepoRelativeTarget(targetArg, cwd))));
const includePatterns = useCliTargetArgs
? null
: grouped.map((targetArg) => toScopedIncludePattern(targetArg, cwd));
: grouped.flatMap((targetArg) => {
const lightLanePatterns = resolveLightLaneIncludePatterns(kind, targetArg, cwd);
return lightLanePatterns ?? [toScopedIncludePattern(targetArg, cwd)];
});
const scopedTargetArgs = useCliTargetArgs ? grouped : [];
plans.push({
config,

View File

@@ -467,9 +467,15 @@ const CORE_SECRET_TARGET_REGISTRY: SecretTargetRegistryEntry[] = [
},
];
const SECRET_TARGET_REGISTRY: SecretTargetRegistryEntry[] = [
...CORE_SECRET_TARGET_REGISTRY,
...listChannelSecretTargetRegistryEntries(),
];
let cachedSecretTargetRegistry: SecretTargetRegistryEntry[] | null = null;
export { SECRET_TARGET_REGISTRY };
export function getSecretTargetRegistry(): SecretTargetRegistryEntry[] {
if (cachedSecretTargetRegistry) {
return cachedSecretTargetRegistry;
}
cachedSecretTargetRegistry = [
...CORE_SECRET_TARGET_REGISTRY,
...listChannelSecretTargetRegistryEntries(),
];
return cachedSecretTargetRegistry;
}

View File

@@ -1,6 +1,6 @@
import type { OpenClawConfig } from "../config/config.js";
import { getPath } from "./path-utils.js";
import { SECRET_TARGET_REGISTRY } from "./target-registry-data.js";
import { getSecretTargetRegistry } from "./target-registry-data.js";
import {
compileTargetRegistryEntry,
expandPathTokens,
@@ -14,15 +14,19 @@ import type {
SecretTargetRegistryEntry,
} from "./target-registry-types.js";
const COMPILED_SECRET_TARGET_REGISTRY = SECRET_TARGET_REGISTRY.map(compileTargetRegistryEntry);
const OPENCLAW_COMPILED_SECRET_TARGETS = COMPILED_SECRET_TARGET_REGISTRY.filter(
(entry) => entry.configFile === "openclaw.json",
);
const AUTH_PROFILES_COMPILED_SECRET_TARGETS = COMPILED_SECRET_TARGET_REGISTRY.filter(
(entry) => entry.configFile === "auth-profiles.json",
);
let compiledSecretTargetRegistryState: {
authProfilesCompiledSecretTargets: CompiledTargetRegistryEntry[];
authProfilesTargetsById: Map<string, CompiledTargetRegistryEntry[]>;
compiledSecretTargetRegistry: CompiledTargetRegistryEntry[];
knownTargetIds: Set<string>;
openClawCompiledSecretTargets: CompiledTargetRegistryEntry[];
openClawTargetsById: Map<string, CompiledTargetRegistryEntry[]>;
targetsByType: Map<string, CompiledTargetRegistryEntry[]>;
} | null = null;
function buildTargetTypeIndex(): Map<string, CompiledTargetRegistryEntry[]> {
function buildTargetTypeIndex(
compiledSecretTargetRegistry: CompiledTargetRegistryEntry[],
): Map<string, CompiledTargetRegistryEntry[]> {
const byType = new Map<string, CompiledTargetRegistryEntry[]>();
const append = (type: string, entry: CompiledTargetRegistryEntry) => {
const existing = byType.get(type);
@@ -32,7 +36,7 @@ function buildTargetTypeIndex(): Map<string, CompiledTargetRegistryEntry[]> {
}
byType.set(type, [entry]);
};
for (const entry of COMPILED_SECRET_TARGET_REGISTRY) {
for (const entry of compiledSecretTargetRegistry) {
append(entry.targetType, entry);
for (const alias of entry.targetTypeAliases ?? []) {
append(alias, entry);
@@ -41,12 +45,11 @@ function buildTargetTypeIndex(): Map<string, CompiledTargetRegistryEntry[]> {
return byType;
}
const TARGETS_BY_TYPE = buildTargetTypeIndex();
const KNOWN_TARGET_IDS = new Set(COMPILED_SECRET_TARGET_REGISTRY.map((entry) => entry.id));
function buildConfigTargetIdIndex(): Map<string, CompiledTargetRegistryEntry[]> {
function buildConfigTargetIdIndex(
entries: CompiledTargetRegistryEntry[],
): Map<string, CompiledTargetRegistryEntry[]> {
const byId = new Map<string, CompiledTargetRegistryEntry[]>();
for (const entry of OPENCLAW_COMPILED_SECRET_TARGETS) {
for (const entry of entries) {
const existing = byId.get(entry.id);
if (existing) {
existing.push(entry);
@@ -57,23 +60,29 @@ function buildConfigTargetIdIndex(): Map<string, CompiledTargetRegistryEntry[]>
return byId;
}
const OPENCLAW_TARGETS_BY_ID = buildConfigTargetIdIndex();
function buildAuthProfileTargetIdIndex(): Map<string, CompiledTargetRegistryEntry[]> {
const byId = new Map<string, CompiledTargetRegistryEntry[]>();
for (const entry of AUTH_PROFILES_COMPILED_SECRET_TARGETS) {
const existing = byId.get(entry.id);
if (existing) {
existing.push(entry);
continue;
}
byId.set(entry.id, [entry]);
function getCompiledSecretTargetRegistryState() {
if (compiledSecretTargetRegistryState) {
return compiledSecretTargetRegistryState;
}
return byId;
const compiledSecretTargetRegistry = getSecretTargetRegistry().map(compileTargetRegistryEntry);
const openClawCompiledSecretTargets = compiledSecretTargetRegistry.filter(
(entry) => entry.configFile === "openclaw.json",
);
const authProfilesCompiledSecretTargets = compiledSecretTargetRegistry.filter(
(entry) => entry.configFile === "auth-profiles.json",
);
compiledSecretTargetRegistryState = {
authProfilesCompiledSecretTargets,
authProfilesTargetsById: buildConfigTargetIdIndex(authProfilesCompiledSecretTargets),
compiledSecretTargetRegistry,
knownTargetIds: new Set(compiledSecretTargetRegistry.map((entry) => entry.id)),
openClawCompiledSecretTargets,
openClawTargetsById: buildConfigTargetIdIndex(openClawCompiledSecretTargets),
targetsByType: buildTargetTypeIndex(compiledSecretTargetRegistry),
};
return compiledSecretTargetRegistryState;
}
const AUTH_PROFILES_TARGETS_BY_ID = buildAuthProfileTargetIdIndex();
function normalizeAllowedTargetIds(targetIds?: Iterable<string>): Set<string> | null {
if (targetIds === undefined) {
return null;
@@ -170,7 +179,7 @@ function toResolvedPlanTarget(
}
export function listSecretTargetRegistryEntries(): SecretTargetRegistryEntry[] {
return COMPILED_SECRET_TARGET_REGISTRY.map((entry) => ({
return getCompiledSecretTargetRegistryState().compiledSecretTargetRegistry.map((entry) => ({
id: entry.id,
targetType: entry.targetType,
...(entry.targetTypeAliases ? { targetTypeAliases: [...entry.targetTypeAliases] } : {}),
@@ -194,11 +203,15 @@ export function listSecretTargetRegistryEntries(): SecretTargetRegistryEntry[] {
}
export function isKnownSecretTargetType(value: unknown): value is string {
return typeof value === "string" && TARGETS_BY_TYPE.has(value);
return (
typeof value === "string" && getCompiledSecretTargetRegistryState().targetsByType.has(value)
);
}
export function isKnownSecretTargetId(value: unknown): value is string {
return typeof value === "string" && KNOWN_TARGET_IDS.has(value);
return (
typeof value === "string" && getCompiledSecretTargetRegistryState().knownTargetIds.has(value)
);
}
export function resolvePlanTargetAgainstRegistry(candidate: {
@@ -207,7 +220,7 @@ export function resolvePlanTargetAgainstRegistry(candidate: {
providerId?: string;
accountId?: string;
}): ResolvedPlanTarget | null {
const entries = TARGETS_BY_TYPE.get(candidate.type);
const entries = getCompiledSecretTargetRegistryState().targetsByType.get(candidate.type);
if (!entries || entries.length === 0) {
return null;
}
@@ -240,7 +253,7 @@ export function resolvePlanTargetAgainstRegistry(candidate: {
}
export function resolveConfigSecretTargetByPath(pathSegments: string[]): ResolvedPlanTarget | null {
for (const entry of OPENCLAW_COMPILED_SECRET_TARGETS) {
for (const entry of getCompiledSecretTargetRegistryState().openClawCompiledSecretTargets) {
if (!entry.includeInPlan) {
continue;
}
@@ -268,10 +281,11 @@ export function discoverConfigSecretTargetsByIds(
targetIds?: Iterable<string>,
): DiscoveredConfigSecretTarget[] {
const allowedTargetIds = normalizeAllowedTargetIds(targetIds);
const registryState = getCompiledSecretTargetRegistryState();
const discoveryEntries = resolveDiscoveryEntries({
allowedTargetIds,
defaultEntries: OPENCLAW_COMPILED_SECRET_TARGETS,
entriesById: OPENCLAW_TARGETS_BY_ID,
defaultEntries: registryState.openClawCompiledSecretTargets,
entriesById: registryState.openClawTargetsById,
});
return discoverSecretTargetsFromEntries(config, discoveryEntries);
}
@@ -285,16 +299,17 @@ export function discoverAuthProfileSecretTargetsByIds(
targetIds?: Iterable<string>,
): DiscoveredConfigSecretTarget[] {
const allowedTargetIds = normalizeAllowedTargetIds(targetIds);
const registryState = getCompiledSecretTargetRegistryState();
const discoveryEntries = resolveDiscoveryEntries({
allowedTargetIds,
defaultEntries: AUTH_PROFILES_COMPILED_SECRET_TARGETS,
entriesById: AUTH_PROFILES_TARGETS_BY_ID,
defaultEntries: registryState.authProfilesCompiledSecretTargets,
entriesById: registryState.authProfilesTargetsById,
});
return discoverSecretTargetsFromEntries(store, discoveryEntries);
}
export function listAuthProfileSecretTargetEntries(): SecretTargetRegistryEntry[] {
return COMPILED_SECRET_TARGET_REGISTRY.filter(
return getCompiledSecretTargetRegistryState().compiledSecretTargetRegistry.filter(
(entry) => entry.configFile === "auth-profiles.json" && entry.includeInAudit,
);
}

View File

@@ -93,4 +93,64 @@ describe("scripts/test-projects changed-target routing", () => {
},
]);
});
it("routes changed plugin-sdk source allowlist files to sibling light tests", () => {
const plans = buildVitestRunPlans(["--changed", "origin/main"], process.cwd(), () => [
"src/plugin-sdk/lazy-value.ts",
]);
expect(plans).toEqual([
{
config: "vitest.plugin-sdk-light.config.ts",
forwardedArgs: [],
includePatterns: ["src/plugin-sdk/lazy-value.test.ts"],
watchMode: false,
},
]);
});
it("routes changed commands source allowlist files to sibling light tests", () => {
const plans = buildVitestRunPlans(["--changed", "origin/main"], process.cwd(), () => [
"src/commands/status-overview-values.ts",
]);
expect(plans).toEqual([
{
config: "vitest.commands-light.config.ts",
forwardedArgs: [],
includePatterns: ["src/commands/status-overview-values.test.ts"],
watchMode: false,
},
]);
});
it("keeps non-allowlisted plugin-sdk source files on the heavy lane", () => {
const plans = buildVitestRunPlans(["--changed", "origin/main"], process.cwd(), () => [
"src/plugin-sdk/facade-runtime.ts",
]);
expect(plans).toEqual([
{
config: "vitest.plugin-sdk.config.ts",
forwardedArgs: [],
includePatterns: ["src/plugin-sdk/**/*.test.ts"],
watchMode: false,
},
]);
});
it("keeps non-allowlisted commands source files on the heavy lane", () => {
const plans = buildVitestRunPlans(["--changed", "origin/main"], process.cwd(), () => [
"src/commands/channels.add.ts",
]);
expect(plans).toEqual([
{
config: "vitest.commands.config.ts",
forwardedArgs: [],
includePatterns: ["src/commands/**/*.test.ts"],
watchMode: false,
},
]);
});
});

View File

@@ -0,0 +1,43 @@
import { describe, expect, it } from "vitest";
import {
isCommandsLightTarget,
resolveCommandsLightIncludePattern,
} from "../vitest.commands-light-paths.mjs";
import {
isPluginSdkLightTarget,
resolvePluginSdkLightIncludePattern,
} from "../vitest.plugin-sdk-paths.mjs";
describe("light vitest path routing", () => {
it("maps plugin-sdk allowlist source and test files to sibling light tests", () => {
expect(isPluginSdkLightTarget("src/plugin-sdk/lazy-value.ts")).toBe(true);
expect(isPluginSdkLightTarget("src/plugin-sdk/lazy-value.test.ts")).toBe(true);
expect(resolvePluginSdkLightIncludePattern("src/plugin-sdk/lazy-value.ts")).toBe(
"src/plugin-sdk/lazy-value.test.ts",
);
expect(resolvePluginSdkLightIncludePattern("src/plugin-sdk/lazy-value.test.ts")).toBe(
"src/plugin-sdk/lazy-value.test.ts",
);
});
it("keeps non-allowlisted plugin-sdk files off the light lane", () => {
expect(isPluginSdkLightTarget("src/plugin-sdk/facade-runtime.ts")).toBe(false);
expect(resolvePluginSdkLightIncludePattern("src/plugin-sdk/facade-runtime.ts")).toBeNull();
});
it("maps commands allowlist source and test files to sibling light tests", () => {
expect(isCommandsLightTarget("src/commands/text-format.ts")).toBe(true);
expect(isCommandsLightTarget("src/commands/text-format.test.ts")).toBe(true);
expect(resolveCommandsLightIncludePattern("src/commands/text-format.ts")).toBe(
"src/commands/text-format.test.ts",
);
expect(resolveCommandsLightIncludePattern("src/commands/text-format.test.ts")).toBe(
"src/commands/text-format.test.ts",
);
});
it("keeps non-allowlisted commands files off the light lane", () => {
expect(isCommandsLightTarget("src/commands/channels.add.ts")).toBe(false);
expect(resolveCommandsLightIncludePattern("src/commands/channels.add.ts")).toBeNull();
});
});

View File

@@ -1,12 +1,53 @@
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",
const commandsLightEntries = [
{ source: "src/commands/cleanup-utils.ts", test: "src/commands/cleanup-utils.test.ts" },
{
source: "src/commands/dashboard.links.ts",
test: "src/commands/dashboard.links.test.ts",
},
{ source: "src/commands/doctor-browser.ts", test: "src/commands/doctor-browser.test.ts" },
{
source: "src/commands/doctor-gateway-auth-token.ts",
test: "src/commands/doctor-gateway-auth-token.test.ts",
},
{
source: "src/commands/sandbox-formatters.ts",
test: "src/commands/sandbox-formatters.test.ts",
},
{
source: "src/commands/status-overview-rows.ts",
test: "src/commands/status-overview-rows.test.ts",
},
{
source: "src/commands/status-overview-surface.ts",
test: "src/commands/status-overview-surface.test.ts",
},
{
source: "src/commands/status-overview-values.ts",
test: "src/commands/status-overview-values.test.ts",
},
{ source: "src/commands/text-format.ts", test: "src/commands/text-format.test.ts" },
];
const commandsLightIncludePatternByFile = new Map(
commandsLightEntries.flatMap(({ source, test }) => [
[source, test],
[test, test],
]),
);
export const commandsLightSourceFiles = commandsLightEntries.map(({ source }) => source);
export const commandsLightTestFiles = commandsLightEntries.map(({ test }) => test);
export function isCommandsLightTarget(file) {
return commandsLightIncludePatternByFile.has(normalizeRepoPath(file));
}
export function isCommandsLightTestFile(file) {
return commandsLightTestFiles.includes(normalizeRepoPath(file));
}
export function resolveCommandsLightIncludePattern(file) {
return commandsLightIncludePatternByFile.get(normalizeRepoPath(file)) ?? null;
}

View File

@@ -1,14 +1,56 @@
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",
const pluginSdkLightEntries = [
{ source: "src/plugin-sdk/acp-runtime.ts", test: "src/plugin-sdk/acp-runtime.test.ts" },
{ source: "src/plugin-sdk/allow-from.ts", test: "src/plugin-sdk/allow-from.test.ts" },
{
source: "src/plugin-sdk/keyed-async-queue.ts",
test: "src/plugin-sdk/keyed-async-queue.test.ts",
},
{ source: "src/plugin-sdk/lazy-value.ts", test: "src/plugin-sdk/lazy-value.test.ts" },
{
source: "src/plugin-sdk/persistent-dedupe.ts",
test: "src/plugin-sdk/persistent-dedupe.test.ts",
},
{ source: "src/plugin-sdk/provider-entry.ts", test: "src/plugin-sdk/provider-entry.test.ts" },
{
source: "src/plugin-sdk/provider-model-shared.ts",
test: "src/plugin-sdk/provider-model-shared.test.ts",
},
{ source: "src/plugin-sdk/provider-tools.ts", test: "src/plugin-sdk/provider-tools.test.ts" },
{
source: "src/plugin-sdk/status-helpers.ts",
test: "src/plugin-sdk/status-helpers.test.ts",
},
{ source: "src/plugin-sdk/temp-path.ts", test: "src/plugin-sdk/temp-path.test.ts" },
{
source: "src/plugin-sdk/text-chunking.ts",
test: "src/plugin-sdk/text-chunking.test.ts",
},
{
source: "src/plugin-sdk/webhook-targets.ts",
test: "src/plugin-sdk/webhook-targets.test.ts",
},
];
const pluginSdkLightIncludePatternByFile = new Map(
pluginSdkLightEntries.flatMap(({ source, test }) => [
[source, test],
[test, test],
]),
);
export const pluginSdkLightSourceFiles = pluginSdkLightEntries.map(({ source }) => source);
export const pluginSdkLightTestFiles = pluginSdkLightEntries.map(({ test }) => test);
export function isPluginSdkLightTarget(file) {
return pluginSdkLightIncludePatternByFile.has(normalizeRepoPath(file));
}
export function isPluginSdkLightTestFile(file) {
return pluginSdkLightTestFiles.includes(normalizeRepoPath(file));
}
export function resolvePluginSdkLightIncludePattern(file) {
return pluginSdkLightIncludePatternByFile.get(normalizeRepoPath(file)) ?? null;
}