openclaw/scripts/test-projects.test-support.mts
Peter Steinberger b621fabe58
fix(test): include omitted agent tests in directory and glob runs (#161428)
* fix(test): include omitted agent tests in directory and glob runs

Expand agent directory and ordinary glob selections into existing regular
files before choosing their runners. Preserve fast, isolated, harness and
database-worker ownership, exclusions, include limits and CLI controls.

Replace plan-shape assertions with actual configuration inventory proof.
The regression fails on the original selector with 163 of 191 tools files
selected. Keep complete coverage while avoiding quadratic test matching.

Validation: Linux Testbox owner and sibling suites, real directory/glob
commands, build, changed checks, import boundaries and both cycle scans;
independent review through P2 passed.

* test: batch helper-import coverage parsing
2026-09-29 17:00:56 -07:00

5789 lines
208 KiB
TypeScript

// Test-project planning helpers used by scripts/run-vitest.mts,
// scripts/test-projects.mts, and focused tests. Exports are intentionally
// granular so project selection stays testable without spawning Vitest.
import { spawnSync, type SpawnSyncOptionsWithStringEncoding } from "node:child_process";
import { randomUUID } from "node:crypto";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import {
agentVitestProjectOwners,
embeddedAgentVitestProjectOwners,
isAgentsCoreIsolatedTestFile,
isAgentsSpawnProductionBoundaryTestFile,
} from "../test/vitest/vitest.agents-paths.mjs";
import { isChannelSurfaceTestFile } from "../test/vitest/vitest.channel-paths.mjs";
import {
cliProcessTestFiles,
isCliProcessTestFile,
} from "../test/vitest/vitest.cli-process-paths.mjs";
import {
commandsLightTestFiles,
isCommandsLightTarget,
resolveCommandsLightIncludePattern,
} from "../test/vitest/vitest.commands-light-paths.mjs";
import {
channelConfigContractPatterns,
channelRegistryContractPatterns,
channelSessionContractPatterns,
channelSurfaceContractPatterns,
} from "../test/vitest/vitest.contracts-paths.mjs";
import {
DATABASE_WORKER_WATCH_OWNER_ENV_KEY,
DATABASE_WORKER_WATCH_TESTS_ENV_KEY,
databaseWorkerCoreFormerFastKinds,
databaseWorkerCoreTestFiles,
isDatabaseWorkerCoreTestFile,
} from "../test/vitest/vitest.database-worker-core-paths.mjs";
import { codexExtensionTestRoots } from "../test/vitest/vitest.extension-codex-paths.mjs";
import {
databaseWorkerExtensionTestFiles,
databaseWorkerExtensionTestRoots,
} from "../test/vitest/vitest.extension-database-workers-paths.mjs";
import { matrixExtensionTestRoots } from "../test/vitest/vitest.extension-matrix-paths.mjs";
import { telegramExtensionTestRoots } from "../test/vitest/vitest.extension-telegram-paths.mjs";
import {
gatewayDatabaseWorkerTestFiles,
gatewayPluginTestFiles,
isGatewayServerTestFile,
} from "../test/vitest/vitest.gateway-server-paths.mjs";
import { intersectIncludePatterns } from "../test/vitest/vitest.include-patterns.ts";
import { packageContractTestFiles } from "../test/vitest/vitest.package-contract-paths.mjs";
import { isSharedVitestExcludedPath } from "../test/vitest/vitest.pattern-file.ts";
import {
isPluginSdkLightTarget,
pluginSdkLightTestFiles,
resolvePluginSdkLightIncludePattern,
} from "../test/vitest/vitest.plugin-sdk-paths.mjs";
import { fullSuiteVitestShards, tuiPtyTestFiles } from "../test/vitest/vitest.test-shards.mjs";
import {
isToolingIsolatedTestFile,
toolingIsolatedTestFiles,
} from "../test/vitest/vitest.tooling-isolated-paths.mjs";
import {
isUiIsolatedTestFile,
uiIsolatedTestFiles,
} from "../test/vitest/vitest.ui-isolated-paths.mjs";
import {
isControlUiSourcePath,
isPluginControlUiPath,
isUiBrowserTestFile,
uiE2eRealGatewayTestFiles,
uiTimingTestFiles,
} from "../test/vitest/vitest.ui-paths.mjs";
import {
getUnitFastIsolatedTestFiles,
getUnitFastTestFiles,
getUnitFastTimerTestFiles,
resolveUnitFastIsolatedTestIncludePattern,
resolveUnitFastTestIncludePattern,
resolveUnitFastTimerTestIncludePattern,
} from "../test/vitest/vitest.unit-fast-paths.mjs";
import {
isBoundaryTestFile,
isBundledPluginDependentUnitTestFile,
isUnitConfigTestFile,
filterUnitConfigTestFiles,
} from "../test/vitest/vitest.unit-paths.mjs";
import {
detectChangedLanes,
listChangedPathsFromGit as listChangedPathsFromGitSource,
} from "./changed-lanes.mts";
import { parsePermissiveBooleanToken } from "./lib/arg-utils.mts";
import {
getChangedPathFacts,
isTestFileTarget,
isTestSupportFileTarget,
} from "./lib/changed-path-facts.mjs";
import {
createExtensionTestProcessTargetChunks,
listTrackedTestPlanFiles,
resolveExtensionTestConfig,
splitExtensionTestProcessTargets,
} from "./lib/extension-test-plan.mts";
import {
createGatewayServerTestTargetChunks,
splitTestTargetChunks as splitTargetChunks,
} from "./lib/gateway-server-test-plan.mts";
import { GIT_LS_FILES_MAX_BUFFER_BYTES } from "./lib/list-test-files.mts";
import { readTestSelectorSourceFacts } from "./lib/test-selector-source-facts.mts";
// CI imports planning before dependency installation; execution owners stay outside this closure.
import { resolveVitestCliEntry } from "./lib/vitest-build-prerequisites.mts";
import { resolveVitestCacheRoot, resolveVitestCacheSlotPath } from "./lib/vitest-cache-slots.mts";
import {
collectVitestFileFilters,
resolveBooleanModeFlag,
vitestOptionConsumesNextArg,
} from "./lib/vitest-cli-mode.mts";
import {
isCiLikeEnv,
resolveLocalFullSuiteProfile,
type VitestHostInfo,
} from "./lib/vitest-local-scheduling.mts";
import {
DEFAULT_VITEST_NO_OUTPUT_HEARTBEAT_MS,
resolveDefaultVitestNoOutputTimeoutMs,
resolveVitestNodeArgs,
} from "./lib/vitest-process-env.mts";
import {
estimateVitestTestFileSeconds,
estimateVitestToolingFileSeconds,
resolveShardTimingKey,
type VitestShardTimingSpec,
} from "./lib/vitest-shard-metadata.mts";
export { isTestFileTarget } from "./lib/changed-path-facts.mjs";
type VitestRunPlan = {
config: string;
databaseWorkerWatchOwner?: string;
databaseWorkerWatchTests?: string[];
forwardedArgs: string[];
timingTargets?: string[];
includePatterns: string[] | null;
watchMode: boolean;
};
export type VitestRunSpec = ReturnType<typeof createVitestRunSpecs>[number];
export type FailedVitestShard = {
code?: number | null;
config: string;
includePatterns?: string[] | null;
noOutputTimedOut?: boolean;
order?: number;
signal?: string | null;
};
type ChangedTestTargetOptions = {
cwd?: string;
env?: NodeJS.ProcessEnv;
broad?: boolean;
combineSiblingWithImportGraph?: boolean;
boundedOwners?: boolean;
forceFullImportGraph?: boolean;
resolveAliases?: boolean;
runtimeOnly?: boolean;
includeExtensionImpact?: boolean;
watchMode?: boolean;
};
type ChangedTestTargetPlan = {
mode: "none" | "broad" | "targets";
targets: string[];
ownerTargets?: string[];
ownerAreas?: string[];
skippedBroadFallbackPaths?: string[];
};
type ImportGraphOptions = {
tooling?: boolean;
resolveAliases?: boolean;
runtimeOnly?: boolean;
direct?: boolean;
};
type ImportGraphAlias = { pattern: string; targets: string[] };
type VitestSpecShape = Pick<VitestRunSpec, "config" | "env"> & {
cacheAssignment?: VitestCacheAssignment;
};
export type VitestCacheAssignment =
| { kind: "scheduler"; root: string; leased?: true }
| { kind: "caller" };
type CacheAssignedSpec<T> = Omit<T, "env"> & {
env: NodeJS.ProcessEnv;
cacheAssignment?: VitestCacheAssignment;
};
type WatchableVitestSpecShape = VitestSpecShape & Pick<VitestRunSpec, "watchMode">;
type ImportGraph = {
files: readonly string[];
reverseImports: Map<string, string[]>;
testFiles: Set<string>;
};
type ImportGraphEdges = {
file: string;
specifiers: string[];
typeOnlySpecifiers: Set<string>;
imports: Set<string>;
references: Set<string>;
};
type UnmatchedExplicitTestTarget = {
target: string;
reason: "glob-matched-no-files" | "path-does-not-exist" | "target-matched-no-test-files";
includePattern?: string;
};
const DEFAULT_VITEST_CONFIG = "test/vitest/vitest.unit.config.ts";
const AGENTS_EMBEDDED_AGENT_TEST_ROOT = agentVitestProjectOwners.embedded.root;
const AGENTS_SPAWN_PRODUCTION_BOUNDARY_VITEST_CONFIG =
agentVitestProjectOwners.spawnProductionBoundary.config;
const AGENTS_CORE_ISOLATED_VITEST_CONFIG = agentVitestProjectOwners.coreIsolated.config;
const AGENTS_CORE_VITEST_CONFIG = agentVitestProjectOwners.core.config;
const AGENTS_EMBEDDED_AGENT_VITEST_CONFIG = agentVitestProjectOwners.embedded.config;
const AGENTS_EMBEDDED_AGENT_INCOMPLETE_TURN_VITEST_CONFIG =
agentVitestProjectOwners.embeddedIncompleteTurn.config;
const AGENTS_EMBEDDED_AGENT_OVERFLOW_COMPACTION_VITEST_CONFIG =
agentVitestProjectOwners.embeddedOverflowCompaction.config;
const AGENTS_EMBEDDED_AGENT_RUN_VITEST_CONFIG = agentVitestProjectOwners.embeddedRun.config;
const AGENTS_SUPPORT_VITEST_CONFIG = agentVitestProjectOwners.support.config;
const AGENTS_TOOLS_VITEST_CONFIG = agentVitestProjectOwners.tools.config;
const AGENTS_VITEST_CONFIG = agentVitestProjectOwners.all.config;
const ACP_VITEST_CONFIG = "test/vitest/vitest.acp.config.ts";
const AUTO_REPLY_CORE_VITEST_CONFIG = "test/vitest/vitest.auto-reply-core.config.ts";
const AUTO_REPLY_VITEST_CONFIG = "test/vitest/vitest.auto-reply.config.ts";
const AUTO_REPLY_REPLY_VITEST_CONFIG = "test/vitest/vitest.auto-reply-reply.config.ts";
const AUTO_REPLY_TOP_LEVEL_VITEST_CONFIG = "test/vitest/vitest.auto-reply-top-level.config.ts";
const BOUNDARY_VITEST_CONFIG = "test/vitest/vitest.boundary.config.ts";
const BUNDLED_VITEST_CONFIG = "test/vitest/vitest.bundled.config.ts";
const CHANNEL_VITEST_CONFIG = "test/vitest/vitest.channels.config.ts";
const CLI_PROCESS_VITEST_CONFIG = "test/vitest/vitest.cli-process.config.ts";
const CLI_VITEST_CONFIG = "test/vitest/vitest.cli.config.ts";
const COMMANDS_LIGHT_VITEST_CONFIG = "test/vitest/vitest.commands-light.config.ts";
const COMMANDS_VITEST_CONFIG = "test/vitest/vitest.commands.config.ts";
const CONTRACTS_CHANNEL_CONFIG_VITEST_CONFIG =
"test/vitest/vitest.contracts-channel-config.config.ts";
const CONTRACTS_CHANNEL_REGISTRY_VITEST_CONFIG =
"test/vitest/vitest.contracts-channel-registry.config.ts";
const CONTRACTS_CHANNEL_SESSION_VITEST_CONFIG =
"test/vitest/vitest.contracts-channel-session.config.ts";
const CONTRACTS_CHANNEL_SURFACE_VITEST_CONFIG =
"test/vitest/vitest.contracts-channel-surface.config.ts";
export const CONTRACTS_PLUGIN_VITEST_CONFIG = "test/vitest/vitest.contracts-plugin.config.ts";
const CRON_VITEST_CONFIG = "test/vitest/vitest.cron.config.ts";
const DAEMON_VITEST_CONFIG = "test/vitest/vitest.daemon.config.ts";
export const E2E_VITEST_CONFIG = "test/vitest/vitest.e2e.config.ts";
const EXTENSION_ACTIVE_MEMORY_VITEST_CONFIG =
"test/vitest/vitest.extension-active-memory.config.ts";
const EXTENSION_ACPX_VITEST_CONFIG = "test/vitest/vitest.extension-acpx.config.ts";
const EXTENSION_BROWSER_VITEST_CONFIG = "test/vitest/vitest.extension-browser.config.ts";
const EXTENSION_CODEX_VITEST_CONFIG = "test/vitest/vitest.extension-codex.config.ts";
const EXTENSION_CODEX_APP_SERVER_ATTEMPT_LIGHT_VITEST_CONFIG =
"test/vitest/vitest.extension-codex-app-server-attempt-light.config.ts";
const EXTENSION_CODEX_APP_SERVER_ATTEMPT_SUPPORT_VITEST_CONFIG =
"test/vitest/vitest.extension-codex-app-server-attempt-support.config.ts";
const EXTENSION_CODEX_APP_SERVER_RUNTIME_VITEST_CONFIG =
"test/vitest/vitest.extension-codex-app-server-runtime.config.ts";
const EXTENSION_CODEX_APP_SERVER_SUPPORT_VITEST_CONFIG =
"test/vitest/vitest.extension-codex-app-server-support.config.ts";
const EXTENSION_CODEX_APP_SERVER_TOOLS_VITEST_CONFIG =
"test/vitest/vitest.extension-codex-app-server-tools.config.ts";
const EXTENSION_CODEX_SURFACE_VITEST_CONFIG =
"test/vitest/vitest.extension-codex-surface.config.ts";
const EXTENSION_DIFFS_VITEST_CONFIG = "test/vitest/vitest.extension-diffs.config.ts";
const EXTENSION_DISCORD_VITEST_CONFIG = "test/vitest/vitest.extension-discord.config.ts";
const EXTENSION_FEISHU_VITEST_CONFIG = "test/vitest/vitest.extension-feishu.config.ts";
const EXTENSION_IMESSAGE_VITEST_CONFIG = "test/vitest/vitest.extension-imessage.config.ts";
const EXTENSION_IRC_VITEST_CONFIG = "test/vitest/vitest.extension-irc.config.ts";
const EXTENSION_LINE_VITEST_CONFIG = "test/vitest/vitest.extension-line.config.ts";
const EXTENSION_MATTERMOST_VITEST_CONFIG = "test/vitest/vitest.extension-mattermost.config.ts";
const EXTENSION_MEDIA_VITEST_CONFIG = "test/vitest/vitest.extension-media.config.ts";
const EXTENSION_MATRIX_VITEST_CONFIG = "test/vitest/vitest.extension-matrix.config.ts";
const EXTENSION_MEMORY_VITEST_CONFIG = "test/vitest/vitest.extension-memory.config.ts";
const EXTENSION_MSTEAMS_VITEST_CONFIG = "test/vitest/vitest.extension-msteams.config.ts";
const EXTENSION_MESSAGING_VITEST_CONFIG = "test/vitest/vitest.extension-messaging.config.ts";
const EXTENSION_MISC_VITEST_CONFIG = "test/vitest/vitest.extension-misc.config.ts";
const EXTENSION_PROVIDER_OPENAI_VITEST_CONFIG =
"test/vitest/vitest.extension-provider-openai.config.ts";
const EXTENSION_PROVIDERS_VITEST_CONFIG = "test/vitest/vitest.extension-providers.config.ts";
const EXTENSION_QA_VITEST_CONFIG = "test/vitest/vitest.extension-qa.config.ts";
const EXTENSION_SIGNAL_VITEST_CONFIG = "test/vitest/vitest.extension-signal.config.ts";
const EXTENSION_SLACK_VITEST_CONFIG = "test/vitest/vitest.extension-slack.config.ts";
const EXTENSION_DATABASE_WORKERS_VITEST_CONFIG =
"test/vitest/vitest.extension-database-workers.config.ts";
const EXTENSION_TELEGRAM_VITEST_CONFIG = "test/vitest/vitest.extension-telegram.config.ts";
const EXTENSION_VOICE_CALL_VITEST_CONFIG = "test/vitest/vitest.extension-voice-call.config.ts";
const EXTENSION_WHATSAPP_VITEST_CONFIG = "test/vitest/vitest.extension-whatsapp.config.ts";
const EXTENSION_ZALO_VITEST_CONFIG = "test/vitest/vitest.extension-zalo.config.ts";
const EXTENSIONS_VITEST_CONFIG = "test/vitest/vitest.extensions.config.ts";
const FULL_AGENTIC_VITEST_CONFIG = "test/vitest/vitest.full-agentic.config.ts";
const FULL_EXTENSIONS_VITEST_CONFIG = "test/vitest/vitest.full-extensions.config.ts";
const GATEWAY_CLIENT_VITEST_CONFIG = "test/vitest/vitest.gateway-client.config.ts";
const GATEWAY_CORE_VITEST_CONFIG = "test/vitest/vitest.gateway-core.config.ts";
const GATEWAY_DATABASE_WORKERS_VITEST_CONFIG =
"test/vitest/vitest.gateway-database-workers.config.ts";
const GATEWAY_METHODS_VITEST_CONFIG = "test/vitest/vitest.gateway-methods.config.ts";
const GATEWAY_SERVER_VITEST_CONFIG = "test/vitest/vitest.gateway-server.config.ts";
const GATEWAY_VITEST_CONFIG = "test/vitest/vitest.gateway.config.ts";
const HOOKS_VITEST_CONFIG = "test/vitest/vitest.hooks.config.ts";
const INFRA_VITEST_CONFIG = "test/vitest/vitest.infra.config.ts";
const MEDIA_VITEST_CONFIG = "test/vitest/vitest.media.config.ts";
const MEDIA_UNDERSTANDING_VITEST_CONFIG = "test/vitest/vitest.media-understanding.config.ts";
const LOGGING_VITEST_CONFIG = "test/vitest/vitest.logging.config.ts";
const PLUGIN_SDK_LIGHT_VITEST_CONFIG = "test/vitest/vitest.plugin-sdk-light.config.ts";
const PLUGIN_SDK_VITEST_CONFIG = "test/vitest/vitest.plugin-sdk.config.ts";
const PLUGINS_VITEST_CONFIG = "test/vitest/vitest.plugins.config.ts";
const UNIT_FAST_VITEST_CONFIG = "test/vitest/vitest.unit-fast.config.ts";
const UNIT_FAST_ISOLATED_VITEST_CONFIG = "test/vitest/vitest.unit-fast-isolated.config.ts";
const UNIT_FAST_FAKE_TIMERS_VITEST_CONFIG = "test/vitest/vitest.unit-fast-fake-timers.config.ts";
const UNIT_SECURITY_VITEST_CONFIG = "test/vitest/vitest.unit-security.config.ts";
const UNIT_SRC_VITEST_CONFIG = "test/vitest/vitest.unit-src.config.ts";
const UNIT_SUPPORT_VITEST_CONFIG = "test/vitest/vitest.unit-support.config.ts";
const EXTENSION_TEST_PROCESS_ROOTS = new Map([
[EXTENSION_CODEX_VITEST_CONFIG, codexExtensionTestRoots],
[EXTENSION_MATRIX_VITEST_CONFIG, matrixExtensionTestRoots],
[EXTENSION_TELEGRAM_VITEST_CONFIG, telegramExtensionTestRoots],
[
EXTENSION_DATABASE_WORKERS_VITEST_CONFIG,
[...databaseWorkerExtensionTestRoots, ...databaseWorkerExtensionTestFiles],
],
]);
const FULL_SUITE_CONFIG_WEIGHT = new Map([
[GATEWAY_VITEST_CONFIG, 180],
[GATEWAY_SERVER_VITEST_CONFIG, 180],
[GATEWAY_CORE_VITEST_CONFIG, 179],
[GATEWAY_CLIENT_VITEST_CONFIG, 178],
[GATEWAY_METHODS_VITEST_CONFIG, 177],
[COMMANDS_VITEST_CONFIG, 175],
[AGENTS_CORE_VITEST_CONFIG, 170],
[AGENTS_EMBEDDED_AGENT_VITEST_CONFIG, 169],
[AGENTS_EMBEDDED_AGENT_INCOMPLETE_TURN_VITEST_CONFIG, 169],
[AGENTS_EMBEDDED_AGENT_OVERFLOW_COMPACTION_VITEST_CONFIG, 169],
[AGENTS_EMBEDDED_AGENT_RUN_VITEST_CONFIG, 169],
[AGENTS_SUPPORT_VITEST_CONFIG, 168],
[AGENTS_TOOLS_VITEST_CONFIG, 167],
[EXTENSION_CODEX_VITEST_CONFIG, 168],
[EXTENSION_CODEX_APP_SERVER_ATTEMPT_LIGHT_VITEST_CONFIG, 82],
[EXTENSION_CODEX_APP_SERVER_ATTEMPT_SUPPORT_VITEST_CONFIG, 80],
[EXTENSION_CODEX_APP_SERVER_RUNTIME_VITEST_CONFIG, 88],
[EXTENSION_CODEX_APP_SERVER_TOOLS_VITEST_CONFIG, 78],
[EXTENSION_CODEX_APP_SERVER_SUPPORT_VITEST_CONFIG, 72],
[EXTENSION_CODEX_SURFACE_VITEST_CONFIG, 68],
[EXTENSION_VOICE_CALL_VITEST_CONFIG, 169],
[EXTENSIONS_VITEST_CONFIG, 168],
[EXTENSION_PROVIDER_OPENAI_VITEST_CONFIG, 167],
["test/vitest/vitest.runtime-config.config.ts", 166],
[CONTRACTS_CHANNEL_CONFIG_VITEST_CONFIG, 85],
[CONTRACTS_CHANNEL_SURFACE_VITEST_CONFIG, 60],
[CONTRACTS_CHANNEL_SESSION_VITEST_CONFIG, 50],
[CONTRACTS_CHANNEL_REGISTRY_VITEST_CONFIG, 35],
[CONTRACTS_PLUGIN_VITEST_CONFIG, 20],
[CHANNEL_VITEST_CONFIG, 164],
[UNIT_FAST_VITEST_CONFIG, 160],
[UNIT_FAST_ISOLATED_VITEST_CONFIG, 159],
[AUTO_REPLY_REPLY_VITEST_CONFIG, 155],
[INFRA_VITEST_CONFIG, 145],
["test/vitest/vitest.secrets.config.ts", 140],
[CRON_VITEST_CONFIG, 135],
["test/vitest/vitest.wizard.config.ts", 130],
[UNIT_SRC_VITEST_CONFIG, 125],
[EXTENSION_MATRIX_VITEST_CONFIG, 100],
[EXTENSION_DISCORD_VITEST_CONFIG, 98],
[EXTENSION_PROVIDERS_VITEST_CONFIG, 96],
[EXTENSION_TELEGRAM_VITEST_CONFIG, 94],
[EXTENSION_WHATSAPP_VITEST_CONFIG, 92],
[AUTO_REPLY_CORE_VITEST_CONFIG, 90],
[CLI_PROCESS_VITEST_CONFIG, 87],
[CLI_VITEST_CONFIG, 86],
[MEDIA_VITEST_CONFIG, 84],
[PLUGINS_VITEST_CONFIG, 82],
[BUNDLED_VITEST_CONFIG, 80],
[EXTENSION_SLACK_VITEST_CONFIG, 78],
[COMMANDS_LIGHT_VITEST_CONFIG, 48],
[PLUGIN_SDK_VITEST_CONFIG, 46],
[AUTO_REPLY_TOP_LEVEL_VITEST_CONFIG, 45],
[PLUGIN_SDK_LIGHT_VITEST_CONFIG, 38],
[DAEMON_VITEST_CONFIG, 36],
[BOUNDARY_VITEST_CONFIG, 34],
["test/vitest/vitest.tooling.config.ts", 32],
["test/vitest/vitest.tooling-isolated.config.ts", 1],
[UNIT_SECURITY_VITEST_CONFIG, 30],
[UNIT_SUPPORT_VITEST_CONFIG, 28],
[EXTENSION_ZALO_VITEST_CONFIG, 24],
[EXTENSION_IRC_VITEST_CONFIG, 20],
[EXTENSION_FEISHU_VITEST_CONFIG, 18],
[EXTENSION_MATTERMOST_VITEST_CONFIG, 16],
[EXTENSION_MESSAGING_VITEST_CONFIG, 14],
[EXTENSION_IMESSAGE_VITEST_CONFIG, 13],
[EXTENSION_LINE_VITEST_CONFIG, 12],
[EXTENSION_SIGNAL_VITEST_CONFIG, 11],
[EXTENSION_ACPX_VITEST_CONFIG, 10],
[EXTENSION_DIFFS_VITEST_CONFIG, 8],
[EXTENSION_ACTIVE_MEMORY_VITEST_CONFIG, 7],
[EXTENSION_MEMORY_VITEST_CONFIG, 6],
[EXTENSION_MSTEAMS_VITEST_CONFIG, 4],
]);
function resolveSpecSortWeight(
spec: VitestShardTimingSpec,
shardTimings: ReadonlyMap<string, number>,
) {
const observed = shardTimings.get(resolveShardTimingKey(spec));
if (observed !== undefined) {
return observed;
}
// Exact selections use their file costs; a whole-config sample would price a
// single worker proof like all tooling. Globs keep the whole-config fallback.
const includes = spec.timingTargets ?? spec.includePatterns;
const estimateFileSeconds =
spec.config === TOOLING_VITEST_CONFIG
? estimateVitestToolingFileSeconds
: estimateVitestTestFileSeconds;
const seconds =
includes?.length && includes.every((file) => isTestFileTarget(file) && !isGlobTarget(file))
? includes.reduce((total, file) => total + estimateFileSeconds(file), 0)
: (FULL_SUITE_CONFIG_WEIGHT.get(spec.config) ?? 0);
return seconds * 1000;
}
function interleaveSlowAndFastSpecs<T>(sortedSpecs: T[]) {
const ordered: T[] = [];
let slowIndex = 0;
let fastIndex = sortedSpecs.length - 1;
while (slowIndex <= fastIndex) {
const slowSpec = sortedSpecs[slowIndex];
if (slowSpec !== undefined) {
ordered.push(slowSpec);
}
slowIndex += 1;
if (slowIndex <= fastIndex) {
const fastSpec = sortedSpecs[fastIndex];
if (fastSpec !== undefined) {
ordered.push(fastSpec);
}
fastIndex -= 1;
}
}
return ordered;
}
function uniqueOrdered<T>(values: T[]) {
return [...new Set(values)];
}
function isPathAtOrUnder(relative: string, root: string) {
return relative === root || relative.startsWith(`${root}/`);
}
/**
* Orders full-suite specs so expensive shards start first in parallel runs.
*/
export function orderFullSuiteSpecsForParallelRun<T extends VitestShardTimingSpec>(
specs: T[],
shardTimings = new Map<string, number>(),
): T[] {
const sortedSpecs = specs.toSorted((a, b) => {
const weightDelta =
resolveSpecSortWeight(b, shardTimings) - resolveSpecSortWeight(a, shardTimings);
if (weightDelta !== 0) {
return weightDelta;
}
return a.config.localeCompare(b.config);
});
return interleaveSlowAndFastSpecs(sortedSpecs);
}
const PROCESS_VITEST_CONFIG = "test/vitest/vitest.process.config.ts";
const RUNTIME_CONFIG_VITEST_CONFIG = "test/vitest/vitest.runtime-config.config.ts";
const SECRETS_VITEST_CONFIG = "test/vitest/vitest.secrets.config.ts";
const SHARED_CORE_VITEST_CONFIG = "test/vitest/vitest.shared-core.config.ts";
const PACKAGE_CONTRACT_VITEST_CONFIG = "test/vitest/vitest.package-contract.config.ts";
const TOOLING_DOCKER_VITEST_CONFIG = "test/vitest/vitest.tooling-docker.config.ts";
const TOOLING_ISOLATED_VITEST_CONFIG = "test/vitest/vitest.tooling-isolated.config.ts";
const TOOLING_VITEST_CONFIG = "test/vitest/vitest.tooling.config.ts";
const TOOLING_DOCKER_TEST_TARGET = "test/scripts/docker-build-helper.test.ts";
const BROAD_TOOLING_SCRIPT_TEST_PATTERNS = new Set([
"test/scripts/**/*.test.ts",
"test/scripts/*.test.ts",
]);
const BROAD_TOOLING_SCRIPT_TEST_TARGET_CHUNK_SIZE = 60;
const FULL_SUITE_AGENTS_CORE_TEST_TARGET_CHUNK_COUNT = 6;
const FULL_SUITE_INFRA_TEST_TARGET_CHUNK_SIZE = 64;
const FULL_SUITE_TOOLING_TEST_TARGET_CHUNK_SIZE = 2;
const FULL_SUITE_UNIT_FAST_TEST_TARGET_CHUNK_SIZE = 70;
const FULL_SUITE_UNIT_SRC_TEST_TARGET_CHUNK_SIZE = 150;
const TUI_VITEST_CONFIG = "test/vitest/vitest.tui.config.ts";
const TUI_PTY_VITEST_CONFIG = "test/vitest/vitest.tui-pty.config.ts";
const UI_VITEST_CONFIG = "test/vitest/vitest.ui.config.ts";
const UI_BROWSER_VITEST_CONFIG = "test/vitest/vitest.ui-browser.config.ts";
export const UI_E2E_VITEST_CONFIG = "test/vitest/vitest.ui-e2e.config.ts";
const UI_ISOLATED_VITEST_CONFIG = "test/vitest/vitest.ui-isolated.config.ts";
const UTILS_VITEST_CONFIG = "test/vitest/vitest.utils.config.ts";
const WIZARD_VITEST_CONFIG = "test/vitest/vitest.wizard.config.ts";
const INCLUDE_FILE_ENV_KEY = "OPENCLAW_VITEST_INCLUDE_FILE";
const FS_MODULE_CACHE_PATH_ENV_KEY = "OPENCLAW_VITEST_FS_MODULE_CACHE_PATH";
const FAILED_SHARD_DIGEST_LIMIT = 12;
const CHANGED_ARGS_PATTERN = /^--changed(?:=(.+))?$/u;
const VITEST_CONFIG_BY_KIND: Record<string, string> = {
unitFast: UNIT_FAST_VITEST_CONFIG,
unitFastIsolated: UNIT_FAST_ISOLATED_VITEST_CONFIG,
unitFastFakeTimers: UNIT_FAST_FAKE_TIMERS_VITEST_CONFIG,
boundary: BOUNDARY_VITEST_CONFIG,
toolingDocker: TOOLING_DOCKER_VITEST_CONFIG,
toolingIsolated: TOOLING_ISOLATED_VITEST_CONFIG,
tooling: TOOLING_VITEST_CONFIG,
contractsChannelSurface: CONTRACTS_CHANNEL_SURFACE_VITEST_CONFIG,
contractsChannelConfig: CONTRACTS_CHANNEL_CONFIG_VITEST_CONFIG,
contractsChannelRegistry: CONTRACTS_CHANNEL_REGISTRY_VITEST_CONFIG,
contractsChannelSession: CONTRACTS_CHANNEL_SESSION_VITEST_CONFIG,
contractsPlugin: CONTRACTS_PLUGIN_VITEST_CONFIG,
bundled: BUNDLED_VITEST_CONFIG,
gateway: GATEWAY_VITEST_CONFIG,
gatewayCore: GATEWAY_CORE_VITEST_CONFIG,
gatewayDatabaseWorkers: GATEWAY_DATABASE_WORKERS_VITEST_CONFIG,
gatewayClient: GATEWAY_CLIENT_VITEST_CONFIG,
gatewayMethods: GATEWAY_METHODS_VITEST_CONFIG,
gatewayServer: GATEWAY_SERVER_VITEST_CONFIG,
hooks: HOOKS_VITEST_CONFIG,
infra: INFRA_VITEST_CONFIG,
runtimeConfig: RUNTIME_CONFIG_VITEST_CONFIG,
cron: CRON_VITEST_CONFIG,
daemon: DAEMON_VITEST_CONFIG,
media: MEDIA_VITEST_CONFIG,
logging: LOGGING_VITEST_CONFIG,
packageContract: PACKAGE_CONTRACT_VITEST_CONFIG,
pluginSdkLight: PLUGIN_SDK_LIGHT_VITEST_CONFIG,
pluginSdk: PLUGIN_SDK_VITEST_CONFIG,
process: PROCESS_VITEST_CONFIG,
secrets: SECRETS_VITEST_CONFIG,
sharedCore: SHARED_CORE_VITEST_CONFIG,
tui: TUI_VITEST_CONFIG,
tuiPty: TUI_PTY_VITEST_CONFIG,
mediaUnderstanding: MEDIA_UNDERSTANDING_VITEST_CONFIG,
acp: ACP_VITEST_CONFIG,
cliProcess: CLI_PROCESS_VITEST_CONFIG,
cli: CLI_VITEST_CONFIG,
commandLight: COMMANDS_LIGHT_VITEST_CONFIG,
command: COMMANDS_VITEST_CONFIG,
autoReply: AUTO_REPLY_VITEST_CONFIG,
autoReplyCore: AUTO_REPLY_CORE_VITEST_CONFIG,
autoReplyReply: AUTO_REPLY_REPLY_VITEST_CONFIG,
autoReplyTopLevel: AUTO_REPLY_TOP_LEVEL_VITEST_CONFIG,
agentCore: AGENTS_CORE_VITEST_CONFIG,
agentEmbedded: AGENTS_EMBEDDED_AGENT_VITEST_CONFIG,
agentEmbeddedIncompleteTurn: AGENTS_EMBEDDED_AGENT_INCOMPLETE_TURN_VITEST_CONFIG,
agentEmbeddedOverflowCompaction: AGENTS_EMBEDDED_AGENT_OVERFLOW_COMPACTION_VITEST_CONFIG,
agentEmbeddedRun: AGENTS_EMBEDDED_AGENT_RUN_VITEST_CONFIG,
agentSupport: AGENTS_SUPPORT_VITEST_CONFIG,
agentTools: AGENTS_TOOLS_VITEST_CONFIG,
agent: AGENTS_VITEST_CONFIG,
agentsSpawnProductionBoundary: AGENTS_SPAWN_PRODUCTION_BOUNDARY_VITEST_CONFIG,
agentsCoreIsolated: AGENTS_CORE_ISOLATED_VITEST_CONFIG,
agentsCore: AGENTS_CORE_VITEST_CONFIG,
agentsSupport: AGENTS_SUPPORT_VITEST_CONFIG,
agentsTools: AGENTS_TOOLS_VITEST_CONFIG,
plugin: PLUGINS_VITEST_CONFIG,
ui: UI_VITEST_CONFIG,
uiIsolated: UI_ISOLATED_VITEST_CONFIG,
uiBrowser: UI_BROWSER_VITEST_CONFIG,
uiTiming: "test/vitest/vitest.ui-timing.config.ts",
uiE2e: UI_E2E_VITEST_CONFIG,
unitSrc: UNIT_SRC_VITEST_CONFIG,
unitSecurity: UNIT_SECURITY_VITEST_CONFIG,
unitSupport: UNIT_SUPPORT_VITEST_CONFIG,
utils: UTILS_VITEST_CONFIG,
wizard: WIZARD_VITEST_CONFIG,
e2e: E2E_VITEST_CONFIG,
extensionActiveMemory: EXTENSION_ACTIVE_MEMORY_VITEST_CONFIG,
extensionAcpx: EXTENSION_ACPX_VITEST_CONFIG,
extensionCodex: EXTENSION_CODEX_VITEST_CONFIG,
extensionDiffs: EXTENSION_DIFFS_VITEST_CONFIG,
extensionBrowser: EXTENSION_BROWSER_VITEST_CONFIG,
extensionDiscord: EXTENSION_DISCORD_VITEST_CONFIG,
extensionFeishu: EXTENSION_FEISHU_VITEST_CONFIG,
extensionImessage: EXTENSION_IMESSAGE_VITEST_CONFIG,
extensionIrc: EXTENSION_IRC_VITEST_CONFIG,
extensionLine: EXTENSION_LINE_VITEST_CONFIG,
extensionMattermost: EXTENSION_MATTERMOST_VITEST_CONFIG,
extensionDatabaseWorkers: EXTENSION_DATABASE_WORKERS_VITEST_CONFIG,
extensionTelegram: EXTENSION_TELEGRAM_VITEST_CONFIG,
extensionVoiceCall: EXTENSION_VOICE_CALL_VITEST_CONFIG,
extensionWhatsApp: EXTENSION_WHATSAPP_VITEST_CONFIG,
extensionZalo: EXTENSION_ZALO_VITEST_CONFIG,
extensionMatrix: EXTENSION_MATRIX_VITEST_CONFIG,
extensionMedia: EXTENSION_MEDIA_VITEST_CONFIG,
extensionMemory: EXTENSION_MEMORY_VITEST_CONFIG,
extensionMisc: EXTENSION_MISC_VITEST_CONFIG,
extensionMsTeams: EXTENSION_MSTEAMS_VITEST_CONFIG,
extensionMessaging: EXTENSION_MESSAGING_VITEST_CONFIG,
extensionProviderOpenAi: EXTENSION_PROVIDER_OPENAI_VITEST_CONFIG,
extensionProvider: EXTENSION_PROVIDERS_VITEST_CONFIG,
extensionQa: EXTENSION_QA_VITEST_CONFIG,
extensionSignal: EXTENSION_SIGNAL_VITEST_CONFIG,
extensionSlack: EXTENSION_SLACK_VITEST_CONFIG,
extensionFull: FULL_EXTENSIONS_VITEST_CONFIG,
channel: CHANNEL_VITEST_CONFIG,
extension: EXTENSIONS_VITEST_CONFIG,
};
const BROAD_CHANGED_FALLBACK_PATTERNS = [
/^package\.json$/u,
/^pnpm-lock\.yaml$/u,
/^test\/setup(?:\.shared|\.extensions|-openclaw-runtime)?\.ts$/u,
/^vitest(?:\..+)?\.(?:config\.ts|paths\.mjs)$/u,
/^test\/vitest\/vitest\.(?:config|shared\.config|scoped-config|performance-config)\.ts$/u,
/^test\/helpers\//u,
];
const PRECISE_SOURCE_TEST_TARGETS = new Map<string, string[]>([
["src/plugins/runtime.retention.test-support.ts", ["src/plugins/runtime.retention.test.ts"]],
[
"src/agents/bash-tools.process-liveness-child.test-support.ts",
["src/agents/bash-tools.process.liveness.test.ts"],
],
[
"patches/vitest@5.0.1.patch",
[
"test/scripts/run-vitest-profile.test.ts",
"test/scripts/run-vitest-state-cleanup.test.ts",
"test/scripts/vitest-fork-shutdown.test.ts",
"test/scripts/vitest-runner-task-updates.test.ts",
],
],
["test/fixtures/vitest-fork-shutdown.mjs", ["test/scripts/vitest-fork-shutdown.test.ts"]],
...["clock", "runner"].map<[string, string[]]>((part) => [
`test/fixtures/vitest-runner-task-updates.${part}.mjs`,
["test/scripts/vitest-runner-task-updates.test.ts"],
]),
...[
"src/system-agent/setup-inference-turn.ts",
"src/agents/embedded-agent-runner/run/run-attempt-dispatch.ts",
].map<[string, string[]]>((sourcePath) => [
sourcePath,
[
"src/agents/embedded-agent-runner/run.overflow-compaction.loop.test.ts",
"src/commands/onboard-guided.inference.e2e.test.ts",
],
]),
[
"src/plugins/contracts/tts-contract-suites.ts",
[
"src/plugins/contracts/core-extension-facade-boundary.test.ts",
"src/plugins/contracts/tts.contract.test.ts",
],
],
[
"extensions/slack/src/monitor/enterprise-install.ts",
[
"extensions/slack/src/monitor/enterprise-install.test.ts",
"extensions/slack/src/monitor/provider.auth-test-token.test.ts",
],
],
[
"extensions/slack/src/channel-actions.ts",
[
"extensions/slack/src/actions.reactions-limit.test.ts",
"extensions/slack/src/channel-actions-setup-status.contract.test.ts",
"extensions/slack/src/message-tools.test.ts",
],
],
[
"src/gateway/worker-environments/worker-turn-launcher.ts",
[
"src/gateway/worker-environments/worker-turn-launcher.test.ts",
"src/gateway/worker-environments/worker-turn-launcher-claim-admission.test.ts",
"src/gateway/worker-environments/worker-turn-launcher-failure-recovery.test.ts",
"src/gateway/worker-environments/worker-turn-launcher-reclaimed-placement.test.ts",
"src/gateway/worker-environments/worker-turn-launcher-remote-handoff.test.ts",
"src/gateway/worker-environments/worker-turn-launcher-terminal-results.test.ts",
],
],
]);
const DOCS_CONFIG_EXAMPLES_TEST_TARGET = "src/config/docs-config-examples.test.ts";
const RUNTIME_SIDECAR_BASELINE_OWNER_TEST_TARGETS = ["src/plugins/bundled-plugin-metadata.test.ts"];
const RUNTIME_SIDECAR_PATH_CONSUMER_TEST_TARGETS = [
...RUNTIME_SIDECAR_BASELINE_OWNER_TEST_TARGETS,
"src/infra/update-global.test.ts",
"src/infra/update-runner.test.ts",
"test/openclaw-npm-postpublish-verify.test.ts",
];
const GITHUB_YAML_PINNING_GUARD_TEST_TARGETS = ["test/scripts/ci-workflow-guards.test.ts"];
const GROUP_VISIBLE_REPLY_TEST_TARGETS = [
"src/auto-reply/reply/dispatch-acp.test.ts",
"src/auto-reply/reply/dispatch-from-config.test.ts",
"src/auto-reply/reply/dispatch-from-config.delivery.test.ts",
"src/auto-reply/reply/dispatch-from-config.lifecycle.test.ts",
"src/auto-reply/reply/followup-runner.test.ts",
"src/auto-reply/reply/groups.test.ts",
"extensions/discord/src/monitor/message-handler.process.test.ts",
"extensions/slack/src/monitor.tool-result.test.ts",
];
const GROUP_VISIBLE_REPLY_PROMPT_TEST_TARGETS = [
"src/agents/system-prompt.test.ts",
...GROUP_VISIBLE_REPLY_TEST_TARGETS,
];
const CHANNEL_CONTRACT_REGISTRY_BACKED_TARGETS = [
"directory",
"plugin",
"surfaces-only",
"threading",
].flatMap((suite) =>
"abcdefgh"
.split("")
.map(
(shard) =>
`src/channels/plugins/contracts/${suite}.registry-backed-shard-${shard}.contract.test.ts`,
),
);
const CHANNEL_PLUGIN_SHAPE_PARITY_TEST_TARGET =
"src/channels/plugins/contracts/plugin-shape.contract.test.ts";
const CHANNEL_PLUGIN_SHAPE_PARITY_WIRING_PATHS = new Set([
"extensions/imessage/message-tool-api.ts",
"extensions/imessage/src/actions.ts",
"extensions/imessage/src/channel.ts",
"extensions/slack/message-tool-api.ts",
"extensions/slack/src/channel-actions.ts",
"extensions/slack/src/channel.ts",
"extensions/mattermost/gateway-auth-api.ts",
"extensions/mattermost/src/channel.ts",
"extensions/feishu/session-key-api.ts",
"extensions/feishu/src/channel.ts",
"extensions/telegram/session-key-api.ts",
"extensions/telegram/src/channel.ts",
"extensions/discord/session-key-api.ts",
"extensions/discord/thread-binding-api.ts",
"extensions/discord/src/channel.ts",
"extensions/matrix/thread-binding-api.ts",
"extensions/matrix/src/channel.ts",
]);
const TEST_HELPER_NORMALIZE_TEXT_TARGETS = [
"src/auto-reply/reply/commands-status.test.ts",
"src/auto-reply/status.test.ts",
"src/tui/components/chat-log.test.ts",
];
const HAPPY_PATH_PROMPT_SNAPSHOT_HELPER_TEST_TARGETS = ["test/scripts/prompt-snapshots.test.ts"];
const APPCAST_TEST_TARGETS = ["test/appcast.test.ts", "test/scripts/make-appcast.test.ts"];
const CODEX_VERSION_CONTRACT_TEST_TARGETS = [
"extensions/codex/src/manifest.test.ts",
"extensions/openai/openai-provider.test.ts",
"test/scripts/codex-client-version-contract.test.ts",
];
// The iframe script and native document load as assets outside the import graph.
const MERMAID_RENDERER_TEST_TARGETS = [
"ui/src/components/markdown-mermaid.runtime.browser.test.ts",
"ui/src/components/markdown-mermaid-native.browser.test.ts",
];
const SOURCE_TEST_TARGETS = new Map([
...PRECISE_SOURCE_TEST_TARGETS,
[
"src/agents/live-provider-owner.ts",
[
"src/agents/live-model-dynamic-candidates.test.ts",
"src/agents/live-model-filter.test.ts",
"src/agents/live-target-matcher.test.ts",
"src/agents/model-compat.test.ts",
],
],
["src/plugin-sdk/memory-host-events.ts", ["src/plugin-sdk/memory-host-events.test.ts"]],
["src/plugin-sdk/persistent-dedupe.ts", ["src/plugin-sdk/memory-host-events.test.ts"]],
[
"extensions/browser/src/browser/chrome-mcp-options.ts",
["extensions/browser/src/browser/chrome-mcp.test.ts"],
],
[
"scripts/prepare-apple-mermaid.mjs",
[
"test/scripts/build-and-run-mac.test.ts",
"test/scripts/package-mac-app.test.ts",
"test/scripts/ci-workflow-guards.test.ts",
],
],
["packages/mermaid-renderer/package.json", MERMAID_RENDERER_TEST_TARGETS],
["packages/mermaid-renderer/vite.config.ts", MERMAID_RENDERER_TEST_TARGETS],
["packages/mermaid-renderer/native/index.html", MERMAID_RENDERER_TEST_TARGETS],
["packages/mermaid-renderer/src/renderer.ts", MERMAID_RENDERER_TEST_TARGETS],
["packages/mermaid-renderer/src/frame.js", MERMAID_RENDERER_TEST_TARGETS],
["packages/mermaid-renderer/src/native.ts", MERMAID_RENDERER_TEST_TARGETS],
[
"packages/normalization-core/src/record-coerce.ts",
["packages/normalization-core/src/record-coerce.test.ts", ...MERMAID_RENDERER_TEST_TARGETS],
],
[
"packages/normalization-core/package.json",
["packages/normalization-core/src/package-exports.test.ts", ...MERMAID_RENDERER_TEST_TARGETS],
],
["extensions/codex/package.json", CODEX_VERSION_CONTRACT_TEST_TARGETS],
["extensions/codex/src/app-server/version.ts", CODEX_VERSION_CONTRACT_TEST_TARGETS],
...["index", "harness"].map<[string, string[]]>((entry) => [
`extensions/copilot/${entry}.ts`,
[
`extensions/copilot/${entry}.test.ts`,
"src/agents/prepared-model-runtime.copilot.integration.test.ts",
],
]),
[
"extensions/copilot/openclaw.plugin.json",
[
"extensions/copilot/openclaw.plugin.json",
DOCS_CONFIG_EXAMPLES_TEST_TARGET,
"src/agents/prepared-model-runtime.copilot.integration.test.ts",
],
],
["src/test-utils/openclaw-test-state.ts", ["src/test-utils/openclaw-test-state.test.ts"]],
[
"src/channels/plugins/contracts/test-helpers/manifest.ts",
[
...CHANNEL_CONTRACT_REGISTRY_BACKED_TARGETS,
"src/channels/plugins/contracts/registry.contract.test.ts",
"src/channels/plugins/contracts/session-binding.registry-backed.contract.test.ts",
],
],
[
"src/channels/plugins/contracts/test-helpers/registry-backed-contract-shards.ts",
CHANNEL_CONTRACT_REGISTRY_BACKED_TARGETS,
],
["test/helpers/normalize-text.ts", TEST_HELPER_NORMALIZE_TEXT_TARGETS],
[
"test/helpers/agents/happy-path-prompt-snapshots.ts",
HAPPY_PATH_PROMPT_SNAPSHOT_HELPER_TEST_TARGETS,
],
[
"test/e2e/qa-lab/runtime/qa-otel-smoke-runtime.ts",
["test/e2e/qa-lab/runtime/qa-otel-smoke.e2e.test.ts"],
],
[
"test/e2e/qa-lab/runtime/heartbeat-active-hours-runtime.ts",
["test/e2e/qa-lab/runtime/heartbeat-active-hours-runtime.test.ts"],
],
[
"test/e2e/qa-lab/runtime/telegram-bot-token-runtime.ts",
["test/e2e/qa-lab/runtime/telegram-bot-token-runtime.test.ts"],
],
["src/plugins/runtime-sidecar-paths-baseline.ts", RUNTIME_SIDECAR_BASELINE_OWNER_TEST_TARGETS],
["src/plugins/runtime-sidecar-paths.ts", RUNTIME_SIDECAR_PATH_CONSUMER_TEST_TARGETS],
["ui/config/control-ui-chunking.ts", ["ui/src/app/control-ui-chunking.test.ts"]],
[
"ui/config/control-ui-boot-modules.json",
["ui/src/app/control-ui-chunking.test.ts", "ui/src/app/vite-config.node.test.ts"],
],
["ui/config/control-ui-locales.ts", ["ui/src/app/vite-config.node.test.ts"]],
[
"src/plugin-sdk/test-helpers/directory-ids.ts",
[
"extensions/discord/src/directory-contract.test.ts",
"extensions/slack/src/directory-contract.test.ts",
"extensions/telegram/src/directory-contract.test.ts",
],
],
[
"src/plugin-sdk/channel-reply-pipeline.ts",
["src/plugins/contracts/plugin-sdk-subpaths.test.ts", ...GROUP_VISIBLE_REPLY_TEST_TARGETS],
],
["src/plugin-sdk/reply-runtime.ts", ["src/plugins/contracts/plugin-sdk-subpaths.test.ts"]],
["extensions/google-meet/index.ts", ["extensions/google-meet/index.test.ts"]],
[
"extensions/google-meet/src/cli.ts",
[
"extensions/google-meet/src/cli-artifacts.test.ts",
"extensions/google-meet/src/cli-runtime.test.ts",
"extensions/google-meet/src/cli.test.ts",
],
],
[
"extensions/google-meet/src/cli-artifact-commands.ts",
["extensions/google-meet/src/cli-artifacts.test.ts"],
],
[
"extensions/google-meet/src/cli-export.ts",
["extensions/google-meet/src/cli-artifacts.test.ts"],
],
[
"extensions/google-meet/src/cli-space-commands.ts",
["extensions/google-meet/src/cli-artifacts.test.ts"],
],
[
"extensions/google-meet/src/cli-runtime-commands.ts",
["extensions/google-meet/src/cli-runtime.test.ts"],
],
["extensions/google-meet/src/cli-doctor.ts", ["extensions/google-meet/src/cli.test.ts"]],
[
"extensions/google-meet/src/cli-command-context.ts",
[
"extensions/google-meet/src/cli-artifacts.test.ts",
"extensions/google-meet/src/cli-runtime.test.ts",
"extensions/google-meet/src/cli.test.ts",
],
],
[
"extensions/google-meet/src/cli-shared.ts",
[
"extensions/google-meet/src/cli-artifacts.test.ts",
"extensions/google-meet/src/cli-runtime.test.ts",
"extensions/google-meet/src/cli.test.ts",
],
],
["extensions/google-meet/src/create.ts", ["extensions/google-meet/index.test.ts"]],
["extensions/google-meet/src/oauth.ts", ["extensions/google-meet/src/oauth.test.ts"]],
[
"extensions/discord/src/monitor/message-handler.ts",
[
"extensions/discord/src/channel-actions.contract.test.ts",
"extensions/discord/src/channel.message-adapter.test.ts",
"extensions/discord/src/channel.test.ts",
"extensions/discord/src/durable-delivery.test.ts",
"extensions/discord/src/monitor/message-handler.bot-self-filter.test.ts",
"extensions/discord/src/monitor/message-handler.queue.test.ts",
"extensions/discord/src/monitor/provider.skill-dedupe.test.ts",
"extensions/discord/src/monitor/provider.test.ts",
],
],
["src/commands/doctor-memory-search.ts", ["src/commands/doctor-memory-search.test.ts"]],
[
"src/agents/test-helpers/live-model-turn-probes.ts",
["src/agents/live-model-turn-probes.test.ts"],
],
[
"src/plugins/provider-auth-choice.ts",
["src/commands/auth-choice.apply.plugin-provider.test.ts", "src/commands/auth-choice.test.ts"],
],
[
"src/secrets/provider-env-vars.ts",
["src/secrets/provider-env-vars.dynamic.test.ts", "src/secrets/provider-env-vars.test.ts"],
],
[
"packages/memory-host-sdk/src/host/embedding-defaults.ts",
["extensions/memory-core/src/memory/embeddings.test.ts"],
],
["src/auto-reply/reply/dispatch-from-config.ts", GROUP_VISIBLE_REPLY_TEST_TARGETS],
["src/auto-reply/reply/source-reply-delivery-mode.ts", GROUP_VISIBLE_REPLY_TEST_TARGETS],
[
"src/auto-reply/reply/effective-reply-route.ts",
["src/auto-reply/reply/effective-reply-route.test.ts", ...GROUP_VISIBLE_REPLY_TEST_TARGETS],
],
["src/auto-reply/reply/get-reply-run.ts", ["src/auto-reply/reply/followup-runner.test.ts"]],
["src/auto-reply/reply/groups.ts", GROUP_VISIBLE_REPLY_TEST_TARGETS],
["src/auto-reply/get-reply-options.types.ts", GROUP_VISIBLE_REPLY_TEST_TARGETS],
["src/agents/system-prompt.ts", GROUP_VISIBLE_REPLY_PROMPT_TEST_TARGETS],
["src/config/types.messages.ts", GROUP_VISIBLE_REPLY_TEST_TARGETS],
["src/config/zod-schema.core.ts", GROUP_VISIBLE_REPLY_TEST_TARGETS],
["src/auto-reply/reply/commands-acp.ts", ["src/auto-reply/reply/commands-acp.test.ts"]],
[
"src/auto-reply/reply/dispatch-acp-command-bypass.ts",
["src/auto-reply/reply/dispatch-acp-command-bypass.test.ts"],
],
]);
const GENERATED_CHANGED_TEST_TARGET_PATTERNS = [
/^extensions\/[^/]+\/src\/host\/.+\/\.bundle\.hash$/u,
/^extensions\/[^/]+\/src\/host\/.+\/[^/]+\.bundle\.js$/u,
];
const SOURCE_ROOTS_FOR_IMPORT_GRAPH = [
"src",
"extensions",
"packages",
"ui/src",
"ui/config",
"ui/public",
"test",
];
const IMPORTABLE_FILE_EXTENSIONS = [".ts", ".tsx", ".mts", ".cts"];
function importGraphPathspecs(roots: string[], suffixes: readonly string[]) {
return [
...roots.flatMap((root) => suffixes.map((suffix) => `:(glob)${root}/**/*${suffix}`)),
...suffixes.map((suffix) => `:(glob)ui/*${suffix}`),
];
}
const IMPORT_GRAPH_GREP_PATHS = importGraphPathspecs(
SOURCE_ROOTS_FOR_IMPORT_GRAPH,
IMPORTABLE_FILE_EXTENSIONS,
);
const TOOLING_IMPORT_GRAPH_ROOTS = [...SOURCE_ROOTS_FOR_IMPORT_GRAPH, "scripts"];
const TOOLING_IMPORTABLE_FILE_EXTENSIONS = [
...IMPORTABLE_FILE_EXTENSIONS,
".js",
".jsx",
".mjs",
".cjs",
];
const TOOLING_IMPORT_GRAPH_GREP_PATHS = importGraphPathspecs(
TOOLING_IMPORT_GRAPH_ROOTS,
TOOLING_IMPORTABLE_FILE_EXTENSIONS,
);
const BROAD_CHANGED_ENV_KEY = "OPENCLAW_TEST_CHANGED_BROAD";
const VITEST_NO_OUTPUT_TIMEOUT_ENV_KEY = "OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS";
const VITEST_NO_OUTPUT_HEARTBEAT_ENV_KEY = "OPENCLAW_VITEST_NO_OUTPUT_HEARTBEAT_MS";
/** Default no-output timeout applied to test-projects Vitest children. */
const DEFAULT_TEST_PROJECTS_VITEST_NO_OUTPUT_TIMEOUT_MS = String(900_000);
/** Default heartbeat interval applied to test-projects Vitest children. */
const DEFAULT_TEST_PROJECTS_VITEST_NO_OUTPUT_HEARTBEAT_MS = String(
DEFAULT_VITEST_NO_OUTPUT_HEARTBEAT_MS,
);
export function formatNoChangedTestTargetLines(skippedBroadFallbackPaths: string[]) {
if (skippedBroadFallbackPaths.length === 0) {
return ["[test] no changed test targets; skipping Vitest."];
}
return [
"[test] no precise changed test targets; skipping Vitest.",
`[test] ${skippedBroadFallbackPaths.length} changed path${
skippedBroadFallbackPaths.length === 1 ? "" : "s"
} require broad Vitest fallback:`,
...skippedBroadFallbackPaths.map((changedPath) => `[test] ${changedPath}`),
"[test] run `OPENCLAW_TEST_CHANGED_BROAD=1 pnpm test:changed` for broad coverage.",
];
}
const EXPLICIT_SOURCE_FULL_IMPORT_GRAPH_THRESHOLD = 12;
function resolveTestProjectsVitestNoOutputTimeoutMs(config: string) {
const directRunnerTimeoutMs = resolveDefaultVitestNoOutputTimeoutMs(["run", "--config", config]);
return String(
Math.max(Number(DEFAULT_TEST_PROJECTS_VITEST_NO_OUTPUT_TIMEOUT_MS), directRunnerTimeoutMs),
);
}
const VITEST_CONFIG_TARGET_KIND_BY_PATH = new Map<string, string>(
Object.entries(VITEST_CONFIG_BY_KIND).map(([kind, config]) => [config, kind]),
);
const RUNNABLE_VITEST_CONFIG_TARGETS = new Set([
"ui/vitest.config.ts",
"vitest.config.ts",
DEFAULT_VITEST_CONFIG,
...Object.values(VITEST_CONFIG_BY_KIND),
...fullSuiteVitestShards.flatMap((shard) => [shard.config, ...shard.projects]),
]);
export const CHANNEL_CONTRACT_CONFIG_PATTERNS = new Map<string, readonly string[]>([
[CONTRACTS_CHANNEL_SURFACE_VITEST_CONFIG, channelSurfaceContractPatterns],
[CONTRACTS_CHANNEL_CONFIG_VITEST_CONFIG, channelConfigContractPatterns],
[CONTRACTS_CHANNEL_REGISTRY_VITEST_CONFIG, channelRegistryContractPatterns],
[CONTRACTS_CHANNEL_SESSION_VITEST_CONFIG, channelSessionContractPatterns],
]);
function normalizePathPattern(value: string) {
return value.replaceAll("\\", "/");
}
function listRepoFilesRecursive(root: string, cwd: string): string[] {
const entries = fs.readdirSync(root, { withFileTypes: true });
return entries.flatMap((entry) => {
const absolute = path.join(root, entry.name);
if (entry.isDirectory()) {
return listRepoFilesRecursive(absolute, cwd);
}
if (!entry.isFile()) {
return [];
}
return [normalizePathPattern(path.relative(cwd, absolute))];
});
}
let cachedBroadScriptTestTargets: string[] = [];
let cachedBroadScriptTestTargetsCwd: string | null = null;
function listBroadScriptTestTargets(pattern: string, cwd: string) {
const root = path.join(cwd, "test/scripts");
if (cachedBroadScriptTestTargetsCwd !== cwd) {
// Broad-target expansion can ask for the same process-stable checkout twice.
// Keep one inventory so planning does not repeat the directory walk.
cachedBroadScriptTestTargets = fs.existsSync(root)
? listRepoFilesRecursive(root, cwd)
.filter((file) => file.endsWith(".test.ts"))
.toSorted((left, right) => left.localeCompare(right))
: [];
cachedBroadScriptTestTargetsCwd = cwd;
}
return cachedBroadScriptTestTargets.filter((file) => path.matchesGlob(file, pattern));
}
function listBroadToolingScriptTestTargets(pattern: string, cwd: string) {
return listBroadScriptTestTargets(pattern, cwd).filter(
(file) => classifyTarget(file, cwd) === "tooling",
);
}
let cachedToolingFullSuiteTestTargets: string[] | null = null;
let cachedToolingFullSuiteTestTargetsCwd: string | null = null;
function listToolingFullSuiteTestTargets(cwd: string) {
if (cachedToolingFullSuiteTestTargets && cachedToolingFullSuiteTestTargetsCwd === cwd) {
return cachedToolingFullSuiteTestTargets;
}
// The CLI plans against one process-stable checkout. Reuse its inventory when
// callers compare full-suite modes instead of walking the tree for every mode.
cachedToolingFullSuiteTestTargets = uniqueOrdered(
[path.join(cwd, "test"), path.join(cwd, "src", "scripts")].flatMap((root) =>
fs.existsSync(root) ? listRepoFilesRecursive(root, cwd) : [],
),
)
// Match Vitest's fixture/live exclusions before forming explicit leaf chunks.
.filter(
(file) =>
file.endsWith(".test.ts") &&
!file.startsWith("test/fixtures/") &&
!file.endsWith(".live.test.ts") &&
classifyTarget(file, cwd) === "tooling",
)
.toSorted((left, right) => left.localeCompare(right));
cachedToolingFullSuiteTestTargetsCwd = cwd;
return cachedToolingFullSuiteTestTargets;
}
function listUnitFastFullSuiteTestTargets() {
const timerTargets = new Set(getUnitFastTimerTestFiles());
const isolatedTargets = new Set(getUnitFastIsolatedTestFiles());
return getUnitFastTestFiles().filter(
(file) => !timerTargets.has(file) && !isolatedTargets.has(file),
);
}
let cachedUnitSrcFullSuiteTestTargets: string[] | null = null;
let cachedUnitSrcFullSuiteTestTargetsCwd: string | null = null;
function listUnitSrcFullSuiteTestTargets(cwd: string) {
if (cachedUnitSrcFullSuiteTestTargets && cachedUnitSrcFullSuiteTestTargetsCwd === cwd) {
return cachedUnitSrcFullSuiteTestTargets;
}
const unitFastTargets = new Set(getUnitFastTestFiles());
const srcDir = path.join(cwd, "src");
cachedUnitSrcFullSuiteTestTargets = filterUnitConfigTestFiles(
(fs.existsSync(srcDir) ? listRepoFilesRecursive(srcDir, cwd) : []).filter((file) =>
file.endsWith(".test.ts"),
),
)
.filter(
(file) =>
!unitFastTargets.has(file) &&
!path.matchesGlob(file, "src/acp/**") &&
!path.matchesGlob(file, "src/security/**"),
)
.toSorted((left, right) => left.localeCompare(right));
cachedUnitSrcFullSuiteTestTargetsCwd = cwd;
return cachedUnitSrcFullSuiteTestTargets;
}
function listAgentsCoreFullSuiteTestTargets(cwd: string) {
const isolatedTests = new Set([
...agentVitestProjectOwners.spawnProductionBoundary.include,
...agentVitestProjectOwners.coreIsolated.include,
]);
const agentsDir = path.join(cwd, "src/agents");
if (!fs.existsSync(agentsDir)) {
return [];
}
return fs
.readdirSync(agentsDir, { withFileTypes: true })
.filter((entry) => entry.isFile() && entry.name.endsWith(".test.ts"))
.map((entry) => `src/agents/${entry.name}`)
.filter((file) => !isolatedTests.has(file))
.toSorted((left, right) => left.localeCompare(right));
}
function listInfraFullSuiteTestTargets(cwd: string) {
const infraDir = path.join(cwd, "src/infra");
return uniqueOrdered([
...(fs.existsSync(infraDir) ? listRepoFilesRecursive(infraDir, cwd) : []),
...databaseWorkerCoreTestFiles.filter((file) => fs.existsSync(path.join(cwd, file))),
])
.filter(
(file) =>
file.endsWith(".test.ts") &&
!isSharedVitestExcludedPath(file) &&
classifyTarget(file, cwd) === "infra",
)
.toSorted((left, right) => left.localeCompare(right));
}
function createBroadToolingScriptPlans(params: VitestRunPlan & { cwd: string }) {
const { config, forwardedArgs, includePatterns, watchMode, cwd } = params;
if (watchMode || config !== TOOLING_VITEST_CONFIG || !includePatterns) {
return null;
}
const pattern = includePatterns[0];
const targets =
pattern !== undefined &&
includePatterns.length === 1 &&
BROAD_TOOLING_SCRIPT_TEST_PATTERNS.has(pattern)
? listBroadToolingScriptTestTargets(pattern, cwd)
: includePatterns.every((target) => target.startsWith("test/scripts/"))
? includePatterns
: [];
if (targets.length <= BROAD_TOOLING_SCRIPT_TEST_TARGET_CHUNK_SIZE) {
return null;
}
const chunkCount = Math.ceil(targets.length / BROAD_TOOLING_SCRIPT_TEST_TARGET_CHUNK_SIZE);
const chunks = splitTargetChunks(targets, chunkCount);
return chunks.length > 0
? chunks.map((chunk) => ({
config,
forwardedArgs,
includePatterns: chunk,
watchMode,
}))
: null;
}
function ownsIncludeSelection(
includePatterns: string[] | null,
ownedTargets?: ReadonlySet<string>,
) {
return (
includePatterns !== null &&
includePatterns.length > 0 &&
includePatterns.every((pattern) => ownedTargets?.has(pattern))
);
}
function resolveInheritedIncludeScope(
includePatterns: string[],
inheritedPatterns: string[],
ownedTargets?: ReadonlySet<string>,
) {
const owned = includePatterns.filter((pattern) => ownedTargets?.has(pattern));
if (owned.length === includePatterns.length) {
return includePatterns;
}
return (
intersectIncludePatterns(
uniqueOrdered([...inheritedPatterns, ...owned]),
includePatterns,
path.matchesGlob,
) ?? includePatterns
);
}
function createBoundedExtensionPlans(
plan: VitestRunPlan,
cwd: string,
env?: NodeJS.ProcessEnv,
ownedTargets?: ReadonlySet<string>,
) {
const { config, forwardedArgs, watchMode } = plan;
const roots = EXTENSION_TEST_PROCESS_ROOTS.get(config);
if (watchMode || !roots) {
return [plan];
}
// Broad CI selections keep their inherited scope and process limits.
// Explicit owned selections do not depend on borrowed include metadata.
const includeFilePath = ownsIncludeSelection(plan.includePatterns, ownedTargets)
? undefined
: env?.[INCLUDE_FILE_ENV_KEY]?.trim();
if (includeFilePath) {
if (!fs.existsSync(includeFilePath)) {
return [{ ...plan, includePatterns: null }];
}
const inheritedTargets = loadIncludePatternsForSpecFilter(env ?? {}) ?? [];
const scopedTargets = plan.includePatterns
? resolveInheritedIncludeScope(plan.includePatterns, inheritedTargets, ownedTargets)
: inheritedTargets;
if (scopedTargets.length === 0) {
return [];
}
const chunks = splitExtensionTestProcessTargets(config, scopedTargets);
if (chunks.length === 0) {
return [];
}
if (chunks.length <= 1) {
return [
{
...plan,
includePatterns:
!plan.includePatterns?.some((target) => ownedTargets?.has(target)) &&
scopedTargets.length === inheritedTargets.length &&
scopedTargets.every((target, index) => target === inheritedTargets[index])
? null
: scopedTargets,
},
];
}
return chunks.map((includePatterns) => ({
config,
forwardedArgs,
includePatterns,
watchMode,
}));
}
const chunks = createExtensionTestProcessTargetChunks(
config,
config === EXTENSION_DATABASE_WORKERS_VITEST_CONFIG && plan.includePatterns
? plan.includePatterns
: roots,
forwardedArgs,
cwd,
);
if (chunks.length === 0) {
// Preserve exact requests for Vitest's existing empty-test diagnostic, never a broad fallback.
return ownsIncludeSelection(plan.includePatterns, ownedTargets) ? [plan] : [];
}
if (chunks.length <= 1) {
return [plan];
}
return chunks.map((includePatterns) => ({
config,
forwardedArgs,
includePatterns,
watchMode,
}));
}
function expandBroadToolingScriptTargets(targetArgs: string[], cwd: string, watchMode: boolean) {
if (watchMode) {
return targetArgs;
}
return uniqueOrdered(
targetArgs.flatMap((targetArg) => {
const pattern = toScopedIncludePattern(targetArg, cwd);
if (!BROAD_TOOLING_SCRIPT_TEST_PATTERNS.has(pattern)) {
return [targetArg];
}
const targets = listBroadScriptTestTargets(pattern, cwd);
return targets.length > 0 ? targets : [targetArg];
}),
);
}
function isExistingPathTarget(arg: string, cwd: string) {
return fs.existsSync(path.resolve(cwd, arg));
}
function isExistingFileTarget(arg: string, cwd: string) {
try {
return fs.statSync(path.resolve(cwd, arg)).isFile();
} catch {
return false;
}
}
function isExistingDirectoryTarget(arg: string, cwd: string) {
try {
return fs.statSync(path.resolve(cwd, arg)).isDirectory();
} catch {
return false;
}
}
function isGlobTarget(arg: string) {
return /[*?[\]{}]|[@+!]\(/u.test(arg);
}
function isLikelyFileTarget(arg: string) {
return /(?:^|\/)[^/]+\.[A-Za-z0-9]+$/u.test(arg);
}
function isPathLikeTargetArg(arg: string, cwd: string) {
if (!arg || arg === "--" || arg.startsWith("-")) {
return false;
}
const relative = toRepoRelativeTarget(arg, cwd);
return (
isGlobTarget(arg) ||
isTestFileTarget(arg) ||
isVitestConfigPathLikeTarget(relative) ||
isExistingPathTarget(arg, cwd) ||
(path.posix.extname(relative) === "" &&
/^(?:src|test|extensions|ui|packages|apps)\//u.test(relative)) ||
Boolean(resolveExplicitTestPrefixTargets(arg, cwd)?.length)
);
}
function toRepoRelativeTarget(arg: string, cwd: string) {
if (isGlobTarget(arg)) {
return normalizePathPattern(arg.replace(/^\.\//u, ""));
}
const absolute = path.resolve(cwd, arg);
return normalizePathPattern(path.relative(cwd, absolute));
}
function explicitIncludeTargets(targetArgs: string[], cwd: string) {
return new Set(
targetArgs
.map((target) => toRepoRelativeTarget(target, cwd))
.filter((target) => isTestFileTarget(target) && !isGlobTarget(target)),
);
}
function toScopedIncludePattern(arg: string, cwd: string) {
const relative = toRepoRelativeTarget(arg, cwd);
if (isGlobTarget(relative) || isTestFileTarget(relative)) {
return relative;
}
if (isExistingFileTarget(arg, cwd) || isLikelyFileTarget(relative)) {
const directory = normalizePathPattern(path.posix.dirname(relative));
return directory === "." ? "**/*.test.ts" : `${directory}/**/*.test.ts`;
}
return `${relative.replace(/\/+$/u, "")}/**/*.test.ts`;
}
const EXPLICIT_TEST_TARGET_ROOTS = ["src", "test", "extensions", "ui", "packages", "apps"];
let cachedExplicitTestTargetFiles: string[] | null = null;
let cachedExplicitTestTargetFilesCwd: string | null = null;
function listExplicitTestTargetFilesFromGit(cwd: string) {
const result = spawnSync(
"git",
[
"ls-files",
"-z",
"--cached",
"--others",
"--exclude-standard",
"--",
...EXPLICIT_TEST_TARGET_ROOTS,
],
{
cwd,
encoding: "utf8",
maxBuffer: GIT_LS_FILES_MAX_BUFFER_BYTES,
stdio: ["ignore", "pipe", "pipe"],
},
);
if (result.status !== 0) {
return null;
}
return result.stdout.split("\0").filter((line) => line.length > 0 && isImportableGraphFile(line));
}
function listExplicitTestTargetFilesForCwd(cwd: string) {
if (cachedExplicitTestTargetFiles && cachedExplicitTestTargetFilesCwd === cwd) {
return cachedExplicitTestTargetFiles;
}
cachedExplicitTestTargetFiles =
listExplicitTestTargetFilesFromGit(cwd) ??
EXPLICIT_TEST_TARGET_ROOTS.flatMap((root) => listImportGraphFiles(cwd, root));
cachedExplicitTestTargetFilesCwd = cwd;
return cachedExplicitTestTargetFiles;
}
function resolveExplicitTestPrefixTargets(targetArg: string, cwd: string) {
if (isExistingPathTarget(targetArg, cwd) || isGlobTarget(targetArg)) {
return null;
}
const relative = toRepoRelativeTarget(targetArg, cwd).replace(/\/+$/u, "");
if (!relative || isLikelyFileTarget(relative)) {
return null;
}
const directory = path.posix.dirname(relative);
const prefix = `${relative}.`;
// Bound filesystem probes to matching tests, even with a cached repo-wide inventory.
const targets = listExplicitTestTargetFilesForCwd(cwd).filter(
(file) =>
path.posix.dirname(file) === directory &&
file.startsWith(prefix) &&
isTestFileTarget(file) &&
fs.existsSync(path.join(cwd, file)),
);
return targets.length > 0 ? targets.toSorted((left, right) => left.localeCompare(right)) : null;
}
function isNormalizedLiteralPath(value: string) {
return /^[\w.-]+(?:\/[\w.-]+)*$/u.test(value) && !/(?:^|\/)\.{1,2}(?:\/|$)/u.test(value);
}
function includePatternMatchesAnyFile(pattern: string, files: string[]) {
const literalPattern = isNormalizedLiteralPath(pattern);
return files.some(
(file) =>
file === pattern ||
// Keep Node's separator, dot-segment, and platform handling for other paths.
((!literalPattern || !isNormalizedLiteralPath(file)) && path.matchesGlob(file, pattern)),
);
}
function resolveExplicitSourceTestTargets(
targetArg: string,
cwd: string,
options: Pick<ChangedTestTargetOptions, "forceFullImportGraph" | "watchMode"> = {},
) {
const relative = toRepoRelativeTarget(targetArg, cwd);
const kind = classifyTarget(targetArg, cwd);
if (shouldUseWholeConfigTarget(kind, targetArg, cwd)) {
return null;
}
if (!isExistingFileTarget(targetArg, cwd)) {
return null;
}
if (isTestFileTarget(relative)) {
return null;
}
const preciseTargets = [
...(resolvePreciseChangedTestTargets(relative, {
cwd,
forceFullImportGraph: options.forceFullImportGraph === true,
}) ?? []),
...(options.watchMode ? [] : resolveKovaSchemaTestTargets(relative)),
];
if (preciseTargets.length > 0) {
return [...new Set(preciseTargets)].toSorted((left, right) => left.localeCompare(right));
}
if (!isTestSupportFileTarget(relative)) {
return null;
}
return [
...new Set(
resolveAffectedTestsFromImportGraph(relative, cwd, {
forceFull: options.forceFullImportGraph === true,
}),
),
].toSorted((left, right) => left.localeCompare(right));
}
function listDirectoryTestTargets(directory: string, cwd: string): string[] {
if (isSharedVitestExcludedPath(directory) || isSharedVitestExcludedPath(`${directory}/`)) {
return [];
}
return fs.readdirSync(path.join(cwd, directory), { withFileTypes: true }).flatMap((entry) => {
const relative = path.posix.join(directory, entry.name);
if (entry.isDirectory()) {
return listDirectoryTestTargets(relative, cwd);
}
return entry.isFile() && relative.endsWith(".test.ts") && !isSharedVitestExcludedPath(relative)
? [relative]
: [];
});
}
function isOrdinaryAgentGlobTarget(relative: string): boolean {
return (
isPathAtOrUnder(relative, agentVitestProjectOwners.all.root) &&
isGlobTarget(relative) &&
!/\.(?:live|e2e)\.test\.ts$/u.test(relative)
);
}
function expandExplicitSourceTestTargets(targetArgs: string[], cwd: string, watchMode: boolean) {
const sourceTargetCount = targetArgs.filter((targetArg) => {
const relative = toRepoRelativeTarget(targetArg, cwd);
return isExistingFileTarget(targetArg, cwd) && !isTestFileTarget(relative);
}).length;
const forceFullImportGraph = sourceTargetCount > EXPLICIT_SOURCE_FULL_IMPORT_GRAPH_THRESHOLD;
return targetArgs.flatMap((targetArg) => {
const relative = toRepoRelativeTarget(targetArg, cwd);
if (classifyTarget(targetArg, cwd) === "extensionFull") {
// The full aggregate already includes the dedicated database-worker project.
return [targetArg];
}
const glob = isGlobTarget(relative);
const directory = isExistingDirectoryTarget(targetArg, cwd);
if (
!watchMode &&
!glob &&
directory &&
(isPathAtOrUnder(relative, "packages") ||
isPathAtOrUnder(relative, agentVitestProjectOwners.all.root))
) {
const targets = listDirectoryTestTargets(relative, cwd).toSorted((left, right) =>
left.localeCompare(right),
);
return targets.length > 0 ? targets : [targetArg];
}
if (!watchMode && isOrdinaryAgentGlobTarget(relative)) {
// Assign every leaf before choosing configs: agent shards exclude light,
// isolated, and database-worker tests owned by other projects.
const targets = expandVitestIncludePatterns([relative], cwd).filter(
(file) =>
file.endsWith(".test.ts") &&
!isSharedVitestExcludedPath(file) &&
isExistingFileTarget(file, cwd),
);
return targets.length > 0 ? targets : [targetArg];
}
// Target shape is invariant across the worker inventory; literal files need no expansion.
const databaseWorkerTargets =
glob || directory
? databaseWorkerExtensionTestFiles.filter((file) =>
glob ? path.matchesGlob(file, relative) : isPathAtOrUnder(file, relative),
)
: [];
if (databaseWorkerTargets.length > 0) {
return [...databaseWorkerTargets, targetArg];
}
if ((isPathAtOrUnder(relative, "ui") || isPluginControlUiPath(relative)) && glob) {
// Expand mixed browser globs before assigning files to their disjoint runners.
const targets = listExplicitTestTargetFilesForCwd(cwd).filter(
(file) => isTestFileTarget(file) && path.matchesGlob(file, relative),
);
return targets.length > 0 ? targets : [targetArg];
}
const prefixTargets = resolveExplicitTestPrefixTargets(targetArg, cwd);
if (prefixTargets) {
return prefixTargets;
}
if (relative === "src/commands" && directory) {
return [COMMANDS_LIGHT_VITEST_CONFIG, COMMANDS_VITEST_CONFIG];
}
// Contract directory targets must fan out to the owning contract lanes; the
// generic channels/plugins projects exclude contracts/**, so routing a
// contracts directory there silently runs zero tests (passWithNoTests).
if (directory) {
if (isPathAtOrUnder(relative, "src/channels/plugins/contracts")) {
return [
CONTRACTS_CHANNEL_SURFACE_VITEST_CONFIG,
CONTRACTS_CHANNEL_CONFIG_VITEST_CONFIG,
CONTRACTS_CHANNEL_REGISTRY_VITEST_CONFIG,
CONTRACTS_CHANNEL_SESSION_VITEST_CONFIG,
];
}
if (isPathAtOrUnder(relative, "src/plugins/contracts")) {
return [CONTRACTS_PLUGIN_VITEST_CONFIG];
}
}
const exactDirectoryTargets = resolveExactSourceDirectoryTestTargets(targetArg, cwd);
if (exactDirectoryTargets) {
return exactDirectoryTargets;
}
const targets = resolveExplicitSourceTestTargets(targetArg, cwd, {
forceFullImportGraph,
watchMode,
});
return targets && targets.length > 0 ? targets : [targetArg];
});
}
const exactSourceDirectoryRoots = [
"src/acp",
"src/agents",
"src/auto-reply",
"src/channels",
"src/cli",
"src/commands",
"src/config",
"src/cron",
"src/daemon",
"src/gateway",
"src/hooks",
"src/infra",
"src/logging",
"src/media",
"src/media-understanding",
"src/plugin-sdk",
"src/plugins",
"src/process",
"src/secrets",
"src/shared",
"src/tui",
"src/utils",
"src/wizard",
"ui/src",
];
function isExactSourceDirectoryTarget(relative: string) {
return exactSourceDirectoryRoots.some((root) => isPathAtOrUnder(relative, root));
}
function resolveExactSourceDirectoryTestTargets(targetArg: string, cwd: string) {
if (!isExistingDirectoryTarget(targetArg, cwd)) {
return null;
}
const relative = toRepoRelativeTarget(targetArg, cwd).replace(/\/+$/u, "");
if (!isExactSourceDirectoryTarget(relative)) {
return null;
}
if (isCanonicalAgentOwnerDirectoryTarget(targetArg, cwd)) {
return [targetArg];
}
const prefix = `${relative}/`;
const lightTargets = uniqueOrdered([
...getUnitFastTestFiles(),
...pluginSdkLightTestFiles,
...commandsLightTestFiles,
]).filter((file) => file.startsWith(prefix));
return lightTargets.length > 0 ? [...lightTargets, targetArg] : null;
}
function isCanonicalAgentOwnerDirectoryTarget(targetArg: string, cwd: string) {
if (!isExistingDirectoryTarget(targetArg, cwd)) {
return false;
}
const kind = classifyTarget(targetArg, cwd);
if (kind === agentVitestProjectOwners.all.kind) {
return false;
}
const relative = toRepoRelativeTarget(targetArg, cwd).replace(/\/+$/u, "");
return Object.values(agentVitestProjectOwners).some(
(owner) => owner.kind === kind && isPathAtOrUnder(relative, owner.root),
);
}
/**
* Finds explicit test path targets that do not match any known project plan.
*/
export function findUnmatchedExplicitTestTargets(args: string[], cwd = process.cwd()) {
const { targetArgs, watchMode } = parseTestProjectsArgs(args, cwd);
if (targetArgs.length === 0) {
return [];
}
let candidateFiles: string[] | null = null;
const getCandidateFiles = () => {
candidateFiles ??= listExplicitTestTargetFilesForCwd(cwd);
return candidateFiles;
};
const unmatched: UnmatchedExplicitTestTarget[] = [];
for (const targetArg of targetArgs) {
const relative = toRepoRelativeTarget(targetArg, cwd);
const absolute = path.resolve(cwd, targetArg);
// Existing exact tests need no inventory scan; lane assignment belongs to the run planner.
if (!isGlobTarget(relative) && isTestFileTarget(relative) && fs.existsSync(absolute)) {
continue;
}
if (
resolveVitestConfigTargetKind(relative) ||
(isVitestConfigFileTarget(relative) && isExistingFileTarget(targetArg, cwd))
) {
continue;
}
const kind = classifyTarget(targetArg, cwd);
if (shouldUseWholeConfigTarget(kind, targetArg, cwd)) {
continue;
}
if (isGlobTarget(relative)) {
if (!includePatternMatchesAnyFile(relative, getCandidateFiles())) {
unmatched.push({
target: targetArg,
reason: "glob-matched-no-files",
});
}
continue;
}
if (!fs.existsSync(absolute)) {
if (resolveExplicitTestPrefixTargets(targetArg, cwd)) {
continue;
}
unmatched.push({
target: targetArg,
reason: "path-does-not-exist",
...(path.posix.extname(relative) === ""
? { includePattern: `${relative}{,.*}.{test,spec}.{js,jsx,ts,tsx,mjs,cjs,mts,cts}` }
: {}),
});
continue;
}
const explicitSupportTargets = resolveExplicitSourceTestTargets(targetArg, cwd, { watchMode });
if (explicitSupportTargets) {
if (explicitSupportTargets.length === 0) {
unmatched.push({
target: targetArg,
reason: "target-matched-no-test-files",
});
}
continue;
}
const includePattern = toScopedIncludePattern(targetArg, cwd);
if (!includePatternMatchesAnyFile(includePattern, getCandidateFiles())) {
unmatched.push({
target: targetArg,
reason: "target-matched-no-test-files",
includePattern,
});
}
}
return unmatched;
}
function isSkippedImportGraphDirectory(name: string) {
return name === ".git" || name === "dist" || name === "node_modules" || name === "vendor";
}
function listImportGraphFiles(
cwd: string,
directory: string,
files: string[] = [],
extensions: readonly string[] = IMPORTABLE_FILE_EXTENSIONS,
recursive = true,
) {
let entries;
try {
entries = fs.readdirSync(path.join(cwd, directory), { withFileTypes: true });
} catch {
return files;
}
for (const entry of entries) {
const relative = normalizePathPattern(path.posix.join(directory, entry.name));
if (entry.isDirectory()) {
if (recursive && !isSkippedImportGraphDirectory(entry.name)) {
listImportGraphFiles(cwd, relative, files, extensions);
}
continue;
}
if (entry.isFile() && extensions.some((ext) => relative.endsWith(ext))) {
files.push(relative);
}
}
return files;
}
function resolveImportSpecifiers(
importer: string,
specifier: string,
fileSet: ReadonlySet<string>,
extensions: readonly string[] = IMPORTABLE_FILE_EXTENSIONS,
aliases: readonly ImportGraphAlias[] = [],
aliasResolutions?: Map<string, string[]>,
runtimeOnly = false,
): string[] {
if (!specifier.startsWith(".")) {
if (aliasResolutions?.has(specifier)) {
return aliasResolutions.get(specifier) ?? [];
}
const resolved = new Set<string>();
for (const { pattern, targets } of aliases) {
const star = pattern.indexOf("*");
const wildcard =
star >= 0 &&
specifier.startsWith(pattern.slice(0, star)) &&
specifier.endsWith(pattern.slice(star + 1))
? specifier.slice(star, specifier.length - (pattern.length - star - 1))
: null;
if (specifier !== pattern && wildcard === null) {
continue;
}
for (const target of targets) {
for (const file of resolveImportSpecifiers(
"package.json",
`./${target.replace("*", wildcard ?? "")}`,
fileSet,
extensions,
[],
undefined,
runtimeOnly,
)) {
resolved.add(file);
}
}
}
const result = [...resolved];
aliasResolutions?.set(specifier, result);
return result;
}
const importerDir = path.posix.dirname(importer);
const base = normalizePathPattern(path.posix.normalize(path.posix.join(importerDir, specifier)));
const candidates = [];
const ext = path.posix.extname(base);
if (ext) {
candidates.push(base);
if ([".js", ".jsx", ".mjs", ".cjs"].includes(ext)) {
const withoutExt = base.slice(0, -ext.length);
candidates.push(...extensions.map((candidateExt) => `${withoutExt}${candidateExt}`));
}
} else {
candidates.push(
...extensions.map((candidateExt) => `${base}${candidateExt}`),
...extensions.map((candidateExt) => `${base}/index${candidateExt}`),
);
}
// A .js runtime sibling must not hide the TypeScript source selected by
// extension substitution. Combined graphs retain both kinds of consumers.
const resolved = [...new Set(candidates.filter((candidate) => fileSet.has(candidate)))];
return runtimeOnly || ![".js", ".jsx", ".mjs", ".cjs"].includes(ext)
? resolved.slice(0, 1)
: resolved;
}
const cachedImportGraphs = new Map<string, { graph: ImportGraph; additionalPaths: string }>();
const cachedImportGraphFiles = new Map<string, string[]>();
const cachedImportGraphGrepMatches = new Map<string, ImportGraphEdges[] | null>();
const cachedImportGraphEdges = new Map<string, ImportGraphEdges>();
const cachedImportGraphAliases = new Map<string, ImportGraphAlias[]>();
function readImportGraphManifest(cwd: string, file: string): Record<string, unknown> {
if (!fs.existsSync(path.join(cwd, file))) {
return {};
}
const value: unknown = JSON.parse(fs.readFileSync(path.join(cwd, file), "utf8"));
if (!value || typeof value !== "object" || Array.isArray(value)) {
throw new Error(`Invalid test selector manifest: ${file}`);
}
return Object.fromEntries(Object.entries(value));
}
/** Read the maintained source mappings without loading a Vitest config or installed package. */
function getImportGraphAliases(cwd: string): ImportGraphAlias[] {
const cached = cachedImportGraphAliases.get(cwd);
if (cached) {
return cached;
}
const aliases: ImportGraphAlias[] = [];
const files = listTrackedTestPlanFiles(cwd, [
"package.json",
":(glob)packages/*/package.json",
":(glob)extensions/*/package.json",
]) ?? [
"package.json",
...["packages", "extensions"].flatMap((root) =>
fs.existsSync(path.join(cwd, root))
? fs.readdirSync(path.join(cwd, root)).map((entry) => `${root}/${entry}/package.json`)
: [],
),
];
const exportTargets = (value: unknown): string[] => {
if (typeof value === "string") {
return [value];
}
return value && typeof value === "object" ? Object.values(value).flatMap(exportTargets) : [];
};
for (const file of files) {
const manifest = readImportGraphManifest(cwd, file);
if (typeof manifest.name !== "string" || !manifest.exports) {
continue;
}
const exports = manifest.exports;
const entries =
typeof exports === "object" &&
!Array.isArray(exports) &&
Object.keys(exports).some((key) => key.startsWith("."))
? Object.entries(exports)
: [[".", exports]];
for (const [subpath, value] of entries) {
if (typeof subpath !== "string" || !subpath.startsWith(".")) {
continue;
}
const targets = exportTargets(value)
.filter(
(target) =>
target.startsWith("./") && !target.includes("/dist/") && !/\.d\.[cm]?ts$/u.test(target),
)
.map((target) => path.posix.join(path.posix.dirname(file), target));
if (targets.length > 0) {
aliases.push({
pattern: `${manifest.name}${subpath === "." ? "" : subpath.slice(1)}`,
targets,
});
}
}
}
const config = readImportGraphManifest(cwd, "tsconfig.json");
const compiler = config.compilerOptions;
if (compiler && typeof compiler === "object" && "paths" in compiler) {
const paths = compiler.paths;
if (paths && typeof paths === "object") {
for (const [pattern, targets] of Object.entries(paths)) {
if (Array.isArray(targets) && targets.every((target) => typeof target === "string")) {
aliases.push({ pattern, targets });
}
}
}
}
// Keep deterministic source mappings; conditional source exports retain every branch.
aliases.sort((left, right) => {
const leftStar = left.pattern.indexOf("*");
const rightStar = right.pattern.indexOf("*");
return (
Number(leftStar >= 0) - Number(rightStar >= 0) ||
(leftStar >= 0 && rightStar >= 0 ? rightStar - leftStar : 0)
);
});
cachedImportGraphAliases.set(cwd, aliases);
return aliases;
}
function isImportableGraphFile(relative: string) {
return IMPORTABLE_FILE_EXTENSIONS.some((ext) => relative.endsWith(ext));
}
function listImportGraphFilesForCwd(cwd: string, options: ImportGraphOptions = {}) {
const tooling = options.tooling === true;
const cacheKey = `${cwd}\0${tooling ? "tooling" : "source"}`;
if (cachedImportGraphFiles.has(cacheKey)) {
return cachedImportGraphFiles.get(cacheKey) ?? [];
}
const roots = tooling ? TOOLING_IMPORT_GRAPH_ROOTS : SOURCE_ROOTS_FOR_IMPORT_GRAPH;
const extensions = tooling ? TOOLING_IMPORTABLE_FILE_EXTENSIONS : IMPORTABLE_FILE_EXTENSIONS;
const files = listTrackedTestPlanFiles(
cwd,
tooling ? TOOLING_IMPORT_GRAPH_GREP_PATHS : IMPORT_GRAPH_GREP_PATHS,
) ?? [
...roots.flatMap((root) => listImportGraphFiles(cwd, root, [], extensions)),
// Root configs are inputs; UI caches and generated sibling trees are not.
...listImportGraphFiles(cwd, "ui", [], extensions, false),
];
cachedImportGraphFiles.set(cacheKey, files);
return files;
}
function stripImportableGraphExtension(
relative: string,
extensions: readonly string[] = IMPORTABLE_FILE_EXTENSIONS,
) {
for (const ext of extensions) {
if (relative.endsWith(ext)) {
return relative.slice(0, -ext.length);
}
}
return relative;
}
function resolveImportGraphSearchTerms(
relative: string,
extensions: readonly string[] = IMPORTABLE_FILE_EXTENSIONS,
) {
const withoutExtension = stripImportableGraphExtension(relative, extensions);
const basename = path.posix.basename(withoutExtension);
if (basename === "index" || basename.length < 3) {
return [];
}
const terms = [];
const segments = withoutExtension.split("/");
if (segments.length > 1) {
terms.push(segments.slice(-2).join("/"), withoutExtension);
}
if (relative.startsWith("test/helpers/")) {
return [...new Set(terms)];
}
terms.push(basename);
return [...new Set(terms)];
}
function resolveImportGraphAliasSearchTerms(
file: string,
aliases: ImportGraphAlias[],
fileSet: ReadonlySet<string>,
extensions: readonly string[],
) {
const withoutExtension = stripImportableGraphExtension(file, extensions);
const identities = [
file,
withoutExtension,
...extensions.map((extension) => `${withoutExtension}${extension}`),
...(path.posix.basename(withoutExtension) === "index"
? [path.posix.dirname(withoutExtension)]
: []),
];
const terms = new Set<string>();
for (const { pattern, targets } of aliases) {
for (const target of targets) {
const normalized = path.posix.normalize(target);
const star = normalized.indexOf("*");
if (star < 0) {
if (
!resolveImportSpecifiers("package.json", `./${target}`, fileSet, extensions).includes(
file,
)
) {
continue;
}
const term = pattern.split("*", 1)[0];
if (!term) {
return null;
}
terms.add(term);
continue;
}
for (const identity of identities) {
const suffix = normalized.slice(star + 1);
if (!identity.startsWith(normalized.slice(0, star)) || !identity.endsWith(suffix)) {
continue;
}
const wildcard = identity.slice(star, identity.length - suffix.length);
const term = stripImportableGraphExtension(pattern.replace("*", wildcard), extensions);
if (!term) {
return null;
}
terms.add(term);
}
}
}
return [...terms];
}
function readImportGraphEdges(
cwd: string,
files: string[],
fileSet: ReadonlySet<string>,
tooling = false,
terms: string[] = [],
) {
const cacheKey = (file: string) => `${cwd}\0${tooling}\0${file}`;
const extensions = tooling ? TOOLING_IMPORTABLE_FILE_EXTENSIONS : IMPORTABLE_FILE_EXTENSIONS;
const resolve = (file: string, specifiers: string[]) =>
new Set(
specifiers.flatMap((specifier) =>
resolveImportSpecifiers(file, specifier, fileSet, extensions),
),
);
// Source facts survive a scope change; relative edges still use this inventory.
for (const file of files) {
if (cachedImportGraphEdges.has(cacheKey(file))) {
continue;
}
const previous = cachedImportGraphEdges.get(`${cwd}\0${!tooling}\0${file}`);
if (previous) {
cachedImportGraphEdges.set(cacheKey(file), {
...previous,
imports: resolve(file, previous.specifiers),
references: new Set(previous.references),
});
}
}
const requests = files
.map((file) => ({ file, parseImports: !cachedImportGraphEdges.has(cacheKey(file)) }))
.filter(({ parseImports }) => parseImports || terms.length > 0);
return readTestSelectorSourceFacts(cwd, requests, terms, GIT_LS_FILES_MAX_BUFFER_BYTES, {
matchingOnly: terms.length > 0,
}).map(({ file, imports, typeOnlyImports, matches, references }) => {
const edges = cachedImportGraphEdges.get(cacheKey(file)) ?? {
file,
specifiers: imports,
typeOnlySpecifiers: new Set(typeOnlyImports),
imports: resolve(file, imports),
references: new Set<string>(),
};
for (const reference of references) {
edges.references.add(reference);
}
cachedImportGraphEdges.set(cacheKey(file), edges);
return { edges, matches };
});
}
function listImportGraphGrepMatches(
cwd: string,
terms: string[],
options: ImportGraphOptions & { testFilesOnly?: boolean } = {},
) {
const tooling = options.tooling === true;
const testFilesOnly = options.testFilesOnly === true;
// Narrow reference matches must not satisfy a later full import-frontier query.
const cacheKey = (term: string) => `${cwd}\0${tooling}\0${testFilesOnly}\0${term}`;
const matches = new Map(
terms.map((term) => [term, cachedImportGraphGrepMatches.get(cacheKey(term)) ?? null]),
);
const missing = [...new Set(terms)].filter(
(term) => !cachedImportGraphGrepMatches.has(cacheKey(term)),
);
if (missing.length === 0) {
return matches;
}
const roots = tooling ? TOOLING_IMPORT_GRAPH_ROOTS : SOURCE_ROOTS_FOR_IMPORT_GRAPH;
const extensions = tooling ? TOOLING_IMPORTABLE_FILE_EXTENSIONS : IMPORTABLE_FILE_EXTENSIONS;
// Literal-reference routing only consumes tests; keep import frontiers on the full scope.
const suffixes = testFilesOnly
? extensions.flatMap((ext) => [`.test${ext}`, `.spec${ext}`])
: extensions;
const grepPaths = importGraphPathspecs(roots, suffixes);
const spawnOptions: SpawnSyncOptionsWithStringEncoding = {
cwd,
encoding: "utf8",
// A frontier can exceed the platform argv limit; Git accepts stdin patterns.
input: missing.join("\n"),
maxBuffer: GIT_LS_FILES_MAX_BUFFER_BYTES,
stdio: ["pipe", "pipe", "pipe"],
};
// Git's fixed-string prefilter rescans for each term. Large frontiers use the
// source reader's single-pass multi-term matcher over the same inventory.
const result =
missing.length <= 64
? spawnSync(
"git",
["grep", "-l", "-z", "--fixed-strings", "-f", "-", "--", ...grepPaths],
spawnOptions,
)
: undefined;
for (const term of missing) {
matches.set(term, []);
}
if (result?.status !== 1) {
const trackedFiles = new Set(listImportGraphFilesForCwd(cwd, { tooling }));
// Source archives use the same filesystem inventory and native reader as the full graph.
const candidates = (
result?.status === 0
? result.stdout.split("\0").filter((file) => trackedFiles.has(file))
: [...trackedFiles].filter((file) => !testFilesOnly || isTestFileTarget(file))
).toSorted((left, right) => left.localeCompare(right));
// Per-term membership preserves the helper first-success rule. One native pass
// matches every candidate, parses only uncached matches, and leaves nonmatches
// uncached so later full-graph queries still read them.
for (const { edges, matches: fileTerms } of readImportGraphEdges(
cwd,
candidates,
trackedFiles,
tooling,
missing,
)) {
for (const term of fileTerms) {
matches.get(term)?.push(edges);
}
}
}
for (const term of missing) {
cachedImportGraphGrepMatches.set(cacheKey(term), matches.get(term) ?? null);
}
return matches;
}
function findDirectImporters(
importedFile: string,
matches: ReadonlyMap<string, ImportGraphEdges[] | null>,
extensions: readonly string[],
resolution?: {
terms: string[];
files: ReadonlySet<string>;
aliases: ImportGraphAlias[];
runtimeOnly?: boolean;
},
) {
const isTestHelper = importedFile.startsWith("test/helpers/");
const terms = resolution?.terms ?? resolveImportGraphSearchTerms(importedFile, extensions);
if (terms.length === 0) {
return null;
}
const importers: string[] = [];
for (const term of terms) {
const candidates = matches.get(term);
if (!candidates) {
return null;
}
for (const edges of candidates) {
const { file, imports } = edges;
const importsChanged = resolution
? edges.specifiers.some(
(specifier) =>
(!resolution.runtimeOnly || !edges.typeOnlySpecifiers.has(specifier)) &&
resolveImportSpecifiers(
file,
specifier,
resolution.files,
extensions,
resolution.aliases,
undefined,
resolution.runtimeOnly,
).includes(importedFile),
)
: imports.has(importedFile);
if (file !== importedFile && !importers.includes(file) && importsChanged) {
importers.push(file);
}
}
if (isTestHelper && importers.length > 0 && term.includes("/")) {
break;
}
}
return importers;
}
/** Prove an entry is unshared using the canonical targeted reverse-import scan. */
export function hasImportGraphConsumers(
changedPaths: string[],
cwd = process.cwd(),
options: ImportGraphOptions = {},
) {
const tracked = listTrackedTestPlanFiles(
cwd,
options.tooling ? TOOLING_IMPORT_GRAPH_GREP_PATHS : IMPORT_GRAPH_GREP_PATHS,
);
// An incomplete archive/filesystem inventory cannot prove that an entry is unshared.
if (tracked === null) {
return true;
}
cachedImportGraphFiles.set(`${cwd}\0${options.tooling ? "tooling" : "source"}`, tracked);
const files = new Set(tracked);
const extensions = options.tooling
? TOOLING_IMPORTABLE_FILE_EXTENSIONS
: IMPORTABLE_FILE_EXTENSIONS;
const aliases = options.resolveAliases ? getImportGraphAliases(cwd) : [];
const termsByFile = new Map<string, string[]>();
for (const file of changedPaths) {
const terms = resolveImportGraphSearchTerms(file, extensions);
const aliasTerms = resolveImportGraphAliasSearchTerms(file, aliases, files, extensions);
if (!files.has(file) || !terms.length || aliasTerms === null) {
return true;
}
termsByFile.set(file, [...new Set([...terms, ...aliasTerms])]);
}
const cached = cachedImportGraphs.get(importGraphCacheKey(cwd, options));
if (
cached?.additionalPaths === "" &&
cached.graph.files.length === tracked.length &&
cached.graph.files.every((file, index) => file === tracked[index]) &&
changedPaths.every((file) => fs.existsSync(path.join(cwd, file)))
) {
return changedPaths.some((file) =>
cached.graph.reverseImports.get(file)?.some((importer) => importer !== file),
);
}
const terms = [...termsByFile.values()].flat();
const matches = listImportGraphGrepMatches(cwd, terms, options);
return changedPaths.some((file) => {
const consumers = findDirectImporters(file, matches, extensions, {
terms: termsByFile.get(file) ?? [],
files,
aliases,
runtimeOnly: options.runtimeOnly,
});
return consumers === null || consumers.length > 0;
});
}
function resolveAffectedTestsFromTargetedImportScan(
changedPath: string,
cwd: string,
options: ImportGraphOptions = {},
) {
const tooling = options.tooling === true;
const files = listImportGraphFilesForCwd(cwd, { tooling });
const fileSet = new Set(files);
if (!fileSet.has(changedPath)) {
return [];
}
const testFiles = new Set(
files.filter((file) => isTestFileTarget(file) && !file.endsWith(".live.test.ts")),
);
let frontier = [changedPath];
const seen = new Set(frontier);
const targets = [];
const extensions = tooling ? TOOLING_IMPORTABLE_FILE_EXTENSIONS : IMPORTABLE_FILE_EXTENSIONS;
while (frontier.length > 0) {
const terms = frontier.flatMap((file) => resolveImportGraphSearchTerms(file, extensions));
const matches = listImportGraphGrepMatches(cwd, terms, { tooling });
const next = [];
for (const current of frontier) {
const importers = findDirectImporters(current, matches, extensions);
if (importers === null) {
return null;
}
for (const importer of importers) {
if (seen.has(importer)) {
continue;
}
seen.add(importer);
if (testFiles.has(importer)) {
targets.push(importer);
} else if (options.direct !== true) {
next.push(importer);
}
}
}
frontier = next;
}
return [...new Set(targets)].toSorted((left, right) => left.localeCompare(right));
}
function importGraphCacheKey(cwd: string, options: ImportGraphOptions) {
return `${cwd}\0${options.tooling === true}\0${options.resolveAliases === true}\0${options.runtimeOnly === true}`;
}
function getImportGraph(
cwd: string,
options: ImportGraphOptions = {},
additionalPaths: string[] = [],
) {
const files = listImportGraphFilesForCwd(cwd, options);
const fileSet = new Set(files);
const missingPaths = additionalPaths.filter((file) => !fileSet.has(file));
const missingKey = missingPaths.toSorted().join("\0");
const cacheKey = importGraphCacheKey(cwd, options);
const cached = cachedImportGraphs.get(cacheKey);
if (cached?.additionalPaths === missingKey) {
return cached.graph;
}
for (const file of missingPaths) {
fileSet.add(file);
}
const reverseImports = new Map<string, string[]>();
const testFiles = new Set(
files.filter((file) => isTestFileTarget(file) && !file.endsWith(".live.test.ts")),
);
readImportGraphEdges(cwd, files, fileSet, options.tooling);
const aliases = options.resolveAliases ? getImportGraphAliases(cwd) : [];
const aliasResolutions = new Map<string, string[]>();
const extensions = options.tooling
? TOOLING_IMPORTABLE_FILE_EXTENSIONS
: IMPORTABLE_FILE_EXTENSIONS;
for (const file of files) {
const edges = cachedImportGraphEdges.get(`${cwd}\0${options.tooling === true}\0${file}`);
if (!edges) {
continue;
}
// Re-resolve cached source facts against deleted paths and this query's alias policy.
for (const specifier of edges.specifiers) {
if (options.runtimeOnly && edges.typeOnlySpecifiers.has(specifier)) {
continue;
}
for (const imported of resolveImportSpecifiers(
file,
specifier,
fileSet,
extensions,
aliases,
aliasResolutions,
options.runtimeOnly,
)) {
const importers = reverseImports.get(imported) ?? [];
importers.push(file);
reverseImports.set(imported, importers);
}
}
}
const graph = { files, reverseImports, testFiles };
cachedImportGraphs.set(cacheKey, { graph, additionalPaths: missingKey });
return graph;
}
/** Query imports/re-exports from targets without scanning unrelated source bodies. */
export function hasImportGraphImpactOnTargets(
changedPaths: string[],
targetPaths: string[] | ((file: string) => boolean),
cwd = process.cwd(),
options: ImportGraphOptions = {},
) {
const changed = new Set(changedPaths);
if (changed.size === 0) {
return false;
}
const files = listImportGraphFilesForCwd(cwd, options);
const targets = Array.isArray(targetPaths) ? targetPaths : files.filter(targetPaths);
if (targets.some((file) => changed.has(file))) {
return true;
}
// Staged deletions have left the index, but surviving importers still own their edges.
const fileSet = new Set([...files, ...changed]);
const extensions = options.tooling
? TOOLING_IMPORTABLE_FILE_EXTENSIONS
: IMPORTABLE_FILE_EXTENSIONS;
const seen = new Set(targets);
const aliases = options.resolveAliases ? getImportGraphAliases(cwd) : [];
const aliasResolutions = new Map<string, string[]>();
let frontier = targets;
while (frontier.length > 0) {
readImportGraphEdges(cwd, frontier, fileSet, options.tooling);
const next: string[] = [];
for (const file of frontier) {
const edges = cachedImportGraphEdges.get(`${cwd}\0${options.tooling === true}\0${file}`);
// Resolve raw cached facts against this query's deleted-path identities too.
for (const specifier of edges?.specifiers ?? []) {
if (options.runtimeOnly && edges?.typeOnlySpecifiers.has(specifier)) {
continue;
}
for (const dependency of resolveImportSpecifiers(
file,
specifier,
fileSet,
extensions,
aliases,
aliasResolutions,
options.runtimeOnly,
)) {
if (changed.has(dependency)) {
return true;
}
if (!options.direct && !seen.has(dependency)) {
seen.add(dependency);
next.push(dependency);
}
}
}
}
frontier = next;
}
return false;
}
function walkAffectedTestsFromImportGraph(
changedPaths: string[],
{ reverseImports, testFiles }: ImportGraph,
direct = false,
) {
const queue = [...changedPaths];
const seen = new Set(queue);
const targets: string[] = [];
for (const current of queue) {
for (const importer of reverseImports.get(current) ?? []) {
if (seen.has(importer)) {
continue;
}
seen.add(importer);
if (testFiles.has(importer)) {
targets.push(importer);
}
if (!direct) {
queue.push(importer);
}
}
}
return targets.toSorted((left, right) => left.localeCompare(right));
}
export function resolveAffectedTestsFromImportGraph(
changedPath: string | string[],
cwd: string,
options: ImportGraphOptions & { forceFull?: boolean } = {},
) {
const paths = typeof changedPath === "string" ? [changedPath] : changedPath;
const changedTests = options.direct
? paths.filter(
(file) =>
isTestFileTarget(file) &&
!file.endsWith(".live.test.ts") &&
fs.existsSync(path.join(cwd, file)),
)
: [];
if (
!paths.length ||
(paths.every(isTestFileTarget) && !hasImportGraphConsumers(paths, cwd, options))
) {
return changedTests;
}
if (
!options.resolveAliases &&
!options.runtimeOnly &&
options.forceFull !== true &&
typeof changedPath === "string"
) {
const targetedTargets = resolveAffectedTestsFromTargetedImportScan(changedPath, cwd, options);
if (targetedTargets !== null) {
return uniqueOrdered([...changedTests, ...targetedTargets]).toSorted((left, right) =>
left.localeCompare(right),
);
}
}
return uniqueOrdered([
...changedTests,
...walkAffectedTestsFromImportGraph(paths, getImportGraph(cwd, options, paths), options.direct),
]).toSorted((left, right) => left.localeCompare(right));
}
/** Complete transitive consumers, including erased type imports unless runtimeOnly is requested. */
export function resolveImportGraphDependents(
changedPaths: readonly string[],
cwd = process.cwd(),
options: ImportGraphOptions = {},
) {
const roots = new Set(changedPaths);
const { reverseImports } = getImportGraph(cwd, options, [...roots]);
const seen = new Set(roots);
// Set iteration visits newly admitted consumers once, including across cycles.
for (const current of seen) {
for (const importer of reverseImports.get(current) ?? []) {
seen.add(importer);
}
}
return [...seen]
.filter((file) => !roots.has(file))
.toSorted((left, right) => left.localeCompare(right));
}
/** Changed resolved dependencies enter the same graph at their literal import consumers. */
export function resolveDependencyTestConsumers(
importers: Array<{ root: string; dependencies: string[] }>,
cwd = process.cwd(),
{
runtimeOnly = false,
importerBindings,
}: Pick<ImportGraphOptions, "runtimeOnly"> & {
importerBindings?: Array<{ root: string; dependencies: string[] }>;
} = {},
) {
if (importers.every(({ dependencies }) => dependencies.length === 0)) {
return { sources: [], tests: [], unresolved: [] };
}
const options = { tooling: true, resolveAliases: true, runtimeOnly };
getImportGraph(cwd, options);
const files = listImportGraphFilesForCwd(cwd, options);
const unresolved = new Map<string, { root: string; dependency: string }>();
for (const { root, dependencies } of importers) {
for (const dependency of dependencies) {
unresolved.set(`${root}\0${dependency}`, { root, dependency });
}
}
const dependencyRoots = new Map<string, string[]>();
// Changed roots retain removed bindings; the lock owner supplies unchanged
// workspace bindings so a root update cannot claim their dependency consumers.
for (const { root, dependencies } of [...(importerBindings ?? []), ...importers]) {
for (const dependency of dependencies) {
const roots = dependencyRoots.get(dependency) ?? [];
if (!roots.includes(root)) {
roots.push(root);
}
dependencyRoots.set(dependency, roots);
}
}
for (const roots of dependencyRoots.values()) {
roots.sort((left, right) => right.length - left.length);
}
const bindingOwner = (file: string, dependency: string) =>
dependencyRoots.get(dependency)?.find((root) => root !== "." && file.startsWith(`${root}/`)) ??
".";
const sources = files.filter((file) => {
const edges = cachedImportGraphEdges.get(`${cwd}\0true\0${file}`);
if (!edges) {
return false;
}
let runtimeConsumer = false;
for (const { root, dependencies } of importers) {
if (root !== "." && !file.startsWith(`${root}/`)) {
continue;
}
for (const dependency of dependencies) {
if (importerBindings && bindingOwner(file, dependency) !== root) {
continue;
}
const specifiers = edges.specifiers.filter(
(specifier) => specifier === dependency || specifier.startsWith(`${dependency}/`),
);
if (specifiers.length > 0) {
// Erased imports prove type ownership; absent usage may be a CLI or
// generated child script and must remain unresolved for the planner.
unresolved.delete(`${root}\0${dependency}`);
runtimeConsumer ||= specifiers.some(
(specifier) => !runtimeOnly || !edges.typeOnlySpecifiers.has(specifier),
);
}
}
}
return runtimeConsumer;
});
return {
sources,
unresolved: [...unresolved.values()],
tests: uniqueOrdered([
...sources.filter((file) => isTestFileTarget(file) && !file.endsWith(".live.test.ts")),
...resolveAffectedTestsFromImportGraph(sources, cwd, options),
]).toSorted((left, right) => left.localeCompare(right)),
};
}
/** Whole-area UI fallback also owns host tests importing UI and changed-source readers. */
export function resolveControlUiTestConsumers(changedPaths: string[], cwd = process.cwd()) {
const uiFiles = listImportGraphFilesForCwd(cwd, { tooling: true }).filter(isControlUiSourcePath);
return uniqueOrdered([
...resolveAffectedTestsFromImportGraph(uiFiles, cwd, { forceFull: true, tooling: true }),
...resolveDirectToolingReferenceTests(changedPaths, cwd),
]).filter((file) => !isControlUiSourcePath(file));
}
function resolveVitestConfigTargetKind(relative: string) {
return VITEST_CONFIG_TARGET_KIND_BY_PATH.get(relative) ?? null;
}
function isVitestConfigPathLikeTarget(relative: string) {
return (
relative === "vitest.config.ts" || /^test\/vitest\/vitest\..+\.config\.ts$/u.test(relative)
);
}
function isVitestConfigFileTarget(relative: string) {
return RUNNABLE_VITEST_CONFIG_TARGETS.has(relative);
}
/** Config identities do not require test discovery or CI shard construction. */
export function listRunnableVitestConfigTargets(): string[] {
return [...RUNNABLE_VITEST_CONFIG_TARGETS];
}
function isVitestConfigTargetForKind(kind: string, targetArg: string, cwd: string) {
return resolveVitestConfigTargetKind(toRepoRelativeTarget(targetArg, cwd)) === kind;
}
function isControlUiE2eTarget(relative: string) {
return (
uiE2eRealGatewayTestFiles.includes(relative) ||
relative === "ui/src/test-helpers/control-ui-e2e.ts" ||
relative === "ui/src/e2e" ||
relative.startsWith("ui/src/e2e/") ||
(isControlUiSourcePath(relative) && relative.endsWith(".e2e.test.ts"))
);
}
function resolveChannelContractTargetKind(relative: string) {
if (!relative.startsWith("src/channels/plugins/contracts/")) {
return null;
}
for (const [config, patterns] of CHANNEL_CONTRACT_CONFIG_PATTERNS) {
if (patterns.some((pattern) => path.matchesGlob(relative, pattern))) {
return VITEST_CONFIG_TARGET_KIND_BY_PATH.get(config);
}
}
return "contractsChannelSession";
}
function listChangedPathsFromGit(baseRef: string, cwd: string) {
return listChangedPathsFromGitSource({ base: baseRef, cwd });
}
function extractChangedBaseRef(args: string[]) {
for (let index = 0; index < args.length; index += 1) {
const arg = args[index];
if (arg === undefined) {
continue;
}
const match = arg.match(CHANGED_ARGS_PATTERN);
if (!match) {
continue;
}
if (match[1]) {
return match[1];
}
const nextArg = args[index + 1];
return nextArg && nextArg !== "--" && !nextArg.startsWith("-") ? nextArg : "HEAD";
}
return null;
}
function stripChangedArgs(args: string[]) {
const strippedArgs = [];
for (let index = 0; index < args.length; index += 1) {
const arg = args[index];
if (arg === undefined) {
continue;
}
const match = arg.match(CHANGED_ARGS_PATTERN);
if (!match) {
strippedArgs.push(arg);
continue;
}
if (!match[1]) {
const nextArg = args[index + 1];
if (nextArg && nextArg !== "--" && !nextArg.startsWith("-")) {
index += 1;
}
}
}
return strippedArgs;
}
function shouldKeepBroadChangedRun(changedPaths: string[]) {
return changedPaths.some((changedPath) =>
PRECISE_SOURCE_TEST_TARGETS.has(changedPath)
? false
: BROAD_CHANGED_FALLBACK_PATTERNS.some((pattern) => pattern.test(changedPath)),
);
}
function resolveToolingChangedTestTargets(
changedPaths: string[],
cwd = process.cwd(),
options: ChangedTestTargetOptions = {},
) {
const targets = [];
for (const changedPath of changedPaths) {
const testTargets =
SOURCE_TEST_TARGETS.get(changedPath) ?? resolveToolingTestTargets(changedPath, cwd, options);
if (!testTargets) {
return null;
}
targets.push(...testTargets);
if (CHANNEL_PLUGIN_SHAPE_PARITY_WIRING_PATHS.has(changedPath)) {
targets.push(CHANNEL_PLUGIN_SHAPE_PARITY_TEST_TARGET);
}
}
return [...new Set(targets)];
}
const TOOLING_SCRIPT_PATH_PATTERN = /^scripts\/(.+)\.(?:mjs|cjs|js|mts|cts|ts|sh|py|ps1)$/u;
function resolveConventionalToolingTestTargets(changedPath: string, cwd = process.cwd()) {
const match = TOOLING_SCRIPT_PATH_PATTERN.exec(changedPath);
if (!match) {
return null;
}
const stem = match[1];
if (stem === undefined) {
return null;
}
const basename = path.posix.basename(stem);
const dashedStem = stem.replaceAll("/", "-");
const e2eLibStem = stem.startsWith("e2e/lib/") ? stem.slice("e2e/lib/".length) : null;
const e2eLibDashedStem = e2eLibStem?.replaceAll("/", "-");
const e2eLibParts = e2eLibStem?.split("/") ?? [];
const e2eLibFamily = e2eLibParts.length > 1 ? e2eLibParts[0] : null;
const e2eLibFamilyCandidates = e2eLibFamily
? [
`test/scripts/${e2eLibFamily}.test.ts`,
`test/scripts/${e2eLibFamily}-client.test.ts`,
`test/scripts/${e2eLibFamily}-assertions.test.ts`,
`test/scripts/${e2eLibFamily}-probe.test.ts`,
]
: [];
const candidates = [
`test/scripts/${stem}.test.ts`,
`test/scripts/${dashedStem}.test.ts`,
`test/scripts/${basename}.test.ts`,
...(e2eLibDashedStem
? [`test/scripts/${e2eLibDashedStem}.test.ts`, `test/scripts/e2e-${e2eLibDashedStem}.test.ts`]
: []),
...e2eLibFamilyCandidates,
`src/scripts/${stem}.test.ts`,
`src/scripts/${dashedStem}.test.ts`,
`src/scripts/${basename}.test.ts`,
];
const targets = candidates.filter((candidate) => fs.existsSync(path.join(cwd, candidate)));
return targets.length > 0 ? targets : null;
}
function isToolingScriptPath(changedPath: string) {
return TOOLING_SCRIPT_PATH_PATTERN.test(changedPath);
}
function resolveUpgradeSurvivorConfigRecipeTargets(changedPath: string) {
if (!/^scripts\/e2e\/lib\/upgrade-survivor\/config-recipe\/[^/]+\.json$/u.test(changedPath)) {
return null;
}
return ["test/scripts/upgrade-survivor-config-recipe.test.ts"];
}
function resolveDocsI18nBehaviorTargets(changedPath: string) {
if (!/^scripts\/docs-i18n\/testdata\/behavior\/[^/]+\/[^/]+$/u.test(changedPath)) {
return null;
}
return ["test/scripts/docs-i18n.test.ts"];
}
function resolveDocsI18nGoTargets(changedPath: string) {
if (!/^scripts\/docs-i18n\/(?:go\.(?:mod|sum)|[^/]+\.go)$/u.test(changedPath)) {
return null;
}
const targets = ["test/scripts/docs-i18n.test.ts"];
if (changedPath === "scripts/docs-i18n/go.mod") {
targets.push("test/scripts/ci-workflow-planning.test.ts");
}
return targets;
}
function resolveK8sManifestTargets(changedPath: string) {
if (!/^scripts\/k8s\/manifests\/[^/]+\.yaml$/u.test(changedPath)) {
return null;
}
return ["test/scripts/k8s-manifests.test.ts"];
}
function resolveParallelsToolingTestTargets(changedPath: string) {
if (
!/^scripts\/e2e\/parallels\/[^/]+\.ts$/u.test(changedPath) &&
!/^scripts\/e2e\/parallels-(?:linux|macos|npm-update|windows)-smoke\.sh$/u.test(changedPath)
) {
return null;
}
const targets = ["test/scripts/parallels-smoke-model.test.ts"];
if (
[
"scripts/e2e/parallels/guest-transports.ts",
"scripts/e2e/parallels/host-command.ts",
"scripts/e2e/parallels/npm-update-scripts.ts",
"scripts/e2e/parallels/npm-update-smoke.ts",
].includes(changedPath)
) {
targets.push("test/scripts/parallels-npm-update-smoke.test.ts");
}
if (changedPath === "scripts/e2e/parallels/update-job-timeout.ts") {
targets.push("test/scripts/parallels-update-job-timeout.test.ts");
}
return targets;
}
function resolveToolingTestOwnerTargets(...owners: string[]) {
return owners.map((owner) => (owner.includes("/") ? owner : `test/scripts/${owner}.test.ts`));
}
const packageAcceptance = "package-acceptance-workflow";
const dockerBuild = "docker-build-helper";
const dockerE2e = "docker-e2e-plan";
const workflowGuards = "ci-workflow-guards";
const workflowPlanning = "ci-workflow-planning";
const workflowEvidence = "ci-workflow-evidence";
const pluginPrerelease = "plugin-prerelease-test-plan";
const releaseCheck = "test/release-check.test.ts";
const installDocker = "test-install-sh-docker";
const changedScope = "src/scripts/ci-changed-scope.test.ts";
const changedScopeTests = [
"src/scripts/ci-changed-scope.contract-fixtures.test.ts",
"src/scripts/ci-changed-scope.control-ui.test.ts",
"src/scripts/ci-changed-scope.git-owner.test.ts",
"src/scripts/ci-changed-scope.native-i18n.test.ts",
changedScope,
"src/scripts/ci-changed-scope.windows.test.ts",
];
const dockerCache = "src/docker-build-cache.test.ts";
const dockerDigests = "src/docker-image-digests.test.ts";
const openaiChatToolsE2e = "test/e2e/qa-lab/runtime/openai-compatible-chat-tools.e2e.test.ts";
const npmPostpublish = "test/openclaw-npm-postpublish-verify.test.ts";
const crossOsReleaseChecks = "openclaw-cross-os-release-checks";
const runNode = "src/infra/run-node.test.ts";
const pluginSdkEntryOwners = [
"src/plugins/contracts/plugin-sdk-index.bundle.test.ts",
"src/plugins/contracts/plugin-sdk-package-contract-guardrails.test.ts",
"src/plugins/contracts/plugin-sdk-subpaths.test.ts",
"src/plugins/contracts/extension-package-project-boundaries.test.ts",
"plugin-sdk-surface-report",
"build-all",
releaseCheck,
"prepare-extension-package-boundary-artifacts",
"ts-topology",
"test/vitest/vitest.tooling.config.ts",
];
// Keep only genuinely ambiguous paths explicit; conventional discovery owns
// unambiguous scripts and direct imports without a second inventory.
const EXACT_TOOLING_TARGETS = new Map<string, string[]>([
// The native gate/check handoff crosses processes outside the import graph.
["scripts/check.mts", ["check", "pr-gate-base"]],
[
"scripts/pr-lib/gates.sh",
[
"pr-correction-preparation",
"pr-crabbox-gate-plan",
"pr-main-refresh",
"pr-merge-hosted",
"pr-metadata",
"pr-prepare-gates",
"pr-prepare-preflight",
"pr-wrappers",
"pr-gate-base",
],
],
[".github/workflows/ci.yml", ["ci-platform-checkout", "ci-linux-git", "ci-git-owner"]],
[".github/actions/setup-android-toolchain/action.yml", [workflowPlanning]],
[".github/workflows/docs-sync-publish.yml", ["docs-sync-publish"]],
[".github/workflows/docs-agent.yml", ["docs-agent-workflow"]],
["scripts/generate-ci-git-owner.mts", ["ci-git-owner"]],
[
".github/workflows/openclaw-live-and-e2e-checks-reusable.yml",
[packageAcceptance, workflowGuards, "release-workflow-matrix-plan", installDocker],
],
[
".github/workflows/plugin-clawhub-release.yml",
[packageAcceptance, "plugin-release-git-lifecycle", workflowGuards],
],
[
".github/workflows/plugin-npm-release.yml",
[
packageAcceptance,
"plugin-npm-extended-stable-workflow",
"plugin-release-git-lifecycle",
workflowGuards,
],
],
[".github/workflows/qa-live-transports-convex.yml", [packageAcceptance, workflowGuards]],
[".github/workflows/update-migration.yml", [packageAcceptance, workflowGuards]],
[
".github/actions/setup-node-env/action.yml",
["setup-node-env-bun", "setup-node-env-semantic-memory", packageAcceptance, workflowGuards],
],
[
".github/actions/setup-node-env/dependency-fingerprint.mjs",
[workflowGuards, "setup-node-env-dependency-fingerprint"],
],
[".github/actions/setup-node-env/seed-bun-from-image.mjs", ["setup-node-env-bun"]],
[".github/actions/setup-pnpm-store-cache/action.yml", [packageAcceptance, workflowGuards]],
[".github/actions/setup-pnpm-store-cache/ensure-node.sh", ["setup-pnpm-store-cache-ensure-node"]],
["test/e2e/qa-lab/runtime/mcp-channels-docker-client.ts", [dockerE2e, pluginPrerelease]],
["scripts/e2e/lib/mcp-code-mode-probe-server.ts", ["mcp-code-mode-gateway-client"]],
["scripts/e2e/cron-cli-docker.sh", [dockerBuild, "docker-e2e-observability"]],
["scripts/ios-release-upload.sh", ["ios-release-wrapper-args", "ios-release-fastlane-gates"]],
["scripts/release-verify-beta.ts", ["release-wrapper-scripts"]],
["scripts/lib/bundled-plugin-build-entries.mjs", ["bundled-plugin-build-entries", releaseCheck]],
["scripts/lib/docker-e2e-package.sh", [dockerBuild]],
["scripts/relay-build-limit-warnings.mts", [dockerBuild]],
[
"scripts/lib/release-version.mjs",
[
"test/release-version.test.ts",
"test/npm-publish-plan.test.ts",
"test/openclaw-npm-release-check.test.ts",
npmPostpublish,
"test/plugin-npm-release.test.ts",
"test/plugin-clawhub-release.test.ts",
"android-version",
"android-pin-version",
"docker-release-policy",
"docker-release-artifacts",
"full-release-validation-at-sha",
"ios-version",
"openclaw-npm-extended-stable-release",
"openclaw-npm-publish",
"npm-prepared-bundle",
"release-preflight",
"release-prepare",
"release-upgrade-baseline",
"release-version",
"upgrade-survivor-baselines",
"upgrade-survivor-config-recipe",
],
],
[
"scripts/lib/release-context.mjs",
["full-release-validation-at-sha", "release-candidate-checklist", packageAcceptance],
],
[
"scripts/lib/clawhub-bootstrap-artifact.mjs",
["clawhub-bootstrap-artifact", "verify-clawhub-published-artifact"],
],
[
"scripts/lib/plugin-npm-release.ts",
["test/plugin-npm-release.test.ts", "test/plugin-clawhub-release.test.ts"],
],
[
"scripts/lib/extension-source-classifier.mts",
[
"extension-source-classifier",
"src/channels/plugins/contracts/channel-import-guardrails.test.ts",
],
],
["scripts/build-stamp.mts", ["src/infra/build-stamp.test.ts"]],
["scripts/run-vitest.mjs", ["run-vitest", "test-projects", "vitest-local-scheduling"]],
["scripts/run-vitest.mts", ["run-vitest", "test-projects", "vitest-local-scheduling"]],
["scripts/run-oxlint-shards.mts", ["run-oxlint", "lint-status"]],
["scripts/run-oxlint.mts", ["run-oxlint", "lint-status"]],
["scripts/run-oxlint.mjs", ["run-oxlint", "lint-status"]],
["scripts/run-lint.mts", ["run-oxlint", "lint-status"]],
["scripts/run-stylelint.mts", ["changed-lanes", "lint-status"]],
[
"scripts/lib/failed-trailer.mts",
["run-oxlint", "run-tsgo", "run-vitest", "changed-lanes", "lint-status"],
],
["scripts/lib/managed-child-process.mts", ["managed-child-process", "lint-status"]],
["scripts/lib/dist-artifact-ownership.mts", ["dist-artifact-ownership", "lint-status"]],
["scripts/lib/dist-artifact-lock.mts", ["dist-artifact-ownership", "lint-status"]],
["scripts/docker-e2e-rerun.mts", ["docker-e2e-helper-cli"]],
["scripts/openclaw-postpack.mjs", [TOOLING_VITEST_CONFIG]],
["scripts/package-manifest.mjs", ["test/openclaw-prepack.test.ts"]],
["scripts/openclaw-npm-prepublish-verify.ts", ["test/openclaw-npm-prepublish-verify.test.ts"]],
["scripts/lib/docker-e2e-scenarios.mts", [dockerE2e, pluginPrerelease]],
["scripts/lib/upgrade-survivor-policy.mjs", [dockerE2e]],
["scripts/lib/upgrade-survivor-scenarios.json", [dockerE2e]],
["scripts/e2e/kitchen-sink-rpc-walk.mts", ["kitchen-sink-rpc-walk", pluginPrerelease]],
[
"scripts/e2e/agents-delete-shared-workspace-docker.sh",
[dockerE2e, changedScope, "src/commands/agents.delete.test.ts"],
],
[
"scripts/e2e/browser-cdp-snapshot-docker.sh",
[dockerBuild, "browser-cdp-snapshot", "e2e-helper-env-limits"],
],
[
"scripts/e2e/config-reload-source-docker.sh",
[
dockerE2e,
packageAcceptance,
"fixture-config",
"e2e-mock-config-limits",
"src/gateway/config-reload.test.ts",
],
],
[
"scripts/e2e/gateway-network-docker.sh",
[dockerBuild, dockerE2e, packageAcceptance, "gateway-network-client", changedScope],
],
["scripts/e2e/npm-telegram-live-runner.ts", ["npm-telegram-live"]],
[
"scripts/e2e/upgrade-survivor-docker.sh",
[
dockerBuild,
dockerE2e,
packageAcceptance,
"upgrade-survivor-probe-gateway",
"upgrade-survivor-assertions",
"upgrade-survivor-mobile-pairing",
"upgrade-survivor-recovery-cleanup",
"openclaw-test-state",
],
],
[
"scripts/e2e/lib/upgrade-survivor/run.sh",
[
packageAcceptance,
"upgrade-survivor-missing-load-path",
"upgrade-survivor-assertions",
"upgrade-survivor-mobile-pairing",
"upgrade-survivor-recovery-cleanup",
"upgrade-survivor-watchos-direct-node",
],
],
[
"scripts/e2e/lib/upgrade-survivor/watchos-direct-node.mjs",
["upgrade-survivor-watchos-direct-node"],
],
[
"scripts/e2e/lib/upgrade-survivor/mobile-pairing-client.mts",
["upgrade-survivor-mobile-pairing"],
],
[
"scripts/e2e/lib/upgrade-survivor/recovery-cleanup-fixture.mjs",
["upgrade-survivor-recovery-cleanup"],
],
[
"scripts/e2e/bundled-plugin-install-uninstall-docker.sh",
[dockerBuild, dockerE2e, pluginPrerelease, "bundled-plugin-install-uninstall-probe"],
],
["scripts/e2e/lib/plugin-update/corrupt-update-scenario.sh", ["plugin-update-unchanged-docker"]],
["scripts/e2e/lib/plugin-update/probe.mjs", ["plugin-update-unchanged-docker"]],
["scripts/e2e/lib/plugin-update/unchanged-scenario.sh", ["plugin-update-unchanged-docker"]],
[
"scripts/e2e/update-corrupt-plugin-docker.sh",
[dockerBuild, dockerE2e, packageAcceptance, "plugin-update-unchanged-docker"],
],
["scripts/e2e/plugins-docker.sh", [dockerBuild, dockerE2e, "plugins-assertions"]],
[
"scripts/e2e/release-user-journey-docker.sh",
[dockerBuild, dockerE2e, packageAcceptance, "release-user-journey-assertions"],
],
[
"scripts/e2e/openai-image-auth-docker.sh",
[
dockerBuild,
dockerE2e,
"openai-image-auth-docker-client",
"extensions/openai/image-generation-provider.test.ts",
],
],
["scripts/e2e/lib/openai-chat-tools/client.mjs", [openaiChatToolsE2e]],
[
"scripts/e2e/lib/openai-web-search-minimal/client.mjs",
["test/e2e/qa-lab/runtime/openai-web-search-minimal.e2e.test.ts"],
],
["scripts/e2e/qr-import-docker.sh", [dockerBuild]],
["scripts/bundle-a2ui.mts", ["bundled-plugin-assets"]],
]);
const SEMANTIC_TOOLING_TARGET_PATTERNS: Array<[RegExp, string[]]> = [
[
/^test\/tsconfig\/tsconfig\.test\.root(?:\.(?:tooling|scripts|e2e|other))?\.json$/u,
["tsgo-core-test-shards", "changed-lanes"],
],
[
/^(?:git-hooks\/pre-commit|scripts\/pre-commit\/(?:guard-staged-content\.mjs|filter-staged-files\.mjs|format-staged\.sh|run-node-tool\.sh)|test\/git-hooks-pre-commit\.test-support\.ts)$/u,
["test/git-hooks-pre-commit.test.ts", "test/git-hooks-pre-commit-boundaries.test.ts"],
],
[
/^scripts\/pr$/u,
[
"pr-merge",
"pr-merge-outcome",
"pr-merge-qualified-refusal",
"pr-operation-lock",
"pr-wrappers",
],
],
[
/^scripts\/pr-lib\/crabbox-gate-contract\.mjs$/u,
["pr-crabbox-gate-publisher", "pr-crabbox-merge-bypass"],
],
[
/^scripts\/pr-lib\/crabbox-gate-plan\.mts$/u,
["pr-crabbox-gate-plan", "pr-crabbox-gate-publisher", "pr-prepare-gates"],
],
[
/^scripts\/pr-lib\/crabbox-merge-bypass\.sh$/u,
["pr-crabbox-merge-bypass", "pr-merge", "pr-merge-outcome"],
],
[/^scripts\/lib\/windows-taskkill\.mjs$/u, ["managed-child-process", "run-with-env"]],
[
/^scripts\/lib\/config-boundary-guard\.mts$/u,
[
"src/plugins/contracts/config-boundary-guard.test.ts",
"src/plugins/contracts/deprecated-internal-config-api.test.ts",
],
],
[
/^\.github\/workflows\/ci\.yml$/u,
[
workflowGuards,
workflowPlanning,
workflowEvidence,
"changed-lanes",
"check-workflows",
"plugin-contract-test-plan",
pluginPrerelease,
"verify-pr-hosted-gates",
],
],
[
/^\.github\/workflows\/ci-check-testbox\.yml$/u,
[workflowGuards, packageAcceptance, "changed-lanes"],
],
[/^\.github\/workflows\/ci-check-arm-testbox\.yml$/u, [workflowGuards, packageAcceptance]],
[/^\.github\/workflows\/crabbox-hydrate\.yml$/u, [workflowGuards, packageAcceptance]],
[
/^\.github\/workflows\/ci-build-artifacts-testbox\.yml$/u,
[packageAcceptance, workflowGuards, workflowPlanning],
],
[
/^\.github\/workflows\/full-release-validation\.yml$/u,
[
"src/dockerfile.test.ts",
"full-release-validation-state",
"full-release-validation-at-sha",
"full-release-candidate-reuse",
"find-reusable-release-validation",
"openclaw-npm-extended-stable-full-validation-workflow",
"release-no-push-workflow",
"release-ci-summary",
packageAcceptance,
pluginPrerelease,
"check-workflows",
],
],
[
/^\.github\/workflows\/full-release-candidate\.yml$/u,
["full-release-candidate-reuse", packageAcceptance, "check-workflows", workflowGuards],
],
[
/^\.github\/workflows\/openclaw-release-checks\.yml$/u,
[packageAcceptance, crossOsReleaseChecks, pluginPrerelease, installDocker, workflowEvidence],
],
[
/^\.github\/workflows\/docker-release(?:-prepare)?\.yml$/u,
[
"src/dockerfile.test.ts",
"docker-channel-promote",
"docker-release-artifacts",
"vercel-container-registry-publish",
],
],
[/^\.github\/workflows\/install-smoke\.yml$/u, ["install-smoke-no-push-workflow", installDocker]],
[
/^\.github\/workflows\/openclaw-performance\.yml$/u,
["openclaw-performance-workflow", "openclaw-performance-git-lifecycle"],
],
[/^\.github\/workflows\/linux-app-release\.yml$/u, ["release-workflow-git-lifecycle"]],
[
/^\.github\/workflows\/macos-release\.yml$/u,
["release-workflow-git-lifecycle", packageAcceptance],
],
[
/^\.github\/workflows\/npm-placeholder-bootstrap\.yml$/u,
["release-workflow-git-lifecycle", "npm-placeholder-publication"],
],
[/^\.github\/workflows\/plugin-prerelease\.yml$/u, [pluginPrerelease]],
[/^\.github\/workflows\/tui-pty\.yml$/u, [packageAcceptance]],
[
/^\.github\/workflows\/openclaw-cross-os-release-checks-reusable\.yml$/u,
[crossOsReleaseChecks, "openclaw-cross-os-release-workflow", packageAcceptance],
],
[
/^\.github\/workflows\/openclaw-release-publish\.yml$/u,
[packageAcceptance, "docker-release-artifacts", "vercel-container-registry-publish"],
],
[/^\.github\/workflows\/package-acceptance\.yml$/u, [packageAcceptance]],
[
/^\.github\/workflows\/vercel-container-registry-publish\.yml$/u,
["docker-channel-promote", "release-plan-producer", "vercel-container-registry-publish"],
],
[
/^\.github\/workflows\/plugin-clawhub-new\.yml$/u,
[packageAcceptance, "plugin-clawhub-new-workflow"],
],
[
/^\.github\/workflows\/openclaw-npm-release\.yml$/u,
[npmPostpublish, "openclaw-npm-extended-stable-workflow", packageAcceptance],
],
[
new RegExp(
[
"^\\.github\\/workflows\\/(?:auto-response|clawsweeper-dispatch|labeler|",
"real-behavior-proof|stale)\\.yml$",
].join(""),
"u",
),
[workflowGuards],
],
[
new RegExp(
[
"^\\.github\\/workflows\\/mantis-(?:discord-(?:smoke|status-reactions|",
"thread-attachment)|slack-desktop-smoke)\\.yml$",
].join(""),
"u",
),
[packageAcceptance, workflowGuards],
],
[
/^\.github\/(?:workflows\/mantis-web-ui-chat-proof\.yml|actions\/mantis-validate-trusted-ref\/action\.yml)$/u,
["mantis-web-ui-chat-proof-workflow", packageAcceptance, workflowGuards],
],
[/^\.github\/workflows\/android-release\.yml$/u, [packageAcceptance, workflowGuards]],
[
/^\.github\/workflows\/(?:qa-profile-evidence|maturity-scorecard|mantis-discord-(?:status-reactions|thread-attachment))\.yml$/u,
[workflowEvidence],
],
[
/^\.github\/(?:actions\/(?:ensure-base-commit|git-owner|publish-generated-pr|mantis-validate-trusted-ref)\/|workflows\/(?:workflow-sanity|qa-profile-evidence|maturity-scorecard|docs-agent|docs-sync-publish|openclaw-performance|linux-app-release|macos-release|npm-placeholder-bootstrap|plugin-clawhub-release|plugin-npm-release|mantis-(?:discord-(?:smoke|status-reactions|thread-attachment)|slack-desktop-smoke|web-ui-chat-proof))\.yml$)/u,
[
"ci-git-owner",
"ci-linux-git",
"ci-platform-checkout",
"src/scripts/ci-changed-scope.git-owner.test.ts",
],
],
[/^\.github\/actions\/publish-generated-pr\//u, [workflowGuards]],
[/^tsconfig\.scripts\.json$/u, ["changed-lanes", "test-projects"]],
[/^scripts\/test-projects\.test-support\.mts$/u, ["test-projects"]],
[/^scripts\/ci-changed-scope\.mjs$/u, [...changedScopeTests, "control-ui-i18n"]],
[/^scripts\/check-changed\.(?:mjs|mts)$/u, ["changed-lanes"]],
[/^scripts\/changed-lanes\.(?:mjs|mts)$/u, ["changed-lanes"]],
[
/^scripts\/(?:lib\/tsx-cli-shim|tsx)\.mjs$/u,
["direct-run-entrypoints", "lint-status", "local-check-runtime"],
],
[
new RegExp(
[
"^scripts\\/(?:generate-prompt-snapshots|prompt-snapshot-files|",
"sync-codex-model-prompt-fixture)\\.ts$",
].join(""),
"u",
),
["prompt-snapshots"],
],
[/^scripts\/e2e\/npm-telegram-live-docker\.sh$/u, ["npm-telegram-live"]],
[
/^scripts\/package-openclaw-for-docker\.m[jt]s$/u,
["test/e2e/qa-lab/runtime/package-openclaw-for-docker.e2e.test.ts"],
],
[/^scripts\/run-node\.(?:mjs|mts)$/u, [runNode]],
[/^scripts\/ios-write-swift-filelist\.m[jt]s$/u, ["ios-run"]],
[
/^scripts\/pr-lib\/(?:merge(?:-outcome)?\.sh|merge-(?:legacy|pre-dispatch)-refusal\.mjs)$/u,
["pr-merge", "pr-merge-outcome", "pr-merge-pre-dispatch-refusal", "pr-merge-qualified-refusal"],
],
[/^scripts\/plugin-clawhub-publish\.sh$/u, ["test/plugin-clawhub-release.test.ts"]],
[/^scripts\/openclaw-npm-postpublish-verify\.ts$/u, [npmPostpublish]],
[
/^scripts\/install\.ps1$/u,
["install-ps1", "website-installer-sync-workflow", crossOsReleaseChecks, changedScope],
],
[
/^scripts\/(?:crabbox-wrapper(?:-providers)?|crabbox-routing-policy|testbox-lease-freshness)\.mts$/u,
["crabbox-wrapper"],
],
[/^scripts\/crabbox-wrapper\.mjs$/u, ["crabbox-wrapper"]],
[
/^scripts\/copy-bundled-plugin-metadata\.(?:mjs|mts)$/u,
["src/plugins/copy-bundled-plugin-metadata.test.ts", runNode],
],
[
/^scripts\/github\/run-openclaw-cross-os-release-checks\.sh$/u,
["openclaw-cross-os-release-workflow"],
],
[
/^scripts\/(?:write-plugin-sdk-entry-dts\.ts|lib\/local-check-runtime\.mts)$/u,
[
"test/scripts/write-plugin-sdk-entry-dts.test.ts",
"build-all",
"declaration-stage",
"tsdown-build",
"prepare-extension-package-boundary-artifacts",
],
],
[/^scripts\/pr-lib\/worktree\.sh$/u, ["test/vitest/vitest.tooling.config.ts"]],
[/^scripts\/dev\/gateway-smoke\.ts$/u, ["test/e2e/qa-lab/runtime/gateway-smoke.e2e.test.ts"]],
[/^scripts\/e2e\/mock-openai-server\.mjs$/u, ["e2e-mock-config-limits"]],
[/^apps\/android\/scripts\/build-release-artifacts\.ts$/u, ["android-release-artifacts"]],
[
new RegExp(
[
"^scripts\\/(?:auth-monitor|mobile-reauth|setup-auth-system|",
"termux-(?:auth-widget|quick-auth|sync-widget))\\.sh$|",
"^scripts\\/systemd\\/openclaw-auth-monitor\\.(?:service|timer)$",
].join(""),
"u",
),
["auth-monitor"],
],
[/^scripts\/native-app-i18n\.ts$/u, ["native-app-i18n", workflowGuards]],
[
/^scripts\/github\/(?:dependency-guard|guard-shared)\.mjs$/u,
["dependency-guard-script", "security-review-workflow"],
],
[
/^scripts\/github\/(?:security-sensitive-guard|guard-shared)\.mjs$/u,
["security-sensitive-guard-script", "security-review-workflow"],
],
[
/^\.github\/workflows\/security-review\.yml$/u,
["security-review-workflow", "security-review-event", "security-review-script", workflowGuards],
],
[
/^scripts\/github\/(?:security-review|security-review-rollout)\.mjs$/u,
["security-review-script", "security-review-rollout"],
],
[
/^scripts\/github\/(?:guard-review|security-review-policy)\.mjs$/u,
[
"dependency-guard-script",
"security-sensitive-guard-script",
"security-review-script",
"security-review-rollout",
],
],
[/^scripts\/github\/guard-shared\.mjs$/u, ["security-review-script", "security-review-event"]],
[/^scripts\/plugin-clawhub-release-check\.ts$/u, ["release-wrapper-scripts"]],
[
/^scripts\/generate-runtime-sidecar-paths-baseline\.ts$/u,
["src/plugins/bundled-plugin-metadata.test.ts"],
],
[
/^scripts\/lib\/guard-inventory-utils\.mjs$/u,
[
"test/extension-import-boundaries.test.ts",
"test/plugin-extension-import-boundary.test.ts",
"test/architecture-smells.test.ts",
"test/test-helper-extension-import-boundary.test.ts",
"extension-import-boundary-checker",
"web-fetch-provider-boundary",
"src/plugins/contracts/plugin-sdk-subpaths.test.ts",
],
],
[
/^scripts\/check-workflows\.mts$/u,
["check-composite-action-input-interpolation", "check-no-conflict-markers", workflowGuards],
],
[/^apps\/android\/fastlane\/Fastfile$/u, ["android-release-fastlane-gates"]],
[/^apps\/ios\/fastlane\/Fastfile$/u, ["ios-release-fastlane-gates"]],
[/^scripts\/ios-release-cut\.(?:sh|ts)$/u, ["ios-release-plan"]],
[/^scripts\/ios-release-prepare\.sh$/u, ["ios-release-prepare", "ios-release-wrapper-args"]],
[
/^scripts\/lib\/bundled-runtime-sidecar-paths\.json$/u,
[
"src/plugins/bundled-plugin-metadata.test.ts",
"src/infra/update-global.test.ts",
"src/infra/update-runner.test.ts",
npmPostpublish,
],
],
[/^scripts\/lib\/android-version\.ts$/u, ["android-version", "android-pin-version"]],
[
/^scripts\/lib\/docker-e2e-plan\.(?:mjs|mts)$/u,
[dockerE2e, "docker-all-scheduler", pluginPrerelease],
],
[
new RegExp(
[
"^scripts\\/lib\\/plugin-sdk-(?:deprecated-barrel-subpaths|entrypoints|",
"private-local-only-subpaths)\\.json$",
].join(""),
"u",
),
pluginSdkEntryOwners,
],
[
/^scripts\/lib\/plugin-sdk-deprecated-public-subpaths\.json$/u,
[
"check-deprecated-api-usage",
"src/plugins/contracts/plugin-sdk-package-contract-guardrails.test.ts",
"plugin-sdk-surface-report",
"build-all",
],
],
[/^scripts\/lib\/plugin-sdk-entries\.(?:mjs|mts)$/u, pluginSdkEntryOwners],
[
/^scripts\/lib\/official-external-channel-(?:catalog|seed)\.json$/u,
[
"src/plugins/official-external-plugin-catalog.test.ts",
releaseCheck,
"test/official-channel-catalog.test.ts",
],
],
[
/^scripts\/lib\/official-external-provider-catalog\.json$/u,
["src/plugins/official-external-plugin-catalog.test.ts", releaseCheck],
],
[
/^scripts\/lib\/official-external-plugin-catalog\.json$/u,
["src/plugins/official-external-plugin-catalog.test.ts", releaseCheck],
],
[
/^scripts\/lib\/workspace-bootstrap-smoke\.mts$/u,
[releaseCheck, "test/openclaw-npm-release-check.test.ts"],
],
[/^scripts\/lib\/extension-test-plan\.(?:mjs|mts)$/u, ["test-extension"]],
[
/^scripts\/lib\/recommended-tool-installs\.json$/u,
["src/plugins/recommended-tool-installs.test.ts", releaseCheck],
],
[/^scripts\/docker\/install-sh-common\/version-parse\.sh$/u, [installDocker]],
[
/^scripts\/lib\/local-build-metadata(?:-paths)?\.(?:mjs|mts)$/u,
[
"src/infra/build-stamp.test.ts",
"runtime-postbuild-stamp",
runNode,
"src/infra/package-dist-inventory.test.ts",
releaseCheck,
"test/openclaw-npm-release-check.test.ts",
"check-gateway-watch-regression",
"check-openclaw-package-tarball",
crossOsReleaseChecks,
],
],
[
/^scripts\/lib\/package-dist-imports\.mjs$/u,
[
"check-package-dist-imports",
"check-openclaw-package-tarball",
"postinstall-bundled-plugins",
releaseCheck,
],
],
[
/^scripts\/lib\/build-metadata\.sh$/u,
[
"test/scripts/docker-setup.test.ts",
"apple-release-source-check",
"ios-version",
"package-mac-app",
installDocker,
],
],
[/^scripts\/lib\/plistbuddy\.sh$/u, ["create-dmg", "package-mac-app", "package-mac-dist"]],
[
/^scripts\/lib\/swift-toolchain\.sh$/u,
["package-mac-app", "package-mac-dist", "xcode-test-logs"],
],
[/^scripts\/stage-cua-driver-macos\.sh$/u, ["package-mac-app"]],
[
/^scripts\/(stage-cloudflared-macos\.sh|lib\/cloudflared-macos\.json)$/u,
["stage-cloudflared-macos", "package-mac-app"],
],
[
/^scripts\/lib\/npm-publish-plan\.mjs$/u,
[
"test/npm-publish-plan.test.ts",
"test/openclaw-npm-release-check.test.ts",
npmPostpublish,
"test/plugin-npm-release.test.ts",
"test/plugin-clawhub-release.test.ts",
"release-upgrade-baseline",
"android-version",
"ios-version",
"upgrade-survivor-baselines",
"upgrade-survivor-config-recipe",
],
],
[/^scripts\/lib\/npm-pack-budget\.mts$/u, [releaseCheck, installDocker]],
[
/^scripts\/lib\/actions-artifact-archive\.mjs$/u,
["full-release-candidate-reuse", "plugin-publication-artifact"],
],
[
/^scripts\/(?:lib\/)?full-release-candidate-reuse\.(?:mjs|d\.mts)$/u,
["full-release-candidate-reuse"],
],
[
/^scripts\/lib\/static-extension-assets\.(?:mjs|mts)$/u,
["bundled-plugin-assets", "runtime-postbuild", runNode, "plugin-npm-runtime-build-args"],
],
[
/^scripts\/lib\/plugin-npm-runtime-build\.(?:mjs|mts)$/u,
["plugin-npm-runtime-build-args", "test/plugin-npm-runtime-build.test.ts"],
],
[/^scripts\/lib\/run-node\.(?:mjs|mts)$/u, [runNode]],
[
/^\.agents\/skills\/openclaw-changelog-update\/scripts\/verify-release-notes\.mjs$/u,
["release-notes-ledger", "verify-release-notes"],
],
[
/^scripts\/e2e\/lib\/docker-stats\/assert-resource-ceiling\.mjs$/u,
["docker-stats-resource-ceiling"],
],
[
/^scripts\/e2e\/mcp-channels-docker\.sh$/u,
[dockerBuild, "docker-e2e-observability", dockerE2e, pluginPrerelease],
],
[
/^scripts\/e2e\/cron-mcp-cleanup-docker\.sh$/u,
[dockerBuild, "docker-e2e-observability", dockerE2e, pluginPrerelease],
],
[
/^scripts\/e2e\/(?:mcp-code-mode-gateway-(?:live-)?docker|lib\/mcp-code-mode\/scenario)\.sh$/u,
[
dockerBuild,
dockerE2e,
pluginPrerelease,
"mcp-code-mode-gateway-client",
"session-log-mentions",
],
],
[
/^scripts\/e2e\/agent-bundle-mcp-tools-docker\.sh$/u,
[
dockerBuild,
dockerE2e,
pluginPrerelease,
"src/agents/agent-bundle-mcp-runtime.test.ts",
"src/agents/agent-bundle-mcp-tools.materialize.test.ts",
],
],
[/^scripts\/lib\/tsgo-sparse-guard\.(?:mjs|mts)$/u, ["run-tsgo", "changed-lanes"]],
[
new RegExp(
[
"^(?:scripts\\/materialize-clawhub-cli\\.sh|",
"\\.github\\/release\\/clawhub-cli\\/package(?:-lock)?\\.json)$",
].join(""),
"u",
),
[packageAcceptance, "plugin-clawhub-new-workflow"],
],
[
new RegExp(
[
"^(?:scripts\\/materialize-vercel-cli\\.sh|",
"\\.github\\/release\\/vercel-cli\\/package(?:-lock)?\\.json)$",
].join(""),
"u",
),
["test/scripts/vercel-container-registry-publish.test.ts"],
],
[
/^scripts\/lib\/generated-text-asset\.mts$/u,
["build-diffs-viewer-runtime", "bundled-plugin-assets"],
],
[/^scripts\/check-plugin-npm-runtime-builds\.mts$/u, ["plugin-npm-runtime-build-args"]],
[
/^scripts\/install\.sh$/u,
[
"install-sh",
installDocker,
"website-installer-sync-workflow",
crossOsReleaseChecks,
changedScope,
],
],
[
/^scripts\/sparkle-build\.ts$/u,
["test/appcast.test.ts", releaseCheck, "package-mac-app", "package-mac-dist"],
],
[
/^test\/vitest\/vitest\.contracts-paths\.mjs$/u,
[
"test-projects",
"test/vitest-projects-config.test.ts",
"test/vitest/vitest.contracts-channel-surface.config.ts",
"test/vitest/vitest.contracts-channel-config.config.ts",
"test/vitest/vitest.contracts-channel-registry.config.ts",
"test/vitest/vitest.contracts-channel-session.config.ts",
],
],
[/^scripts\/e2e\/openai-chat-tools-docker\.sh$/u, [openaiChatToolsE2e, dockerE2e]],
[
/^scripts\/e2e\/session-runtime-context-docker\.sh$/u,
[
dockerE2e,
"src/agents/embedded-agent-runner/run/runtime-context-prompt.test.ts",
"src/agents/embedded-agent-runner/transcript-rewrite.test.ts",
],
],
[
new RegExp(
[
"^scripts\\/e2e\\/(?!(?:config-reload-source|",
"kitchen-sink-(?:plugin|rpc)|npm-telegram-live|onboard|openai-chat-tools|",
"plugin-lifecycle-matrix|release-media-memory|session-runtime-context|",
"update-corrupt-plugin)-docker\\.sh$).+-docker\\.sh$",
].join(""),
"u",
),
[dockerBuild, dockerE2e],
],
[/^scripts\/e2e\/kitchen-sink-rpc-docker\.sh$/u, [dockerBuild, pluginPrerelease]],
[/^scripts\/e2e\/kitchen-sink-plugin-docker\.sh$/u, [dockerBuild, pluginPrerelease]],
[/^scripts\/e2e\/plugin-lifecycle-matrix-docker\.sh$/u, [dockerBuild]],
[
/^scripts\/e2e\/release-media-memory-docker\.sh$/u,
[dockerE2e, "release-media-memory-scenario"],
],
[/^scripts\/e2e\/codex-media-path-docker\.sh$/u, ["codex-media-path-client"]],
[/^scripts\/e2e\/live-plugin-tool-docker\.sh$/u, ["live-plugin-tool-assertions"]],
[/^scripts\/e2e\/onboard-docker\.sh$/u, [dockerBuild, "openclaw-test-state"]],
[
new RegExp(
[
"^scripts\\/e2e\\/(?:codex-npm-plugin-live|config-reload-source|",
"gateway-network|npm-onboard-channel-agent|doctor-install-switch|",
"update-channel-switch|skill-install|upgrade-survivor|",
"update-corrupt-plugin|release-(?:plugin-marketplace|typed-onboarding|",
"upgrade-user-journey|user-journey)|plugin-binding-command-escape)-docker",
"\\.sh$",
].join(""),
"u",
),
[packageAcceptance],
],
[
/^scripts\/e2e\/npm-onboard-channel-agent-docker\.sh$/u,
["npm-onboard-channel-agent-assertions"],
],
[/^scripts\/docker\/cleanup-smoke\/Dockerfile$/u, [dockerCache, dockerDigests, dockerBuild]],
[
/^scripts\/docker\/install-sh-(?:e2e|nonroot|smoke)\/Dockerfile$/u,
[dockerCache, dockerDigests, installDocker],
],
[
/^scripts\/docker\/sandbox\/Dockerfile$/u,
[dockerCache, dockerDigests, "src/dockerfile.test.ts"],
],
[
/^scripts\/docker\/sandbox\/Dockerfile\.browser$/u,
[dockerCache, dockerDigests, "src/agents/sandbox/browser.create.test.ts"],
],
[/^scripts\/docker\/sandbox\/Dockerfile\.common$/u, [dockerCache]],
[/^scripts\/e2e\/Dockerfile$/u, [dockerCache, dockerDigests, dockerBuild, dockerE2e]],
[/^scripts\/e2e\/Dockerfile\.qr-import$/u, [dockerCache, dockerDigests, dockerBuild]],
[
/^scripts\/e2e\/plugin-binding-command-escape\.Dockerfile$/u,
[dockerDigests, dockerBuild, dockerE2e],
],
[
new RegExp(
[
"^(?:scripts\\/e2e|test\\/e2e\\/qa-lab\\/runtime)\\/agent-bundle-mcp-tools-doc",
"ker(?:-client)?\\.(?:sh|ts)$",
].join(""),
"u",
),
[
"src/agents/agent-bundle-mcp-runtime.test.ts",
"src/agents/agent-bundle-mcp-tools.materialize.test.ts",
],
],
[
/^test\/e2e\/qa-lab\/runtime\/system-agent-first-run-docker-client\.ts$/u,
[
"src/cli/program/register.onboard.test.ts",
"src/cli/run-main.test.ts",
"src/cli/run-main.exit.test.ts",
"src/commands/system-agent-with-inference.test.ts",
"src/system-agent/assistant.configured.test.ts",
"src/system-agent/assistant.test.ts",
"src/system-agent/system-agent.test.ts",
"src/system-agent/operations.test.ts",
"src/system-agent/overview.test.ts",
"src/system-agent/audit.test.ts",
],
],
[
/^scripts\/e2e\/system-agent-first-run-spec\.json$/u,
["src/system-agent/operations.test.ts", "src/system-agent/audit.test.ts"],
],
[
/^scripts\/e2e\/system-agent-rescue-docker-client\.ts$/u,
[
"src/system-agent/rescue-policy.test.ts",
"src/system-agent/rescue-message.test.ts",
"src/system-agent/operations.test.ts",
"src/system-agent/audit.test.ts",
],
],
[
/^scripts\/e2e\/session-runtime-context-docker(?:-client)?\.(?:sh|ts)$/u,
[
"src/agents/embedded-agent-runner/run/runtime-context-prompt.test.ts",
"src/agents/embedded-agent-runner/transcript-rewrite.test.ts",
],
],
[
new RegExp(
[
"^(?:scripts\\/e2e\\/(?!(?:cron-mcp-cleanup-docker-client\\.ts)$)(?:agent-bu",
"ndle-mcp-tools|mcp-[^/]+-docker|cron-mcp-cleanup-docker|",
"bundled-plugin-install-uninstall|plugin-update-unchanged).+|",
"test\\/e2e\\/qa-lab\\/runtime\\/mcp-channels-docker-client\\.ts)$",
].join(""),
"u",
),
[pluginPrerelease, dockerE2e],
],
[
new RegExp(
[
"^(?:scripts\\/e2e\\/mcp-code-mode-gateway-(?:docker|live-docker|",
"docker-client).+|scripts\\/mcp-code-mode-gateway-e2e\\.ts)$",
].join(""),
"u",
),
["mcp-code-mode-gateway-client", "session-log-mentions"],
],
[
/^scripts\/e2e\/(?:mcp-channels|cron-cli|cron-mcp-cleanup)-docker\.sh$/u,
["docker-e2e-observability"],
],
[
/^scripts\/e2e\/cron-mcp-cleanup-docker-client\.ts$/u,
[
"cron-mcp-cleanup-docker-client",
"src/gateway/server.cron.test.ts",
"src/gateway/server-methods/agent.test.ts",
"src/cron/isolated-agent/run.fast-mode.test.ts",
"src/cron/active-jobs-manual-run.test.ts",
],
],
[/^scripts\/e2e\/cron-mcp-cleanup-docker\.sh$/u, ["cron-mcp-cleanup-docker-client"]],
[
/^test\/e2e\/qa-lab\/runtime\/mcp-channels\.fixture\.ts$/u,
["test/e2e/qa-lab/runtime/mcp-gateway-transport.e2e.test.ts", "cron-mcp-cleanup-docker-client"],
],
[
/^scripts\/e2e\/lib\/auth-profile-store-assertions\.mjs$/u,
["release-scenarios-assertions", "npm-onboard-channel-agent-assertions"],
],
[
/^scripts\/lib\/plugin-npm-package-manifest\.(?:mjs|mts)$/u,
["plugin-npm-package-manifest-args", "test/plugin-npm-package-manifest.test.ts"],
],
[
/^scripts\/e2e\/lib\/release-assertion-files\.mjs$/u,
["release-scenarios-assertions", "release-user-journey-assertions"],
],
[/^scripts\/e2e\/lib\/fixtures\/config\.mjs$/u, ["fixture-config"]],
[/^scripts\/e2e\/lib\/fixtures\/plugins\.mjs$/u, ["fixture-plugin-commands"]],
[
/^scripts\/e2e\/lib\/codex-app-server-fixture\.mjs$/u,
["codex-media-path-client", "test/e2e/qa-lab/runtime/codex-auth-product-proof.e2e.test.ts"],
],
[
/^scripts\/e2e\/lib\/incremental-line-reader\.mjs$/u,
[
"incremental-line-reader",
"config-reload-log-scanner",
"codex-media-path-client",
"test/e2e/qa-lab/runtime/codex-auth-product-proof.e2e.test.ts",
],
],
[
/^scripts\/e2e\/lib\/fixture\.mjs$/u,
["fixture-config", "fixtures-workspace", "fixture-plugin-commands"],
],
[
/^test\/e2e\/qa-lab\/runtime\/openai-image-auth-docker-client\.ts$/u,
[
"openai-image-auth-docker-client",
"extensions/openai/image-generation-provider.test.ts",
"src/image-generation/openai-compatible-image-provider.test.ts",
],
],
[
/^test\/e2e\/qa-lab\/runtime\/codex-auth-app-server\.fixture\.mjs$/u,
["test/e2e/qa-lab/runtime/codex-auth-product-proof.e2e.test.ts"],
],
[
/^scripts\/e2e\/lib\/openai-web-search-minimal\/(?:mock-server\.mjs|scenario\.sh)$/u,
[
"test/e2e/qa-lab/runtime/openai-web-search-minimal.e2e.test.ts",
"test/e2e/qa-lab/runtime/openai-web-search-minimal-assertions.e2e.test.ts",
],
],
[
/^scripts\/e2e\/lib\/openwebui\/http-probe\.mjs$/u,
["test/e2e/qa-lab/runtime/openwebui-probe.e2e.test.ts"],
],
[
/^scripts\/e2e\/(?:browser-cdp-snapshot-docker\.sh|lib\/env-limits\.mjs)$/u,
["e2e-helper-env-limits"],
],
[/^scripts\/e2e\/skill-install-docker\.sh$/u, ["e2e-shell-tempfiles"]],
[/^scripts\/e2e\/npm-onboard-channel-agent-docker\.sh$/u, [pluginPrerelease]],
[
/^scripts\/e2e\/lib\/clawhub-fixture-server\.cjs$/u,
["clawhub-fixture-server", pluginPrerelease],
],
[
/^scripts\/e2e\/lib\/codex-media-path\/jsonl-request-tail\.mts$/u,
["codex-media-path-client", "test/e2e/qa-lab/runtime/codex-auth-product-proof.e2e.test.ts"],
],
[
/^scripts\/e2e\/lib\/codex-npm-plugin-live\/assertions\.mjs$/u,
["codex-install-assertions", dockerBuild],
],
[
/^scripts\/e2e\/(?:lib\/openai-chat-tools\/scenario\.sh|openai-chat-tools-docker\.sh)$/u,
[openaiChatToolsE2e],
],
[/^scripts\/e2e\/lib\/openai-chat-tools\/write-config\.mjs$/u, [openaiChatToolsE2e]],
[
/^scripts\/e2e\/openai-web-search-minimal-docker\.sh$/u,
[
"test/e2e/qa-lab/runtime/openai-web-search-minimal.e2e.test.ts",
"test/e2e/qa-lab/runtime/openai-web-search-minimal-assertions.e2e.test.ts",
],
],
[
/^scripts\/e2e\/openwebui-docker\.sh$/u,
["test/e2e/qa-lab/runtime/openwebui-probe.e2e.test.ts", "fixture-config"],
],
];
function resolveSemanticToolingTargets(changedPath: string) {
return SEMANTIC_TOOLING_TARGET_PATTERNS.flatMap(([pattern, owners]) =>
pattern.test(changedPath) ? resolveToolingTestOwnerTargets(...owners) : [],
);
}
function isWorkflowLintConfigPath(changedPath: string) {
return changedPath === ".github/actionlint.yaml";
}
function isGithubWorkflowOrActionYaml(changedPath: string) {
return (
/^\.github\/workflows\/[^/]+\.ya?ml$/u.test(changedPath) ||
/^\.github\/actions\/.+\.ya?ml$/u.test(changedPath)
);
}
function resolveGithubYamlGuardTargets(changedPath: string) {
if (isGithubWorkflowOrActionYaml(changedPath)) {
return GITHUB_YAML_PINNING_GUARD_TEST_TARGETS;
}
return null;
}
function resolveDirectToolingReferenceTests(changedPath: string | string[], cwd: string) {
const changedPaths = typeof changedPath === "string" ? [changedPath] : changedPath;
const matches = listImportGraphGrepMatches(cwd, changedPaths, {
tooling: true,
testFilesOnly: true,
});
return changedPaths.flatMap((filePath) =>
(matches.get(filePath) ?? [])
.filter(
({ file, references }) =>
file !== "test/scripts/test-projects.test.ts" &&
!file.endsWith(".live.test.ts") &&
isTestFileTarget(file) &&
references.has(filePath),
)
.map(({ file }) => file),
);
}
function hasToolingSourceOwner(changedPath: string, implementationPath: string): boolean {
const facts = getChangedPathFacts(changedPath);
return (
facts.surface === "rootTooling" ||
changedPath === "Dockerfile" ||
changedPath === ".crabbox.yaml" ||
changedPath.startsWith(".agents/") ||
isToolingScriptPath(implementationPath) ||
(facts.surface === "app" && /\/(?:fastlane|scripts)\//u.test(changedPath)) ||
(facts.surface === "extension" && /\/(?:scripts\/|package\.json$)/u.test(changedPath)) ||
(facts.surface === "rootTest" && changedPath.startsWith("test/e2e/qa-lab/"))
);
}
/** Inputs that retain the full maintainer-tooling family in automatic CI. */
export function isToolingTestOwnerPath(changedPath: string): boolean {
const implementationPath = changedPath.endsWith(".d.mts")
? changedPath.replace(/\.d\.mts$/u, ".mjs")
: changedPath;
const facts = getChangedPathFacts(changedPath);
return (
isToolingIsolatedTestFile(changedPath) ||
changedPath.startsWith("scripts/") ||
changedPath.startsWith("src/scripts/") ||
changedPath.startsWith("config/ci-") ||
changedPath.startsWith(".github/") ||
changedPath.startsWith("test/scripts/") ||
facts.surface === "rootGlobal" ||
facts.surface === "rootTest" ||
facts.surface === "testFixture" ||
facts.surface === "legacyRootAsset" ||
facts.surface === "unknown" ||
EXACT_TOOLING_TARGETS.has(implementationPath) ||
resolveSemanticToolingTargets(implementationPath).length > 0 ||
hasToolingSourceOwner(changedPath, implementationPath)
);
}
function resolveToolingTestTargets(
changedPath: string,
cwd = process.cwd(),
options: ChangedTestTargetOptions = {},
) {
if (
/^test\/scripts\/(?:ci-(?:checkout|git-owner|linux-git|platform-checkout|windows-process-census)\.test(?:-support)?\.ts|generated-publisher\.test-support\.ts|openclaw-performance-(?:workflow\.test(?:-support)?|git-lifecycle\.test)\.ts|plugin-release-git-lifecycle\.test\.ts|release-workflow-git-lifecycle\.test\.ts|fixtures\/(?:ci-platform-checkout\.mjs|ci-checkout-auth\.py|ci-windows-process-census\.(?:mjs|py)))$/u.test(
changedPath,
)
) {
return resolveToolingTestOwnerTargets(
"ci-git-owner",
"ci-linux-git",
"ci-platform-checkout",
"openclaw-performance-workflow",
"openclaw-performance-git-lifecycle",
"plugin-release-git-lifecycle",
"release-workflow-git-lifecycle",
workflowGuards,
);
}
if (changedPath.startsWith("test/scripts/") && isTestFileTarget(changedPath)) {
return [changedPath];
}
if (
!options.boundedOwners &&
BROAD_CHANGED_FALLBACK_PATTERNS.some((pattern) => pattern.test(changedPath))
) {
return null;
}
// Test-runner declarations still share their implementation owner. Script
// declaration sidecars are gone, so this no longer aliases scripts/*.d.mts.
const implementationPath =
!changedPath.startsWith("scripts/") && changedPath.endsWith(".d.mts")
? changedPath.replace(/\.d\.mts$/u, ".mjs")
: changedPath;
const githubYaml = isGithubWorkflowOrActionYaml(implementationPath);
const exactOwners = EXACT_TOOLING_TARGETS.get(implementationPath);
if (exactOwners && !githubYaml && !options.boundedOwners) {
return resolveToolingTestOwnerTargets(...exactOwners);
}
const boundedOwner = (target: string) =>
!options.boundedOwners || target !== TOOLING_VITEST_CONFIG;
const exactTargets = exactOwners
? resolveToolingTestOwnerTargets(...exactOwners).filter(boundedOwner)
: [];
const semanticTargets = resolveSemanticToolingTargets(implementationPath).filter(boundedOwner);
const facts = getChangedPathFacts(changedPath);
const toolingTestSource =
changedPath.startsWith("test/scripts/") &&
TOOLING_IMPORTABLE_FILE_EXTENSIONS.some((ext) => implementationPath.endsWith(ext));
const hasToolingOwner =
exactTargets.length > 0 ||
semanticTargets.length > 0 ||
toolingTestSource ||
hasToolingSourceOwner(changedPath, implementationPath);
if (!hasToolingOwner) {
return null;
}
const crossOsReleaseTargets =
implementationPath === "scripts/openclaw-cross-os-release-checks.ts" ||
implementationPath.startsWith("scripts/lib/cross-os-release-checks/")
? ["test/scripts/openclaw-cross-os-release-checks.test.ts"]
: null;
const explicitTargets =
(changedPath === "Dockerfile"
? [
"src/docker-build-cache.test.ts",
"src/docker-image-digests.test.ts",
"src/dockerfile.test.ts",
"test/scripts/test-install-sh-docker.test.ts",
]
: changedPath === ".crabbox.yaml"
? ["test/scripts/package-acceptance-workflow.test.ts"]
: null) ??
crossOsReleaseTargets ??
resolveUpgradeSurvivorConfigRecipeTargets(implementationPath) ??
resolveDocsI18nBehaviorTargets(implementationPath) ??
resolveDocsI18nGoTargets(implementationPath) ??
resolveK8sManifestTargets(implementationPath) ??
resolveParallelsToolingTestTargets(implementationPath);
const githubYamlGuardTargets = resolveGithubYamlGuardTargets(implementationPath);
const conventionalTargets = resolveConventionalToolingTestTargets(implementationPath, cwd);
const hasDirectOwner = Boolean(
exactTargets.length ||
explicitTargets?.length ||
githubYamlGuardTargets?.length ||
semanticTargets.length ||
conventionalTargets?.length,
);
const importGraphResult =
!options.boundedOwners &&
!hasDirectOwner &&
(options.forceFullImportGraph ||
TOOLING_IMPORTABLE_FILE_EXTENSIONS.some((ext) => implementationPath.endsWith(ext)))
? options.forceFullImportGraph
? resolveAffectedTestsFromImportGraph(implementationPath, cwd, {
tooling: true,
forceFull: true,
resolveAliases: options.resolveAliases,
runtimeOnly: options.runtimeOnly,
})
: resolveAffectedTestsFromTargetedImportScan(implementationPath, cwd, {
tooling: true,
direct: !toolingTestSource,
})
: [];
if (toolingTestSource && importGraphResult === null) {
// Keep caller fallbacks; a partial literal reference cannot prove an opaque frontier.
return null;
}
const importGraphTargets = importGraphResult ?? [];
const referenceTargets =
githubYaml || (semanticTargets.length === 0 && !hasDirectOwner)
? resolveDirectToolingReferenceTests(implementationPath, cwd)
: [];
const targets = [
...(!hasDirectOwner && isRoutableChangedTarget(changedPath) && isTestFileTarget(changedPath)
? [changedPath]
: []),
...exactTargets,
...(explicitTargets ?? []),
...semanticTargets,
...(conventionalTargets ?? []),
...importGraphTargets,
...referenceTargets,
...(githubYamlGuardTargets ?? []),
// Root aliases also control native bundling; keep the existing tooling owners.
...(changedPath === "tsconfig.json" ? MERMAID_RENDERER_TEST_TARGETS : []),
];
if (targets.length > 0 || isWorkflowLintConfigPath(implementationPath)) {
return uniqueOrdered(targets);
}
return !options.boundedOwners &&
(isToolingScriptPath(implementationPath) || facts.surface === "rootTooling")
? [TOOLING_VITEST_CONFIG]
: null;
}
function shouldUseBroadChangedTargets(env = process.env) {
return parsePermissiveBooleanToken(env[BROAD_CHANGED_ENV_KEY]) === true;
}
function isRoutableChangedTarget(changedPath: string) {
if (GENERATED_CHANGED_TEST_TARGET_PATTERNS.some((pattern) => pattern.test(changedPath))) {
return false;
}
if (changedPath.endsWith(".live.test.ts")) {
return false;
}
const surface = getChangedPathFacts(changedPath).surface;
return (
["source", "package", "extension", "rootTest"].includes(surface) ||
changedPath === "ui" ||
changedPath.startsWith("ui/") ||
["src", "test", "extensions", "packages"].includes(changedPath)
);
}
function resolveSiblingTestTarget(changedPath: string, cwd: string) {
if (!/\.[cm]?tsx?$/u.test(changedPath) || isTestFileTarget(changedPath)) {
return null;
}
const withoutExtension = changedPath.replace(/\.[cm]?tsx?$/u, "");
const sibling = `${withoutExtension}.test.ts`;
return fs.existsSync(path.join(cwd, sibling)) ? sibling : null;
}
function shouldCombineSiblingTestWithImportGraph(changedPath: string) {
const sourcePrefix = `${changedPath.replace(/\.[cm]?tsx?$/u, "")}.`;
return (
changedPath.startsWith("test/helpers/") ||
databaseWorkerExtensionTestFiles.some((file) => file.startsWith(sourcePrefix))
);
}
function shouldRouteChangedTargetWithoutImportGraph(changedPath: string) {
return changedPath.endsWith(".live.test.ts") || isControlUiSourcePath(changedPath);
}
function resolvePromptSnapshotFixtureTargets(changedPath: string) {
if (
!/^test\/fixtures\/agents\/prompt-snapshots\/.+\.(?:json|md(?:\.diff)?)$/u.test(changedPath)
) {
return null;
}
return ["test/scripts/prompt-snapshots.test.ts"];
}
function resolvePackageFixtureTargets(changedPath: string, cwd: string) {
const match = /^packages\/([^/]+)\/test\/fixtures\/([^/]+)\/.+$/u.exec(changedPath);
const packageName = match?.[1];
const fixtureFamily = match?.[2];
if (!packageName || !fixtureFamily) {
return null;
}
const owner = `packages/${packageName}/src/${fixtureFamily}.test.ts`;
return fs.existsSync(path.join(cwd, owner)) ? [owner] : null;
}
function resolveAppcastTargets(changedPath: string) {
return /^appcast(?:-(?:arm64|x86_64))?\.xml$/u.test(changedPath) ? APPCAST_TEST_TARGETS : null;
}
function resolveKovaSchemaTestTargets(changedPath: string) {
// The workflow fixture reads schema bytes, so imports cannot express this dependency.
return changedPath === "src/config/zod-schema.agent-defaults.ts" ||
changedPath === "src/config/zod-schema.agent-defaults-base.ts"
? ["test/scripts/openclaw-performance-workflow.test.ts"]
: [];
}
function resolvePreciseChangedTestTargets(
changedPath: string,
options: ChangedTestTargetOptions & { skipImportGraph?: boolean },
) {
const cwd = options.cwd ?? process.cwd();
const pluginSdkInclude = resolvePluginSdkLightIncludePattern(changedPath);
const mappedTargets =
SOURCE_TEST_TARGETS.get(changedPath) ??
(pluginSdkInclude
? pluginSdkLightTestFiles.filter((file) => path.matchesGlob(file, pluginSdkInclude))
: null) ??
(/^extensions\/[^/]+\/openclaw\.plugin\.json$/u.test(changedPath)
? [changedPath, DOCS_CONFIG_EXAMPLES_TEST_TARGET]
: null) ??
resolveToolingTestTargets(changedPath, cwd, options) ??
resolveAppcastTargets(changedPath) ??
resolvePromptSnapshotFixtureTargets(changedPath) ??
resolvePackageFixtureTargets(changedPath, cwd);
if (mappedTargets) {
return mappedTargets;
}
if (isRoutableChangedTarget(changedPath) && isTestFileTarget(changedPath)) {
return [changedPath];
}
const siblingTest = resolveSiblingTestTarget(changedPath, cwd);
if (
siblingTest &&
!shouldCombineSiblingTestWithImportGraph(changedPath) &&
options.combineSiblingWithImportGraph !== true
) {
return [siblingTest];
}
if (!options.boundedOwners && shouldRouteChangedTargetWithoutImportGraph(changedPath)) {
return isControlUiSourcePath(changedPath) ? [changedPath] : null;
}
if (options.skipImportGraph === true) {
return options.boundedOwners && siblingTest ? [siblingTest] : null;
}
const facts = getChangedPathFacts(changedPath);
if (
facts.surface === "source" ||
facts.surface === "package" ||
facts.surface === "extension" ||
changedPath.startsWith("test/helpers/") ||
changedPath.startsWith("ui/src/") ||
changedPath.startsWith("ui/config/")
) {
const affectedTests = resolveAffectedTestsFromImportGraph(changedPath, cwd, {
forceFull: options.forceFullImportGraph === true,
resolveAliases: options.resolveAliases,
runtimeOnly: options.runtimeOnly,
});
if (affectedTests.length > 0) {
return siblingTest ? uniqueOrdered([siblingTest, ...affectedTests]) : affectedTests;
}
}
return siblingTest ? [siblingTest] : null;
}
function isDeletedChangedTestTarget(changedPath: string, cwd: string) {
return isTestFileTarget(changedPath) && !fs.existsSync(path.join(cwd, changedPath));
}
function resolveConventionalChangedOwnerTargets(changedPath: string, cwd: string) {
if (!isRoutableChangedTarget(changedPath) || changedPath.endsWith(".live.test.ts")) {
return [];
}
const stem = changedPath.replace(/\.[cm]?[jt]sx?$/u, "");
const siblings = listImportGraphFilesForCwd(cwd, { tooling: true }).filter(
(file) =>
file.startsWith(`${stem}.`) && isTestFileTarget(file) && !file.endsWith(".live.test.ts"),
);
if (siblings.length > 0) {
return siblings;
}
if (shouldKeepBroadChangedRun([changedPath])) {
return [];
}
const directory = path.posix.dirname(changedPath);
// Root leaves have no narrower conventional owner; their importers and smoke
// coverage must not turn into a whole-repository directory selection.
return [".", "src", "test", "extensions", "packages", "ui"].includes(directory)
? []
: [directory];
}
const TOOLING_TEST_OWNER_AREAS = ["scripts", "src/scripts", "test/scripts"];
function resolveChangedTestOwnerAreas(targets: string[], cwd: string): string[] {
return uniqueOrdered(
targets.flatMap((target) => {
if (RUNNABLE_VITEST_CONFIG_TARGETS.has(target) || isGlobTarget(target)) {
return [];
}
if (isExistingDirectoryTarget(target, cwd) || !isLikelyFileTarget(target)) {
return [target];
}
if (
target.startsWith("scripts/") ||
target.startsWith("src/scripts/") ||
target.startsWith("test/scripts/")
) {
return TOOLING_TEST_OWNER_AREAS;
}
const { surface } = getChangedPathFacts(target);
if (surface === "ui") {
return ["ui"];
}
const parts = target.split("/");
if (
(surface === "source" || surface === "package" || surface === "extension") &&
parts.length > 2
) {
return [parts.slice(0, 2).join("/")];
}
return [];
}),
);
}
function resolveBoundedChangedTestTargetPlan(
changedPaths: string[],
options: ChangedTestTargetOptions,
): ChangedTestTargetPlan {
const cwd = options.cwd ?? process.cwd();
const targets: string[] = [];
const ownerTargets: string[] = [];
const ownerAreas: string[] = [];
const graphOptions = {
tooling: true,
resolveAliases: options.resolveAliases,
runtimeOnly: options.runtimeOnly,
};
const onlyTestPaths = changedPaths.every(isTestFileTarget);
const testsHaveConsumers =
onlyTestPaths && hasImportGraphConsumers(changedPaths, cwd, graphOptions);
let graph: ImportGraph | undefined;
for (const changedPath of changedPaths) {
const mappedOwners = resolvePreciseChangedTestTargets(changedPath, {
...options,
skipImportGraph: true,
})?.flatMap((target) =>
!isTestFileTarget(target) &&
!isGlobTarget(target) &&
!RUNNABLE_VITEST_CONFIG_TARGETS.has(target) &&
!isExistingDirectoryTarget(target, cwd) &&
isLikelyFileTarget(target)
? resolveConventionalChangedOwnerTargets(target, cwd)
: [target],
);
const pluginPackageOwner =
getChangedPathFacts(changedPath).surface === "extension" &&
path.posix.basename(changedPath) === "package.json"
? resolveConventionalChangedOwnerTargets(changedPath, cwd)
: [];
const explicitOwners = uniqueOrdered([...(mappedOwners ?? []), ...pluginPackageOwner]);
const owners =
explicitOwners.length > 0
? explicitOwners
: resolveConventionalChangedOwnerTargets(changedPath, cwd);
const hasExplicitScope = explicitOwners.some(
(owner) => isGlobTarget(owner) || !isTestFileTarget(owner),
);
const areas = resolveChangedTestOwnerAreas(
hasExplicitScope ? owners : [changedPath, ...owners],
cwd,
);
const broadInput = shouldKeepBroadChangedRun([changedPath]);
if (
owners.length === 0 &&
areas.length === 0 &&
(broadInput || isToolingTestOwnerPath(changedPath))
) {
areas.push(...TOOLING_TEST_OWNER_AREAS);
}
let directImporters = new Set<string>();
let affectedTests: string[] = [];
if (!onlyTestPaths || testsHaveConsumers) {
// Deleted and non-source inputs share one resolution universe for the whole plan.
graph ??= getImportGraph(cwd, graphOptions, changedPaths);
directImporters = new Set(walkAffectedTestsFromImportGraph([changedPath], graph, true));
affectedTests = walkAffectedTestsFromImportGraph([changedPath], graph);
}
// Direct readers keep their coverage without broadening the owner's transitive area.
const importers = affectedTests.filter(
(file) =>
directImporters.has(file) ||
(!broadInput && owners.length === 0 && areas.length === 0) ||
areas.some((area) => isPathAtOrUnder(file, area)) ||
owners.some((owner) => owner === file || path.matchesGlob(file, owner)),
);
ownerTargets.push(...owners);
if (!isTestFileTarget(changedPath)) {
ownerAreas.push(...areas);
}
targets.push(...owners, ...importers);
if (CHANNEL_PLUGIN_SHAPE_PARITY_WIRING_PATHS.has(changedPath)) {
targets.push(CHANNEL_PLUGIN_SHAPE_PARITY_TEST_TARGET);
}
}
return {
mode: "targets",
ownerTargets: uniqueOrdered(ownerTargets),
ownerAreas: uniqueOrdered(ownerAreas),
targets: uniqueOrdered([
...targets,
...(options.watchMode ? [] : changedPaths.flatMap(resolveKovaSchemaTestTargets)),
]),
};
}
/**
* Maps changed repo paths to the smallest useful Vitest target plan.
*/
export function resolveChangedTestTargetPlan(
changedPaths: string[],
options: ChangedTestTargetOptions = {},
): ChangedTestTargetPlan {
if (changedPaths.length === 0) {
return { mode: "none", targets: [] };
}
const cwd = options.cwd ?? process.cwd();
if (options.boundedOwners) {
return resolveBoundedChangedTestTargetPlan(changedPaths, options);
}
const executableChangedPaths = changedPaths.filter(
(changedPath) => !isDeletedChangedTestTarget(changedPath, cwd),
);
const toolingTargets = resolveToolingChangedTestTargets(executableChangedPaths, cwd, options);
if (toolingTargets) {
return { mode: "targets", targets: toolingTargets };
}
const changedLanes = detectChangedLanes(executableChangedPaths);
const env = options.env ?? {};
const useBroadFallback = options.broad ?? shouldUseBroadChangedTargets(env);
const skipImportGraph = changedLanes.lanes.all && !useBroadFallback;
const targets = [];
const skippedBroadFallbackPaths = [];
for (const changedPath of executableChangedPaths) {
const needsPluginShapeParity = CHANNEL_PLUGIN_SHAPE_PARITY_WIRING_PATHS.has(changedPath);
const preciseTargets = resolvePreciseChangedTestTargets(changedPath, {
...options,
skipImportGraph,
});
if (preciseTargets) {
targets.push(...preciseTargets);
if (needsPluginShapeParity) {
targets.push(CHANNEL_PLUGIN_SHAPE_PARITY_TEST_TARGET);
}
continue;
}
const needsBroadFallback = shouldKeepBroadChangedRun([changedPath]) || changedLanes.lanes.all;
if (needsBroadFallback) {
if (useBroadFallback) {
return { mode: "broad", targets: [] };
}
skippedBroadFallbackPaths.push(changedPath);
continue;
}
if (isRoutableChangedTarget(changedPath)) {
targets.push(changedPath);
}
if (needsPluginShapeParity) {
targets.push(CHANNEL_PLUGIN_SHAPE_PARITY_TEST_TARGET);
}
}
if (
useBroadFallback &&
options.includeExtensionImpact !== false &&
changedLanes.extensionImpactFromCore
) {
targets.push("extensions");
}
const plan: ChangedTestTargetPlan = {
mode: "targets",
targets: uniqueOrdered([
...targets,
...(options.watchMode ? [] : executableChangedPaths.flatMap(resolveKovaSchemaTestTargets)),
]),
};
if (skippedBroadFallbackPaths.length > 0) {
plan.skippedBroadFallbackPaths = [...new Set(skippedBroadFallbackPaths)];
}
return plan;
}
export function listFullExtensionVitestProjectConfigs() {
return (
fullSuiteVitestShards.find((shard) => shard.config === FULL_EXTENSIONS_VITEST_CONFIG)
?.projects ?? []
);
}
export function resolveChangedTargetArgs(
args: string[],
cwd = process.cwd(),
listChangedPaths: (baseRef: string, cwd: string) => string[] = listChangedPathsFromGit,
options: ChangedTestTargetOptions = {},
) {
const plan = resolveChangedTestTargetPlanForArgs(args, cwd, listChangedPaths, options);
if (!plan) {
return null;
}
if (plan.mode === "broad") {
return null;
}
return plan.targets;
}
export function resolveChangedTestTargetPlanForArgs(
args: string[],
cwd = process.cwd(),
listChangedPaths: (baseRef: string, cwd: string) => string[] = listChangedPathsFromGit,
options: ChangedTestTargetOptions = {},
) {
const baseRef = extractChangedBaseRef(args);
if (!baseRef) {
return null;
}
const changedPaths = listChangedPaths(baseRef, cwd);
return resolveChangedTestTargetPlan(changedPaths, {
cwd,
...options,
watchMode: parseTestProjectsArgs(args, cwd).watchMode,
});
}
function classifyTarget(arg: string, cwd: string, beforeDatabaseWorkerOwnership = false) {
const relative = toRepoRelativeTarget(arg, cwd);
if (!beforeDatabaseWorkerOwnership && databaseWorkerExtensionTestFiles.includes(relative)) {
return "extensionDatabaseWorkers";
}
const configTargetKind = resolveVitestConfigTargetKind(relative);
if (configTargetKind) {
return configTargetKind;
}
if (
gatewayPluginTestFiles.includes(relative) &&
(beforeDatabaseWorkerOwnership || !gatewayDatabaseWorkerTestFiles.includes(relative))
) {
return "gatewayMethods";
}
if (beforeDatabaseWorkerOwnership) {
const formerFastKind = databaseWorkerCoreFormerFastKinds.get(relative);
if (formerFastKind) {
return formerFastKind;
}
} else if (isDatabaseWorkerCoreTestFile(relative)) {
return "infra";
}
if (gatewayDatabaseWorkerTestFiles.includes(relative)) {
return beforeDatabaseWorkerOwnership ? "gateway" : "gatewayDatabaseWorkers";
}
if (isAgentsCoreIsolatedTestFile(relative)) {
return agentVitestProjectOwners.coreIsolated.kind;
}
if (isAgentsSpawnProductionBoundaryTestFile(relative)) {
return agentVitestProjectOwners.spawnProductionBoundary.kind;
}
if (isControlUiE2eTarget(relative)) {
return "uiE2e";
}
if (packageContractTestFiles.includes(relative)) {
return "packageContract";
}
if (isUiIsolatedTestFile(relative)) {
return "uiIsolated";
}
if (uiTimingTestFiles.includes(relative)) {
return "uiTiming";
}
if (isUiBrowserTestFile(relative)) {
return "uiBrowser";
}
if (isPathAtOrUnder(relative, "ui") || isPluginControlUiPath(relative)) {
return "ui";
}
if (relative.startsWith("src/tui/tui-pty-") || tuiPtyTestFiles.includes(relative)) {
return "tuiPty";
}
if (relative.endsWith(".e2e.test.ts")) {
return "e2e";
}
if (
relative === "src/gateway/gateway.test.ts" ||
relative === "src/gateway/server.startup-matrix-migration.integration.test.ts"
) {
return "e2e";
}
const channelContractKind = resolveChannelContractTargetKind(relative);
if (channelContractKind) {
return channelContractKind;
}
if (relative.startsWith("src/plugins/contracts/")) {
return "contractsPlugin";
}
// These tests share stateful runner mocks and must keep the dedicated serial
// owner even when their contents also qualify for a unit-fast lane.
if (agentVitestProjectOwners.embeddedIncompleteTurn.include.includes(relative)) {
return agentVitestProjectOwners.embeddedIncompleteTurn.kind;
}
// Explicit isolation ownership wins over inferred unit-fast eligibility.
// Otherwise a thin wrapper can move a stateful tooling test into a shared worker.
if (isToolingIsolatedTestFile(relative)) {
return "toolingIsolated";
}
if (isCliProcessTestFile(relative)) {
return "cliProcess";
}
if (resolveUnitFastTimerTestIncludePattern(relative)) {
return "unitFastFakeTimers";
}
if (resolveUnitFastIsolatedTestIncludePattern(relative)) {
return "unitFastIsolated";
}
if (resolveUnitFastTestIncludePattern(relative)) {
return "unitFast";
}
if (relative === "extensions") {
return "extensionFull";
}
if (getChangedPathFacts(relative).surface === "extension") {
const extensionRoot = relative.split("/").slice(0, 2).join("/");
return VITEST_CONFIG_TARGET_KIND_BY_PATH.get(resolveExtensionTestConfig(extensionRoot))!;
}
if (isChannelSurfaceTestFile(relative)) {
return "channel";
}
if (isBoundaryTestFile(relative)) {
return "boundary";
}
if (relative === TOOLING_DOCKER_TEST_TARGET) {
return "toolingDocker";
}
if (
relative.startsWith("test/") ||
relative === "src/scripts" ||
relative.startsWith("src/scripts/") ||
relative === "src/config/doc-baseline.integration.test.ts" ||
relative === "src/config/schema.base.generated.test.ts" ||
relative === "src/config/schema.help.quality.test.ts"
) {
return "tooling";
}
if (isBundledPluginDependentUnitTestFile(relative)) {
return "bundled";
}
if (isPathAtOrUnder(relative, "src/channels")) {
return "channel";
}
if (isPathAtOrUnder(relative, "src/gateway")) {
return !beforeDatabaseWorkerOwnership &&
!isGlobTarget(relative) &&
isGatewayServerTestFile(relative)
? "gatewayServer"
: "gateway";
}
if (
isPathAtOrUnder(relative, "packages/gateway-client") ||
isPathAtOrUnder(relative, "packages/gateway-protocol")
) {
return "gatewayClient";
}
if (isPathAtOrUnder(relative, "src/hooks")) {
return "hooks";
}
if (isPathAtOrUnder(relative, "src/infra")) {
return "infra";
}
if (isPathAtOrUnder(relative, "src/config")) {
return "runtimeConfig";
}
if (isPathAtOrUnder(relative, "src/cron")) {
return "cron";
}
if (isPathAtOrUnder(relative, "src/daemon")) {
return "daemon";
}
if (isPathAtOrUnder(relative, "src/media-understanding")) {
return "mediaUnderstanding";
}
if (isPathAtOrUnder(relative, "src/media")) {
return "media";
}
if (isPathAtOrUnder(relative, "src/logging")) {
return "logging";
}
if (isPathAtOrUnder(relative, "src/plugin-sdk")) {
return isPluginSdkLightTarget(relative) ? "pluginSdkLight" : "pluginSdk";
}
if (isPathAtOrUnder(relative, "src/process")) {
return "process";
}
if (isPathAtOrUnder(relative, "src/secrets")) {
return "secrets";
}
if (isPathAtOrUnder(relative, "src/shared")) {
return "sharedCore";
}
if (isPathAtOrUnder(relative, "src/tui")) {
return "tui";
}
if (isPathAtOrUnder(relative, "src/acp")) {
return "acp";
}
if (isPathAtOrUnder(relative, "src/cli")) {
return "cli";
}
if (isPathAtOrUnder(relative, "src/commands")) {
return isCommandsLightTarget(relative) ? "commandLight" : "command";
}
if (isPathAtOrUnder(relative, "src/auto-reply")) {
return "autoReply";
}
if (isPathAtOrUnder(relative, agentVitestProjectOwners.all.root)) {
// Focused runs must preserve the full suite's isolated harness and hook-timeout contracts.
if (
relative === agentVitestProjectOwners.all.root ||
relative === AGENTS_EMBEDDED_AGENT_TEST_ROOT
) {
return agentVitestProjectOwners.all.kind;
}
if (agentVitestProjectOwners.embeddedOverflowCompaction.include.includes(relative)) {
return agentVitestProjectOwners.embeddedOverflowCompaction.kind;
}
if (isPathAtOrUnder(relative, agentVitestProjectOwners.embeddedRun.root)) {
return agentVitestProjectOwners.embeddedRun.kind;
}
if (isPathAtOrUnder(relative, AGENTS_EMBEDDED_AGENT_TEST_ROOT)) {
return isGlobTarget(relative)
? agentVitestProjectOwners.all.kind
: agentVitestProjectOwners.embedded.kind;
}
if (isPathAtOrUnder(relative, agentVitestProjectOwners.tools.root)) {
return agentVitestProjectOwners.tools.kind;
}
if (isGlobTarget(relative)) {
const owner =
relative.slice(agentVitestProjectOwners.all.root.length + 1).split("/", 1)[0] ?? "";
return isGlobTarget(owner)
? agentVitestProjectOwners.all.kind
: agentVitestProjectOwners.support.kind;
}
return isTestFileTarget(relative) &&
path.posix.dirname(relative) === agentVitestProjectOwners.core.root
? agentVitestProjectOwners.core.kind
: agentVitestProjectOwners.support.kind;
}
if (isPathAtOrUnder(relative, "src/plugins")) {
return "plugin";
}
if (isPathAtOrUnder(relative, "src/utils")) {
return "utils";
}
if (isPathAtOrUnder(relative, "src/wizard")) {
return "wizard";
}
return "default";
}
function resolveLightLaneIncludePatterns(kind: string, targetArg: string, cwd: string) {
const relative = toRepoRelativeTarget(targetArg, cwd);
if (kind === "unitFast") {
const includePattern = resolveUnitFastTestIncludePattern(relative);
return includePattern ? [includePattern] : null;
}
if (kind === "unitFastFakeTimers") {
const includePattern = resolveUnitFastTimerTestIncludePattern(relative);
return includePattern ? [includePattern] : null;
}
if (kind === "unitFastIsolated") {
const includePattern = resolveUnitFastIsolatedTestIncludePattern(relative);
return includePattern ? [includePattern] : null;
}
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 shouldUseWholeConfigTarget(kind: string, targetArg: string, cwd: string) {
if (isVitestConfigTargetForKind(kind, targetArg, cwd)) {
return true;
}
if (kind === "uiE2e") {
const relative = toRepoRelativeTarget(targetArg, cwd);
return relative === "ui/src/test-helpers/control-ui-e2e.ts";
}
if (kind !== "ui") {
return false;
}
const relative = toRepoRelativeTarget(targetArg, cwd);
// Source files need whole-project coverage; existing directories already define a test scope.
if (isTestFileTarget(relative) || isExistingDirectoryTarget(targetArg, cwd)) {
return false;
}
if (isPluginControlUiPath(relative) && !isLikelyFileTarget(relative)) {
return false;
}
return isControlUiSourcePath(relative);
}
function createVitestArgs(
params: Pick<VitestRunPlan, "config" | "forwardedArgs" | "watchMode"> & {
env?: NodeJS.ProcessEnv;
},
) {
return [
"exec",
"node",
...resolveVitestNodeArgs(params.env),
resolveVitestCliEntry(),
...(params.watchMode ? [] : ["run"]),
"--config",
params.config,
...(params.config === UI_E2E_VITEST_CONFIG ? ["--configLoader", "runner"] : []),
...params.forwardedArgs,
];
}
export function createVitestPreflightPnpmArgs(config: string) {
if (config !== UI_E2E_VITEST_CONFIG) {
return null;
}
return ["exec", "node", "--import", "tsx", "scripts/ensure-playwright-chromium.mts"];
}
export function parseTestProjectsArgs(args: string[], cwd = process.cwd()) {
const forwardedArgs: string[] = [];
const nonTargetArgs: string[] = [];
const targetArgs: string[] = [];
let watchMode = false;
let passthrough = false;
for (let index = 0; index < args.length; index++) {
const arg = args[index]!;
if (arg === "--") {
// The project wrapper consumes separators; direct native and batch calls retain theirs.
passthrough = targetArgs.length > 0;
continue;
}
const watch = resolveBooleanModeFlag(args, index, "watch", "-w");
if (watch) {
watchMode = watch.value;
}
// Preserve bare wrapper --watch's existing omission of the named run command.
const next = args[index + 1];
if (!passthrough && arg === "--watch" && next !== "true" && next !== "false") {
continue;
}
forwardedArgs.push(arg);
if (vitestOptionConsumesNextArg(arg, next)) {
forwardedArgs.push(next!);
// Preserve operand occurrences even when their value also names a target.
nonTargetArgs.push(arg, next!);
index++;
} else if (!passthrough && isPathLikeTargetArg(arg, cwd)) {
targetArgs.push(arg);
} else {
nonTargetArgs.push(arg);
}
}
return { forwardedArgs, nonTargetArgs, targetArgs, watchMode };
}
export function buildVitestRunPlans(
args: string[],
cwd = process.cwd(),
listChangedPaths: (baseRef: string, cwd: string) => string[] = listChangedPathsFromGit,
options: ChangedTestTargetOptions = {},
) {
const {
forwardedArgs,
nonTargetArgs: remainingArgs,
targetArgs,
watchMode,
} = parseTestProjectsArgs(args, cwd);
const changedTargetArgs =
targetArgs.length === 0 ? resolveChangedTargetArgs(args, cwd, listChangedPaths, options) : null;
const requestedTargetArgs = changedTargetArgs ?? targetArgs;
const ownedTargets = explicitIncludeTargets(targetArgs, cwd);
if (
watchMode &&
requestedTargetArgs.some((target) => {
const relative = toRepoRelativeTarget(target, cwd);
return (
(isPathAtOrUnder(relative, "ui") || isPluginControlUiPath(relative)) &&
isGlobTarget(relative)
);
})
) {
throw new Error(
"watch mode with UI glob targets is not supported; use a literal test path, directory, or dedicated UI suite",
);
}
const activeTargetArgs = expandBroadToolingScriptTargets(
expandExplicitSourceTestTargets(requestedTargetArgs, cwd, watchMode),
cwd,
watchMode,
);
const activeForwardedArgs =
changedTargetArgs !== null ? stripChangedArgs(forwardedArgs) : forwardedArgs;
if (changedTargetArgs !== null && activeTargetArgs.length === 0) {
return [];
}
if (activeTargetArgs.length === 0) {
return [
{
config: DEFAULT_VITEST_CONFIG,
forwardedArgs: activeForwardedArgs,
includePatterns: null,
watchMode,
},
];
}
const nonTargetArgs = changedTargetArgs !== null ? activeForwardedArgs : remainingArgs;
const classifiedTargets = activeTargetArgs.map((targetArg) => ({
targetArg,
relative: toRepoRelativeTarget(targetArg, cwd),
kind: classifyTarget(targetArg, cwd),
}));
const gatewayProjectShards = (options.env ?? process.env).OPENCLAW_GATEWAY_PROJECT_SHARDS;
const hasGatewayAggregateTarget = classifiedTargets.some(
({ kind, relative }) =>
kind === "gateway" ||
(kind === "gatewayServer" &&
!isVitestConfigFileTarget(relative) &&
gatewayProjectShards === "0"),
);
const explicitConfigTargets = classifiedTargets.map(({ relative }) => relative);
const databaseWorkerPatterns = uniqueOrdered([
...requestedTargetArgs,
...activeTargetArgs,
]).flatMap((targetArg) => {
const relative = toRepoRelativeTarget(targetArg, cwd);
if (
!watchMode &&
(isOrdinaryAgentGlobTarget(relative) ||
(!isGlobTarget(relative) &&
isExistingDirectoryTarget(targetArg, cwd) &&
(isPathAtOrUnder(relative, "packages") ||
isPathAtOrUnder(relative, agentVitestProjectOwners.all.root))))
) {
// Expanded selections already contribute their existing leaves through activeTargetArgs.
return [];
}
return isTestFileTarget(relative) ||
isGlobTarget(relative) ||
isExistingDirectoryTarget(targetArg, cwd)
? [toScopedIncludePattern(targetArg, cwd)]
: [];
});
const impliedDatabaseWorkerTargets = databaseWorkerCoreTestFiles.filter((file) =>
databaseWorkerPatterns.some((pattern) => includePatternMatchesAnyFile(pattern, [file])),
);
const hasPackageFileTarget = classifiedTargets.some(
({ kind, relative }) =>
kind === "packageContract" && relative !== PACKAGE_CONTRACT_VITEST_CONFIG,
);
// The wrapper removes its separator; remaining positional filters can widen
// a package file run beyond the package inventory. Option operands cannot.
const hasE2eTarget =
classifiedTargets.some(({ kind }) => kind === "e2e") ||
(!explicitConfigTargets.includes(PACKAGE_CONTRACT_VITEST_CONFIG) &&
hasPackageFileTarget &&
collectVitestFileFilters(["run", ...nonTargetArgs]).length > 0);
if (
explicitConfigTargets.every(isVitestConfigFileTarget) &&
impliedDatabaseWorkerTargets.length === 0
) {
if (watchMode && explicitConfigTargets.length > 1) {
throw new Error(
"watch mode with mixed test suites is not supported; target one suite at a time or use a dedicated suite command",
);
}
return explicitConfigTargets
.filter((config) => !hasE2eTarget || config !== PACKAGE_CONTRACT_VITEST_CONFIG)
.filter(
(config) => !hasGatewayAggregateTarget || config !== GATEWAY_DATABASE_WORKERS_VITEST_CONFIG,
)
.flatMap((config) =>
createBoundedExtensionPlans(
{
config,
forwardedArgs: nonTargetArgs,
includePatterns: null,
watchMode,
},
cwd,
options.env,
),
);
}
const groupedTargets = new Map<string, string[]>();
for (const { targetArg, relative, kind: targetKind } of classifiedTargets) {
if (!watchMode && relative === AGENTS_EMBEDDED_AGENT_TEST_ROOT) {
// The recursive parent spans four harness owners; keep every isolated project intact.
for (const { kind, include: targets } of embeddedAgentVitestProjectOwners) {
const current = groupedTargets.get(kind) ?? [];
for (const target of targets) {
if (!current.includes(target)) {
current.push(target);
}
}
groupedTargets.set(kind, current);
}
continue;
}
// A requested Gateway aggregate already owns its child tests. Watch also
// keeps that aggregate; mixed E2E selections retain their serial build owner.
const kind =
(targetKind === "gatewayDatabaseWorkers" ||
(targetKind === "gatewayServer" && !isVitestConfigFileTarget(relative))) &&
(watchMode || hasGatewayAggregateTarget)
? "gateway"
: hasE2eTarget && targetKind === "packageContract"
? "e2e"
: targetKind;
const current = groupedTargets.get(kind) ?? [];
current.push(
...(hasE2eTarget && relative === PACKAGE_CONTRACT_VITEST_CONFIG
? packageContractTestFiles
: kind === "gateway" && relative === GATEWAY_DATABASE_WORKERS_VITEST_CONFIG
? gatewayDatabaseWorkerTestFiles
: [targetArg]),
);
groupedTargets.set(kind, current);
}
if (impliedDatabaseWorkerTargets.length > 0) {
const current = groupedTargets.get("infra") ?? [];
for (const target of impliedDatabaseWorkerTargets) {
if (!current.includes(target)) {
current.push(target);
}
}
groupedTargets.set("infra", current);
}
const toolingTargets = groupedTargets.get("tooling") ?? [];
if (
!watchMode &&
toolingTargets.some((targetArg) =>
includePatternMatchesAnyFile(toScopedIncludePattern(targetArg, cwd), [
TOOLING_DOCKER_TEST_TARGET,
]),
)
) {
const current = groupedTargets.get("toolingDocker") ?? [];
if (!current.includes(TOOLING_DOCKER_TEST_TARGET)) {
current.push(TOOLING_DOCKER_TEST_TARGET);
groupedTargets.set("toolingDocker", current);
}
}
const activeIncludePatterns = activeTargetArgs.map((target) =>
toScopedIncludePattern(target, cwd),
);
const impliedToolingIsolatedTargets = !watchMode
? toolingIsolatedTestFiles.filter((file) =>
activeIncludePatterns.some((pattern) => includePatternMatchesAnyFile(pattern, [file])),
)
: [];
if (impliedToolingIsolatedTargets.length > 0) {
const current = groupedTargets.get("toolingIsolated") ?? [];
for (const target of impliedToolingIsolatedTargets) {
if (!current.includes(target)) {
current.push(target);
}
}
groupedTargets.set("toolingIsolated", current);
}
const uiTargets = groupedTargets.get("ui") ?? [];
const uiIncludePatterns = uiTargets.map((target) => toScopedIncludePattern(target, cwd));
const impliedUiTimingTargets = uiTimingTestFiles.filter((file) =>
uiIncludePatterns.some((pattern) => includePatternMatchesAnyFile(pattern, [file])),
);
if (impliedUiTimingTargets.length > 0) {
groupedTargets.set("uiTiming", [
...new Set([...(groupedTargets.get("uiTiming") ?? []), ...impliedUiTimingTargets]),
]);
}
const broadUiTargets = uiTargets.filter(
(targetArg) => !isTestFileTarget(toRepoRelativeTarget(targetArg, cwd)),
);
if (broadUiTargets.length > 0) {
const browserTargets = listExplicitTestTargetFilesForCwd(cwd).filter(
(file) =>
isUiBrowserTestFile(file) &&
broadUiTargets.some(
(targetArg) =>
shouldUseWholeConfigTarget("ui", targetArg, cwd) ||
includePatternMatchesAnyFile(toScopedIncludePattern(targetArg, cwd), [file]),
),
);
if (browserTargets.length > 0) {
groupedTargets.set("uiBrowser", [
...new Set([...(groupedTargets.get("uiBrowser") ?? []), ...browserTargets]),
]);
}
}
const impliedUiIsolatedTargets = uiIsolatedTestFiles.filter((file) =>
uiIncludePatterns.some((pattern) => includePatternMatchesAnyFile(pattern, [file])),
);
if (impliedUiIsolatedTargets.length > 0) {
const current = groupedTargets.get("uiIsolated") ?? [];
for (const target of impliedUiIsolatedTargets) {
if (!current.includes(target)) {
current.push(target);
}
}
groupedTargets.set("uiIsolated", current);
}
// Source-child ownership can cross shared suites (for example state tests).
// Match every active target so broad selections cannot silently omit excluded children.
const impliedCliProcessTargets = cliProcessTestFiles.filter((file) =>
activeIncludePatterns.some((pattern) => includePatternMatchesAnyFile(pattern, [file])),
);
if (impliedCliProcessTargets.length > 0) {
const current = groupedTargets.get("cliProcess") ?? [];
for (const target of impliedCliProcessTargets) {
if (!current.includes(target)) {
current.push(target);
}
}
groupedTargets.set("cliProcess", current);
}
const impliedExtensionWorkerTargets = classifiedTargets
.filter(({ kind }) => kind === "extensionDatabaseWorkers")
.map(({ relative }) => relative);
const impliedWatchWorkerTargets = [
...impliedDatabaseWorkerTargets,
...impliedExtensionWorkerTargets,
];
const previousWatchKinds = watchMode
? new Set(classifiedTargets.map(({ targetArg }) => classifyTarget(targetArg, cwd, true)))
: new Set<string>();
if (watchMode && (groupedTargets.size > 1 || previousWatchKinds.size > 1)) {
if (impliedWatchWorkerTargets.length > 0 && previousWatchKinds.size === 1) {
const previousKind = [...previousWatchKinds][0]!;
const wholeOwner = classifiedTargets.some(({ targetArg }) =>
shouldUseWholeConfigTarget(previousKind, targetArg, cwd),
);
return [
{
config: "test/vitest/vitest.database-worker-watch.config.ts",
databaseWorkerWatchOwner: VITEST_CONFIG_BY_KIND[previousKind] ?? DEFAULT_VITEST_CONFIG,
databaseWorkerWatchTests: wholeOwner
? [...databaseWorkerCoreTestFiles, ...databaseWorkerExtensionTestFiles].filter(
(file) => classifyTarget(file, cwd, true) === previousKind,
)
: impliedWatchWorkerTargets,
forwardedArgs: nonTargetArgs,
includePatterns: wholeOwner
? null
: uniqueOrdered(activeTargetArgs.map((target) => toScopedIncludePattern(target, cwd))),
watchMode: true,
},
];
}
throw new Error(
"watch mode with mixed test suites is not supported; target one suite at a time or use a dedicated suite command",
);
}
const orderedKinds = Object.keys(VITEST_CONFIG_BY_KIND);
orderedKinds.splice(orderedKinds.indexOf("boundary"), 0, "default");
const plans: VitestRunPlan[] = [];
for (const kind of orderedKinds) {
const grouped = groupedTargets.get(kind);
if (!grouped || grouped.length === 0) {
continue;
}
if (kind === "extensionFull") {
const configs = watchMode
? [FULL_EXTENSIONS_VITEST_CONFIG]
: listFullExtensionVitestProjectConfigs();
for (const config of configs) {
const plan = {
config,
forwardedArgs: nonTargetArgs,
includePatterns: null,
watchMode,
};
plans.push(...createBoundedExtensionPlans(plan, cwd, options.env));
}
continue;
}
const config = VITEST_CONFIG_BY_KIND[kind] ?? DEFAULT_VITEST_CONFIG;
const useCliTargetArgs =
kind === "e2e" ||
kind === "packageContract" ||
grouped.every((targetArg) => isCanonicalAgentOwnerDirectoryTarget(targetArg, cwd)) ||
(kind === "default" &&
grouped.every((targetArg) => isTestFileTarget(toRepoRelativeTarget(targetArg, cwd))));
const useWholeConfigTarget = grouped.some((targetArg) =>
shouldUseWholeConfigTarget(kind, targetArg, cwd),
);
const scopedTargetArgs =
useCliTargetArgs && !useWholeConfigTarget ? uniqueOrdered(grouped) : [];
const forwardedPlanArgs = [...nonTargetArgs, ...scopedTargetArgs];
const unitCliIncludes =
kind === "default" &&
useCliTargetArgs &&
!watchMode &&
!options.env?.[INCLUDE_FILE_ENV_KEY]?.trim() &&
grouped.every((targetArg) => !path.isAbsolute(targetArg))
? uniqueOrdered(grouped.map((targetArg) => toRepoRelativeTarget(targetArg, cwd)))
: null;
// CI needs explicit selection metadata. Keep the CLI filters too: the unit
// config and runner use them for exclude and empty-selection policy.
const scopedUnitIncludes =
unitCliIncludes?.length &&
unitCliIncludes.every(
(file) =>
!isGlobTarget(file) && isUnitConfigTestFile(file) && isExistingFileTarget(file, cwd),
)
? unitCliIncludes
: null;
const includePatterns = useCliTargetArgs
? scopedUnitIncludes
: useWholeConfigTarget
? null
: uniqueOrdered(
grouped.flatMap((targetArg) => {
const lightLanePatterns = resolveLightLaneIncludePatterns(kind, targetArg, cwd);
return lightLanePatterns ?? [toScopedIncludePattern(targetArg, cwd)];
}),
);
const broadToolingScriptPlans = createBroadToolingScriptPlans({
config,
cwd,
forwardedArgs: forwardedPlanArgs,
includePatterns,
watchMode,
});
if (broadToolingScriptPlans) {
plans.push(...broadToolingScriptPlans);
continue;
}
const processRoots = EXTENSION_TEST_PROCESS_ROOTS.get(config);
const boundedExtensionRoots = grouped.flatMap((targetArg) => {
const root = toRepoRelativeTarget(targetArg, cwd);
return processRoots?.includes(root) && isExistingDirectoryTarget(targetArg, cwd)
? [root]
: [];
});
const boundedRootsCoverGroupedTargets = grouped.every((targetArg) => {
const relativeTarget = toRepoRelativeTarget(targetArg, cwd);
return boundedExtensionRoots.some(
(root) => relativeTarget === root || relativeTarget.startsWith(`${root}/`),
);
});
const boundedExtensionPlans =
kind === "extensionDatabaseWorkers" ||
(boundedExtensionRoots.length > 0 && boundedRootsCoverGroupedTargets)
? createBoundedExtensionPlans(
{
config,
forwardedArgs: forwardedPlanArgs,
includePatterns,
watchMode,
},
cwd,
options.env,
ownedTargets,
)
: null;
if (boundedExtensionPlans) {
plans.push(...boundedExtensionPlans);
continue;
}
plans.push({
config,
forwardedArgs: forwardedPlanArgs,
includePatterns,
watchMode,
});
}
return plans;
}
export function buildFullSuiteVitestRunPlans(args: string[], cwd = process.cwd()): VitestRunPlan[] {
const { forwardedArgs, targetArgs, watchMode } = parseTestProjectsArgs(args, cwd);
if (watchMode) {
return [
{
config: "vitest.config.ts",
forwardedArgs,
includePatterns: null,
watchMode,
},
];
}
const parallelShardCount = parsePositiveInt(
process.env.OPENCLAW_TEST_PROJECTS_PARALLEL,
"OPENCLAW_TEST_PROJECTS_PARALLEL",
);
const expandToProjectConfigs =
process.env.OPENCLAW_TEST_PROJECTS_LEAF_SHARDS === "1" ||
(parallelShardCount !== null && parallelShardCount > 1) ||
shouldExpandLocalFullSuiteShardsByDefault(process.env);
return fullSuiteVitestShards.flatMap((shard) => {
if (
process.env.OPENCLAW_TEST_SKIP_FULL_EXTENSIONS_SHARD === "1" &&
shard.config === FULL_EXTENSIONS_VITEST_CONFIG
) {
return [];
}
// The remote Testbox full gate runs every agentic and extension project in one process tree.
// Bound project and worker lifetimes before either aggregate reaches V8's heap limit.
const expandShard =
expandToProjectConfigs ||
(process.env.OPENCLAW_TESTBOX_REMOTE_RUN === "1" &&
(shard.config === FULL_AGENTIC_VITEST_CONFIG ||
shard.config === FULL_EXTENSIONS_VITEST_CONFIG));
const configs = expandShard ? shard.projects : [shard.config];
return configs.flatMap((config) => {
if (expandShard && targetArgs.length === 0) {
let chunks: string[][] | null = null;
if (config === AGENTS_CORE_VITEST_CONFIG) {
// A single non-isolated agents-core process grows until its worker can
// exit under the full-suite memory load. Bound each process lifetime.
chunks = splitTargetChunks(
listAgentsCoreFullSuiteTestTargets(cwd),
FULL_SUITE_AGENTS_CORE_TEST_TARGET_CHUNK_COUNT,
);
} else if (config === UNIT_FAST_VITEST_CONFIG) {
const targets = listUnitFastFullSuiteTestTargets();
const chunkCount = Math.ceil(
targets.length / FULL_SUITE_UNIT_FAST_TEST_TARGET_CHUNK_SIZE,
);
chunks = splitTargetChunks(targets, chunkCount);
} else if (config === UNIT_SRC_VITEST_CONFIG) {
// The 600+ file process can run for 20 minutes and has produced late,
// non-reproducible mock-transform failures under Bun. Bound the worker
// lifetime while preserving the complete file inventory.
const targets = listUnitSrcFullSuiteTestTargets(cwd);
const chunkCount = Math.ceil(targets.length / FULL_SUITE_UNIT_SRC_TEST_TARGET_CHUNK_SIZE);
chunks = splitTargetChunks(targets, chunkCount);
} else if (config === INFRA_VITEST_CONFIG) {
// Isolated infra files can share the scheduler without sharing fork state.
const targets = listInfraFullSuiteTestTargets(cwd);
chunks = splitTargetChunks(
targets,
Math.ceil(targets.length / FULL_SUITE_INFRA_TEST_TARGET_CHUNK_SIZE),
);
} else if (config === TOOLING_VITEST_CONFIG) {
// Tooling tests spawn package managers and native helpers. Keep native
// process lifetime short enough that unrelated files cannot crash together.
const targets = listToolingFullSuiteTestTargets(cwd);
const chunkCount = Math.ceil(targets.length / FULL_SUITE_TOOLING_TEST_TARGET_CHUNK_SIZE);
chunks = splitTargetChunks(targets, chunkCount);
} else if (config === GATEWAY_SERVER_VITEST_CONFIG) {
chunks = createGatewayServerTestTargetChunks(cwd);
} else {
const roots = EXTENSION_TEST_PROCESS_ROOTS.get(config);
if (roots) {
chunks = createExtensionTestProcessTargetChunks(config, roots, forwardedArgs, cwd);
}
}
if (chunks !== null) {
return chunks.map((targets) => ({
config,
forwardedArgs: [...forwardedArgs, ...targets],
timingTargets: targets,
includePatterns: null,
watchMode: false,
}));
}
}
return [
{
config,
forwardedArgs,
includePatterns: null,
watchMode: false,
},
];
});
});
}
function shouldUseLocalFullSuiteParallelByDefault(env = process.env) {
if (hasConservativeVitestWorkerBudget(env)) {
return false;
}
return env.OPENCLAW_TEST_PROJECTS_SERIAL !== "1" && !isCiLikeEnv(env);
}
function shouldExpandLocalFullSuiteShardsByDefault(env = process.env) {
return !isCiLikeEnv(env);
}
function parsePositiveInt(value: string | undefined, label: string) {
const text = value?.trim();
if (!text) {
return null;
}
if (!/^\d+$/u.test(text)) {
throw new Error(`${label} must be a positive integer; got: ${value}`);
}
const parsed = Number(text);
if (!Number.isSafeInteger(parsed) || parsed <= 0) {
throw new Error(`${label} must be a positive integer; got: ${value}`);
}
return parsed;
}
function hasConservativeVitestWorkerBudget(env: NodeJS.ProcessEnv) {
const workerBudget = parsePositiveInt(
env.OPENCLAW_VITEST_MAX_WORKERS ?? env.OPENCLAW_TEST_WORKERS,
env.OPENCLAW_VITEST_MAX_WORKERS === undefined
? "OPENCLAW_TEST_WORKERS"
: "OPENCLAW_VITEST_MAX_WORKERS",
);
return workerBudget !== null && workerBudget <= 1;
}
const FULL_EXTENSIONS_CONFIG = "test/vitest/vitest.full-extensions.config.ts";
const FULL_EXTENSIONS_MIN_HEAP_MB = 8192;
function ensureMaxOldSpaceSize(nodeOptions: string | undefined, minimumMb: number) {
const normalized = nodeOptions?.trim() ?? "";
const matches = Array.from(
normalized.matchAll(/(^|\s)--max[-_]old[-_]space[-_]size(?:=|\s+)(\d+)(?=\s|$)/gu),
);
const match = matches.at(-1);
if (!match) {
return [normalized, `--max-old-space-size=${minimumMb}`].filter(Boolean).join(" ");
}
const currentMb = Number(match[2]);
if (Number.isSafeInteger(currentMb) && currentMb >= minimumMb) {
return normalized;
}
const start = match.index;
const replacement = match[0].replace(/\d+$/u, String(minimumMb));
return `${normalized.slice(0, start)}${replacement}${normalized.slice(start + match[0].length)}`;
}
export function applyFullExtensionsHeapBudget<T extends VitestSpecShape>(
specs: T[],
params: { env?: NodeJS.ProcessEnv } = {},
): Array<Omit<T, "env"> & { env: NodeJS.ProcessEnv }> {
const baseEnv = params.env ?? {};
return specs.map((spec) =>
spec.config === FULL_EXTENSIONS_CONFIG
? {
...spec,
env: {
...spec.env,
NODE_OPTIONS: ensureMaxOldSpaceSize(
spec.env?.NODE_OPTIONS ?? baseEnv.NODE_OPTIONS,
FULL_EXTENSIONS_MIN_HEAP_MB,
),
},
}
: spec,
);
}
export function resolveParallelFullSuiteConcurrency(
specCount: number,
envInput?: NodeJS.ProcessEnv,
hostInfo?: VitestHostInfo,
) {
let env = envInput;
env ??= process.env;
const override = parsePositiveInt(
env.OPENCLAW_TEST_PROJECTS_PARALLEL,
"OPENCLAW_TEST_PROJECTS_PARALLEL",
);
if (override !== null) {
return Math.min(override, specCount);
}
if (env.OPENCLAW_TEST_PROJECTS_SERIAL === "1") {
return 1;
}
if (isCiLikeEnv(env)) {
return 1;
}
if (hasConservativeVitestWorkerBudget(env)) {
return 1;
}
if (
env.OPENCLAW_TEST_PROJECTS_LEAF_SHARDS !== "1" &&
!shouldUseLocalFullSuiteParallelByDefault(env)
) {
return 1;
}
return Math.min(resolveLocalFullSuiteProfile(env, hostInfo).shardParallelism, specCount);
}
export function applyParallelVitestCachePaths<T extends VitestSpecShape>(
specs: T[],
params: { cwd?: string; env?: NodeJS.ProcessEnv } = {},
): Array<CacheAssignedSpec<T>> {
const baseEnv = params.env ?? process.env;
const cwd = params.cwd ?? process.cwd();
const sharedRoot = baseEnv.OPENCLAW_VITEST_FS_MODULE_CACHE_ROOT?.trim();
// Project callers historically supplied a root through PATH. ROOT makes CI's
// ownership explicit while a simultaneous PATH remains a caller-owned leaf.
const legacyRoot = sharedRoot ? undefined : baseEnv[FS_MODULE_CACHE_PATH_ENV_KEY]?.trim();
const cacheRoot = legacyRoot || resolveVitestCacheRoot(baseEnv, cwd);
const configSlots = new Map<string, number>();
return specs.map((spec) => {
const specCachePath = spec.env?.[FS_MODULE_CACHE_PATH_ENV_KEY]?.trim();
if (
spec.cacheAssignment?.kind === "caller" ||
(spec.cacheAssignment?.kind !== "scheduler" && specCachePath && specCachePath !== legacyRoot)
) {
return { ...spec, cacheAssignment: spec.cacheAssignment ?? { kind: "caller" } };
}
const firstSlot = resolveVitestCacheSlotPath(cacheRoot, spec.config, 0, cwd);
const slot = configSlots.get(firstSlot) ?? 0;
configSlots.set(firstSlot, slot + 1);
return {
...spec,
cacheAssignment: { kind: "scheduler", root: cacheRoot },
env: {
...spec.env,
[FS_MODULE_CACHE_PATH_ENV_KEY]: resolveVitestCacheSlotPath(
cacheRoot,
spec.config,
slot,
cwd,
),
},
};
});
}
export function applyDefaultVitestCachePaths<T extends WatchableVitestSpecShape>(
specs: T[],
params: { cwd?: string; env?: NodeJS.ProcessEnv } = {},
): Array<CacheAssignedSpec<T>> {
if (specs.some((spec) => spec.watchMode)) {
return specs;
}
const baseEnv = params.env ?? process.env;
const oneConfig = specs.length <= 1 || specs.every((spec) => spec.config === specs[0]?.config);
// Before ROOT existed these serial callers owned PATH as an exact leaf.
if (
!baseEnv.OPENCLAW_VITEST_FS_MODULE_CACHE_ROOT?.trim() &&
baseEnv[FS_MODULE_CACHE_PATH_ENV_KEY]?.trim() &&
oneConfig
) {
return specs;
}
if (process.platform === "win32" && oneConfig) {
return specs;
}
return applyParallelVitestCachePaths(specs, params);
}
export function applyDefaultVitestNoOutputTimeout<T extends WatchableVitestSpecShape>(
specs: T[],
params: { env?: NodeJS.ProcessEnv } = {},
): Array<Omit<T, "env"> & { env: NodeJS.ProcessEnv }> {
const baseEnv = params.env ?? process.env;
if (
Object.hasOwn(baseEnv, VITEST_NO_OUTPUT_TIMEOUT_ENV_KEY) &&
Object.hasOwn(baseEnv, VITEST_NO_OUTPUT_HEARTBEAT_ENV_KEY)
) {
return specs;
}
return specs.map((spec) => {
if (spec.watchMode) {
return spec;
}
const env = spec.env ?? {};
const nextEnv = { ...env };
if (
!Object.hasOwn(baseEnv, VITEST_NO_OUTPUT_TIMEOUT_ENV_KEY) &&
!Object.hasOwn(env, VITEST_NO_OUTPUT_TIMEOUT_ENV_KEY)
) {
nextEnv[VITEST_NO_OUTPUT_TIMEOUT_ENV_KEY] = resolveTestProjectsVitestNoOutputTimeoutMs(
spec.config,
);
}
if (
!Object.hasOwn(baseEnv, VITEST_NO_OUTPUT_HEARTBEAT_ENV_KEY) &&
!Object.hasOwn(env, VITEST_NO_OUTPUT_HEARTBEAT_ENV_KEY)
) {
nextEnv[VITEST_NO_OUTPUT_HEARTBEAT_ENV_KEY] =
DEFAULT_TEST_PROJECTS_VITEST_NO_OUTPUT_HEARTBEAT_MS;
}
return {
...spec,
env: nextEnv,
};
});
}
export function createVitestRunSpecs(
args: string[],
params: { baseEnv?: NodeJS.ProcessEnv; cwd?: string } = {},
) {
const cwd = params.cwd ?? process.cwd();
const baseEnv = params.baseEnv ?? process.env;
const ownedTargets = explicitIncludeTargets(parseTestProjectsArgs(args, cwd).targetArgs, cwd);
const plans = filterPlansForContractIncludeFile(
buildVitestRunPlans(args, cwd, listChangedPathsFromGit, { env: baseEnv }),
baseEnv,
ownedTargets,
);
const inheritedIncludes = plans.some(
(plan) =>
plan.includePatterns !== null && !ownsIncludeSelection(plan.includePatterns, ownedTargets),
)
? loadIncludePatternsForSpecFilter(baseEnv)
: null;
return plans.flatMap((originalPlan, index) => {
const plan =
originalPlan.includePatterns && inheritedIncludes
? {
...originalPlan,
includePatterns: resolveInheritedIncludeScope(
originalPlan.includePatterns,
inheritedIncludes,
ownedTargets,
),
}
: originalPlan;
if (inheritedIncludes && plan.includePatterns?.length === 0) {
return [];
}
const includeFilePath = plan.includePatterns
? path.join(os.tmpdir(), `openclaw-vitest-include-${randomUUID()}-${index}.json`)
: null;
return {
config: plan.config,
timingTargets: plan.timingTargets,
env:
includeFilePath || plan.databaseWorkerWatchOwner
? {
...baseEnv,
...(includeFilePath ? { [INCLUDE_FILE_ENV_KEY]: includeFilePath } : {}),
...(plan.databaseWorkerWatchOwner
? {
[DATABASE_WORKER_WATCH_OWNER_ENV_KEY]: plan.databaseWorkerWatchOwner,
[DATABASE_WORKER_WATCH_TESTS_ENV_KEY]: JSON.stringify(
plan.databaseWorkerWatchTests,
),
}
: {}),
}
: baseEnv,
includeFilePath,
includePatterns: plan.includePatterns,
pnpmArgs: createVitestArgs(plan),
preflightPnpmArgs: createVitestPreflightPnpmArgs(plan.config),
watchMode: plan.watchMode,
};
});
}
function loadIncludePatternsForSpecFilter(env: NodeJS.ProcessEnv) {
const filePath = env[INCLUDE_FILE_ENV_KEY]?.trim();
if (!filePath) {
return null;
}
const parsed: unknown = JSON.parse(fs.readFileSync(filePath, "utf8"));
if (!Array.isArray(parsed)) {
return [];
}
return parsed.filter((value): value is string => typeof value === "string" && value.length > 0);
}
function includePatternMatchesConfig(candidate: string, configPatterns: readonly string[]) {
return configPatterns.some(
(pattern) => path.matchesGlob(candidate, pattern) || path.matchesGlob(pattern, candidate),
);
}
function filterPlansForContractIncludeFile(
plans: VitestRunPlan[],
env: NodeJS.ProcessEnv,
ownedTargets?: ReadonlySet<string>,
) {
if (
!plans.some(
(plan) =>
CHANNEL_CONTRACT_CONFIG_PATTERNS.has(plan.config) &&
!ownsIncludeSelection(plan.includePatterns, ownedTargets),
)
) {
return plans;
}
const includePatterns = loadIncludePatternsForSpecFilter(env);
if (!includePatterns) {
return plans;
}
return plans.filter((plan) => {
if (plan.includePatterns?.some((pattern) => ownedTargets?.has(pattern))) {
return true;
}
const configPatterns = CHANNEL_CONTRACT_CONFIG_PATTERNS.get(plan.config);
if (!configPatterns) {
return true;
}
return includePatterns.some((candidate) =>
includePatternMatchesConfig(candidate, configPatterns),
);
});
}
function expandVitestIncludePatterns(includePatterns: string[], cwd: string) {
const candidateFiles = includePatterns.some(isGlobTarget)
? listExplicitTestTargetFilesForCwd(cwd).toSorted()
: [];
return uniqueOrdered(
includePatterns.flatMap((pattern) => {
if (!isGlobTarget(pattern)) {
return [pattern];
}
return candidateFiles.filter((file) => path.matchesGlob(file, pattern));
}),
);
}
export function writeVitestIncludeFile(
filePath: string,
includePatterns: string[],
options: { cwd?: string; expandGlobs?: boolean } = {},
) {
// Shared Vitest projects intersect this file with their ownership globs.
// One-shot runs emit concrete paths; watch runs retain globs for new files.
const expandedPatterns =
options.expandGlobs === false
? includePatterns
: expandVitestIncludePatterns(includePatterns, options.cwd ?? process.cwd());
fs.writeFileSync(filePath, `${JSON.stringify(expandedPatterns, null, 2)}\n`);
}
function shellQuote(value: string) {
const text = value;
if (text === "") {
return "''";
}
if (/^[A-Za-z0-9_./:=@%+-]+$/u.test(text)) {
return text;
}
return `'${text.replaceAll("'", "'\\''")}'`;
}
function formatFailedShardRerunCommand(failure: FailedVitestShard) {
const includePatterns = failure.includePatterns ?? [];
if (includePatterns.length > 0) {
return ["pnpm", "test", ...includePatterns.map(shellQuote), "--", "--reporter=verbose"].join(
" ",
);
}
return [
"node",
"scripts/run-vitest.mjs",
"run",
"--config",
shellQuote(failure.config),
"--reporter=verbose",
].join(" ");
}
function formatFailedShardStatus(failure: FailedVitestShard) {
const details = [];
if (failure.code !== undefined && failure.code !== null) {
details.push(`exit ${failure.code}`);
}
if (failure.signal) {
details.push(`signal ${failure.signal}`);
}
if (failure.noOutputTimedOut) {
details.push("no-output timeout");
}
return details.length > 0 ? ` (${details.join(", ")})` : "";
}
export function formatFailedShardDigest(failures: FailedVitestShard[]) {
if (failures.length === 0) {
return [];
}
const orderedFailures = failures.toSorted((left, right) => {
const leftOrder = typeof left.order === "number" ? left.order : Number.MAX_SAFE_INTEGER;
const rightOrder = typeof right.order === "number" ? right.order : Number.MAX_SAFE_INTEGER;
return leftOrder - rightOrder || left.config.localeCompare(right.config);
});
const shown = orderedFailures.slice(0, FAILED_SHARD_DIGEST_LIMIT);
const lines = [`[test] failed shard digest (${failures.length}):`];
for (const failure of shown) {
const includePatterns = failure.includePatterns ?? [];
const includes =
includePatterns.length > 0 ? ` includes=${includePatterns.map(shellQuote).join(",")}` : "";
lines.push(`[test] - ${failure.config}${formatFailedShardStatus(failure)}${includes}`);
lines.push(`[test] rerun: ${formatFailedShardRerunCommand(failure)}`);
}
if (shown.length < failures.length) {
lines.push(`[test] - ... ${failures.length - shown.length} more failed shard(s) omitted`);
}
return lines;
}