mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-15 23:24:03 -06:00
fc87337c14
Five-file Telegram jobs finished the first file, then isolate re-imported the next graph in silence until the 300s watchdog killed the worker. Recycle the Vitest process after each file and keep five files per CI job.
4353 lines
152 KiB
TypeScript
4353 lines
152 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 } 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,
|
|
} 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 { isAcpxExtensionRoot } from "../test/vitest/vitest.extension-acpx-paths.mjs";
|
|
import { isActiveMemoryExtensionRoot } from "../test/vitest/vitest.extension-active-memory-paths.mjs";
|
|
import { isBrowserExtensionRoot } from "../test/vitest/vitest.extension-browser-paths.mjs";
|
|
import { resolveSplitChannelExtensionShard } from "../test/vitest/vitest.extension-channel-split-paths.mjs";
|
|
import { isCodexExtensionRoot } from "../test/vitest/vitest.extension-codex-paths.mjs";
|
|
import { isDiffsExtensionRoot } from "../test/vitest/vitest.extension-diffs-paths.mjs";
|
|
import { isFeishuExtensionRoot } from "../test/vitest/vitest.extension-feishu-paths.mjs";
|
|
import { isIrcExtensionRoot } from "../test/vitest/vitest.extension-irc-paths.mjs";
|
|
import {
|
|
isMatrixExtensionRoot,
|
|
matrixExtensionTestRoots,
|
|
} from "../test/vitest/vitest.extension-matrix-paths.mjs";
|
|
import { isMattermostExtensionRoot } from "../test/vitest/vitest.extension-mattermost-paths.mjs";
|
|
import { isMediaExtensionRoot } from "../test/vitest/vitest.extension-media-paths.mjs";
|
|
import { isMemoryExtensionRoot } from "../test/vitest/vitest.extension-memory-paths.mjs";
|
|
import { isMessagingExtensionRoot } from "../test/vitest/vitest.extension-messaging-paths.mjs";
|
|
import { isMiscExtensionRoot } from "../test/vitest/vitest.extension-misc-paths.mjs";
|
|
import { isMsTeamsExtensionRoot } from "../test/vitest/vitest.extension-msteams-paths.mjs";
|
|
import {
|
|
isProviderExtensionRoot,
|
|
isProviderOpenAiExtensionRoot,
|
|
} from "../test/vitest/vitest.extension-provider-paths.mjs";
|
|
import { isQaExtensionRoot } from "../test/vitest/vitest.extension-qa-paths.mjs";
|
|
import {
|
|
isTelegramExtensionRoot,
|
|
telegramExtensionTestRoots,
|
|
} from "../test/vitest/vitest.extension-telegram-paths.mjs";
|
|
import { isVoiceCallExtensionRoot } from "../test/vitest/vitest.extension-voice-call-paths.mjs";
|
|
import { isWhatsAppExtensionRoot } from "../test/vitest/vitest.extension-whatsapp-paths.mjs";
|
|
import { isZaloExtensionRoot } from "../test/vitest/vitest.extension-zalo-paths.mjs";
|
|
import {
|
|
isPluginSdkLightTarget,
|
|
pluginSdkLightTestFiles,
|
|
resolvePluginSdkLightIncludePattern,
|
|
} from "../test/vitest/vitest.plugin-sdk-paths.mjs";
|
|
import { fullSuiteVitestShards } 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 {
|
|
getUnitFastIsolatedTestFiles,
|
|
getUnitFastTestFiles,
|
|
getUnitFastTimerTestFiles,
|
|
resolveUnitFastIsolatedTestIncludePattern,
|
|
resolveUnitFastTestIncludePattern,
|
|
resolveUnitFastTimerTestIncludePattern,
|
|
} from "../test/vitest/vitest.unit-fast-paths.mjs";
|
|
import {
|
|
isBoundaryTestFile,
|
|
isBundledPluginDependentUnitTestFile,
|
|
} 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 } from "./lib/changed-path-facts.mjs";
|
|
import {
|
|
createExtensionTestProcessTargetChunks,
|
|
splitExtensionTestProcessTargets,
|
|
} from "./lib/extension-test-plan.mts";
|
|
import {
|
|
GATEWAY_SERVER_TEST_PROCESS_COUNT,
|
|
listGatewayServerTestTargets,
|
|
splitTestTargetChunks as splitTargetChunks,
|
|
} from "./lib/gateway-server-test-plan.mts";
|
|
import {
|
|
isCiLikeEnv,
|
|
resolveLocalFullSuiteProfile,
|
|
type VitestHostInfo,
|
|
} from "./lib/vitest-local-scheduling.mts";
|
|
import {
|
|
DEFAULT_VITEST_NO_OUTPUT_HEARTBEAT_MS,
|
|
resolveDefaultVitestNoOutputTimeoutMs,
|
|
resolveVitestCliEntry,
|
|
resolveVitestNodeArgs,
|
|
} from "./run-vitest.mts";
|
|
|
|
type VitestRunPlan = {
|
|
config: string;
|
|
forwardedArgs: 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;
|
|
forceFullImportGraph?: boolean;
|
|
includeExtensionImpact?: boolean;
|
|
};
|
|
|
|
type ChangedTestTargetPlan = {
|
|
mode: "none" | "broad" | "targets";
|
|
targets: string[];
|
|
skippedBroadFallbackPaths?: string[];
|
|
};
|
|
|
|
type ImportGraphOptions = { tooling?: boolean };
|
|
type VitestSpecShape = Pick<VitestRunSpec, "config" | "env">;
|
|
type WatchableVitestSpecShape = VitestSpecShape & Pick<VitestRunSpec, "watchMode">;
|
|
type ImportGraph = {
|
|
reverseImports: Map<string, string[]>;
|
|
reverseReexports: Map<string, string[]>;
|
|
testFiles: 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_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";
|
|
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";
|
|
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_VITEST_CONFIG =
|
|
"test/vitest/vitest.extension-codex-app-server-attempt.config.ts";
|
|
const EXTENSION_CODEX_APP_SERVER_ATTEMPT_EXTRA_VITEST_CONFIG =
|
|
"test/vitest/vitest.extension-codex-app-server-attempt-extra.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_CHANNELS_VITEST_CONFIG = "test/vitest/vitest.extension-channels.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_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_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_MATRIX_VITEST_CONFIG, matrixExtensionTestRoots],
|
|
[EXTENSION_TELEGRAM_VITEST_CONFIG, telegramExtensionTestRoots],
|
|
]);
|
|
|
|
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_VITEST_CONFIG, 168],
|
|
[EXTENSION_CODEX_APP_SERVER_ATTEMPT_EXTRA_VITEST_CONFIG, 118],
|
|
[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],
|
|
["test/vitest/vitest.tasks.config.ts", 165],
|
|
[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 resolveConfigSortWeight(config: string, shardTimings: ReadonlyMap<string, number>) {
|
|
return shardTimings.get(config) ?? (FULL_SUITE_CONFIG_WEIGHT.get(config) ?? 0) * 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 { config: string }>(
|
|
specs: T[],
|
|
shardTimings = new Map<string, number>(),
|
|
): T[] {
|
|
const sortedSpecs = specs.toSorted((a, b) => {
|
|
const weightDelta =
|
|
resolveConfigSortWeight(b.config, shardTimings) -
|
|
resolveConfigSortWeight(a.config, 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 TASKS_VITEST_CONFIG = "test/vitest/vitest.tasks.config.ts";
|
|
const PACKAGE_DOCKER_VITEST_CONFIG = "test/vitest/vitest.package-docker.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 PACKAGE_DOCKER_TEST_TARGET =
|
|
"test/e2e/qa-lab/runtime/package-openclaw-for-docker.e2e.test.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_TOOLING_TEST_TARGET_CHUNK_SIZE = 2;
|
|
const FULL_SUITE_UNIT_FAST_TEST_TARGET_CHUNK_SIZE = 70;
|
|
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_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,
|
|
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,
|
|
packageDocker: PACKAGE_DOCKER_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,
|
|
tasks: TASKS_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,
|
|
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,
|
|
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,
|
|
extensionChannel: EXTENSION_CHANNELS_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/system-agent/setup-inference-persist.ts",
|
|
"src/agents/embedded-agent-runner/run/attempt-dispatch-preparation.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",
|
|
],
|
|
],
|
|
]);
|
|
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/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",
|
|
];
|
|
const SOURCE_TEST_TARGETS = new Map([
|
|
...PRECISE_SOURCE_TEST_TARGETS,
|
|
["extensions/codex/package.json", CODEX_VERSION_CONTRACT_TEST_TARGETS],
|
|
["extensions/codex/src/app-server/version.ts", CODEX_VERSION_CONTRACT_TEST_TARGETS],
|
|
["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-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",
|
|
"src/auto-reply/reply/dispatch-from-config.test.ts",
|
|
],
|
|
],
|
|
["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",
|
|
"test",
|
|
];
|
|
const IMPORTABLE_FILE_EXTENSIONS = [".ts", ".tsx", ".mts", ".cts"];
|
|
const IMPORT_GRAPH_GREP_PATHS = SOURCE_ROOTS_FOR_IMPORT_GRAPH.flatMap((root) =>
|
|
IMPORTABLE_FILE_EXTENSIONS.map((ext) => `:(glob)${root}/**/*${ext}`),
|
|
);
|
|
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 = TOOLING_IMPORT_GRAPH_ROOTS.flatMap((root) =>
|
|
TOOLING_IMPORTABLE_FILE_EXTENSIONS.map((ext) => `:(glob)${root}/**/*${ext}`),
|
|
);
|
|
const IMPORT_SPECIFIER_PATTERN =
|
|
/\b(?:import|export)\s+(?:type\s+)?(?:[^'"]*?\s+from\s+)?["']([^"']+)["']|\bimport\s*\(\s*["']([^"']+)["']\s*\)/gu;
|
|
const REEXPORT_SPECIFIER_PATTERN =
|
|
/\bexport\s+(?:type\s+)?(?:\*\s+(?:as\s+\w+\s+)?from\s+|[^"']+?\s+from\s+)["']([^"']+)["']/gu;
|
|
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";
|
|
const VITEST_NO_OUTPUT_RETRY_ENV_KEY = "OPENCLAW_VITEST_NO_OUTPUT_RETRY";
|
|
/** Default no-output timeout applied to test-projects Vitest children. */
|
|
export const DEFAULT_TEST_PROJECTS_VITEST_NO_OUTPUT_TIMEOUT_MS = String(900_000);
|
|
/** Default heartbeat interval applied to test-projects Vitest children. */
|
|
export 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([
|
|
"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) : [],
|
|
),
|
|
)
|
|
// Explicit leaf targets bypass the config's live-test exclusion and produce an empty shard.
|
|
.filter(
|
|
(file) =>
|
|
file.endsWith(".test.ts") &&
|
|
!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),
|
|
);
|
|
}
|
|
|
|
function listAgentsCoreFullSuiteTestTargets(cwd: string) {
|
|
const isolatedTests = new Set(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 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 createBoundedExtensionPlans(plan: VitestRunPlan, env?: NodeJS.ProcessEnv) {
|
|
const { config, forwardedArgs, watchMode } = plan;
|
|
const roots = EXTENSION_TEST_PROCESS_ROOTS.get(config);
|
|
if (watchMode || !roots) {
|
|
return [plan];
|
|
}
|
|
// A CI include file already owns the test scope. Keep that file set, but
|
|
// still honor process lifetime so isolate:true configs cannot re-import
|
|
// a second heavy file in the same Vitest process.
|
|
const includeFilePath = env?.[INCLUDE_FILE_ENV_KEY]?.trim();
|
|
if (includeFilePath) {
|
|
if (!fs.existsSync(includeFilePath)) {
|
|
return [{ ...plan, includePatterns: null }];
|
|
}
|
|
const scopedTargets = loadIncludePatternsForSpecFilter(env ?? {}) ?? [];
|
|
const chunks = splitExtensionTestProcessTargets(config, scopedTargets);
|
|
if (chunks.length <= 1) {
|
|
return [{ ...plan, includePatterns: null }];
|
|
}
|
|
return chunks.map((includePatterns) => ({
|
|
config,
|
|
forwardedArgs,
|
|
includePatterns,
|
|
watchMode,
|
|
}));
|
|
}
|
|
const chunks = createExtensionTestProcessTargetChunks(config, roots, forwardedArgs);
|
|
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 isFileLikeTarget(arg: string) {
|
|
return /\.(?:test|spec)\.[cm]?[jt]sx?$/u.test(arg);
|
|
}
|
|
|
|
export function isTestFileTarget(arg: string) {
|
|
return /\.(?:test|spec)\.[cm]?[jt]sx?$/u.test(arg);
|
|
}
|
|
|
|
export function isTestSupportFileTarget(arg: string) {
|
|
if (/(?:^|\/)(?:test-helpers|test-support)(?:\/|$)/u.test(arg)) {
|
|
return true;
|
|
}
|
|
const basename = path.posix.basename(arg).replace(/\.[cm]?[jt]sx?$/u, "");
|
|
return /(?:^|[._-])(?:suite|test-(?:helpers|support))(?:[._-]|$)/u.test(basename);
|
|
}
|
|
|
|
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) ||
|
|
isFileLikeTarget(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 toScopedIncludePattern(arg: string, cwd: string) {
|
|
const relative = toRepoRelativeTarget(arg, cwd);
|
|
if (isGlobTarget(relative) || isFileLikeTarget(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",
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
},
|
|
);
|
|
if (result.status !== 0) {
|
|
return null;
|
|
}
|
|
return result.stdout
|
|
.split("\0")
|
|
.map((line) => normalizePathPattern(line.trim()))
|
|
.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}.`;
|
|
const targets = listExplicitTestTargetFilesForCwd(cwd).filter(
|
|
(file) =>
|
|
fs.existsSync(path.join(cwd, file)) &&
|
|
path.posix.dirname(file) === directory &&
|
|
file.startsWith(prefix) &&
|
|
isTestFileTarget(file),
|
|
);
|
|
return targets.length > 0 ? targets.toSorted((left, right) => left.localeCompare(right)) : null;
|
|
}
|
|
|
|
function includePatternMatchesAnyFile(pattern: string, files: string[]) {
|
|
return files.some((file) => file === pattern || path.matchesGlob(file, pattern));
|
|
}
|
|
|
|
function resolveExplicitSourceTestTargets(
|
|
targetArg: string,
|
|
cwd: string,
|
|
options: Pick<ChangedTestTargetOptions, "forceFullImportGraph"> = {},
|
|
) {
|
|
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,
|
|
});
|
|
if (preciseTargets && 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 expandExplicitSourceTestTargets(targetArgs: string[], cwd: string) {
|
|
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);
|
|
const prefixTargets = resolveExplicitTestPrefixTargets(targetArg, cwd);
|
|
if (prefixTargets) {
|
|
return prefixTargets;
|
|
}
|
|
if (relative === "src/commands" && isExistingDirectoryTarget(targetArg, cwd)) {
|
|
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 (isExistingDirectoryTarget(targetArg, cwd)) {
|
|
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,
|
|
});
|
|
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/tasks",
|
|
"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 } = 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);
|
|
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;
|
|
}
|
|
|
|
const absolute = path.resolve(cwd, targetArg);
|
|
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;
|
|
}
|
|
|
|
if (isTestFileTarget(relative)) {
|
|
continue;
|
|
}
|
|
|
|
const explicitSupportTargets = resolveExplicitSourceTestTargets(targetArg, cwd);
|
|
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,
|
|
) {
|
|
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 (!isSkippedImportGraphDirectory(entry.name)) {
|
|
listImportGraphFiles(cwd, relative, files, extensions);
|
|
}
|
|
continue;
|
|
}
|
|
if (entry.isFile() && extensions.some((ext) => relative.endsWith(ext))) {
|
|
files.push(relative);
|
|
}
|
|
}
|
|
return files;
|
|
}
|
|
|
|
function resolveImportSpecifier(
|
|
importer: string,
|
|
specifier: string,
|
|
fileSet: ReadonlySet<string>,
|
|
extensions: readonly string[] = IMPORTABLE_FILE_EXTENSIONS,
|
|
) {
|
|
if (!specifier.startsWith(".")) {
|
|
return null;
|
|
}
|
|
|
|
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}`),
|
|
);
|
|
}
|
|
|
|
return candidates.find((candidate) => fileSet.has(candidate)) ?? null;
|
|
}
|
|
|
|
let cachedImportGraph: ImportGraph | null = null;
|
|
let cachedImportGraphCwd: string | null = null;
|
|
const cachedImportGraphFiles = new Map<string, string[]>();
|
|
const cachedImportGraphGrepMatches = new Map<string, string[] | null>();
|
|
const cachedDirectImporters = new Map<string, string[] | null>();
|
|
|
|
function isImportableGraphFile(relative: string) {
|
|
return IMPORTABLE_FILE_EXTENSIONS.some((ext) => relative.endsWith(ext));
|
|
}
|
|
|
|
function listImportGraphFilesFromGit(
|
|
cwd: string,
|
|
roots: readonly string[],
|
|
extensions: readonly string[],
|
|
) {
|
|
const result = spawnSync("git", ["ls-files", "--", ...roots], {
|
|
cwd,
|
|
encoding: "utf8",
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
});
|
|
if (result.status !== 0) {
|
|
return null;
|
|
}
|
|
return result.stdout
|
|
.split("\n")
|
|
.map((line) => normalizePathPattern(line.trim()))
|
|
.filter((line) => line.length > 0 && extensions.some((ext) => line.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 =
|
|
listImportGraphFilesFromGit(cwd, roots, extensions) ??
|
|
roots.flatMap((root) => listImportGraphFiles(cwd, root, [], extensions));
|
|
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(stripImportableGraphExtension(relative, extensions));
|
|
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 listImportGraphGrepMatches(cwd: string, term: string, options: ImportGraphOptions = {}) {
|
|
const tooling = options.tooling === true;
|
|
const cacheKey = `${cwd}\0${tooling ? "tooling" : "source"}\0${term}`;
|
|
if (cachedImportGraphGrepMatches.has(cacheKey)) {
|
|
return cachedImportGraphGrepMatches.get(cacheKey) ?? null;
|
|
}
|
|
|
|
const roots = tooling ? TOOLING_IMPORT_GRAPH_ROOTS : SOURCE_ROOTS_FOR_IMPORT_GRAPH;
|
|
const extensions = tooling ? TOOLING_IMPORTABLE_FILE_EXTENSIONS : IMPORTABLE_FILE_EXTENSIONS;
|
|
let result = spawnSync(
|
|
"rg",
|
|
[
|
|
"--files-with-matches",
|
|
"--fixed-strings",
|
|
"--hidden",
|
|
"--no-ignore",
|
|
...extensions.flatMap((ext) => ["--glob", `*${ext}`]),
|
|
"--glob",
|
|
"!**/node_modules/**",
|
|
"--glob",
|
|
"!**/dist/**",
|
|
"--glob",
|
|
"!**/vendor/**",
|
|
term,
|
|
"--",
|
|
...roots,
|
|
],
|
|
{
|
|
cwd,
|
|
encoding: "utf8",
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
},
|
|
);
|
|
if (result.error || (result.status !== 0 && result.status !== 1)) {
|
|
result = spawnSync(
|
|
"git",
|
|
[
|
|
"grep",
|
|
"-l",
|
|
"--fixed-strings",
|
|
term,
|
|
"--",
|
|
...(tooling ? TOOLING_IMPORT_GRAPH_GREP_PATHS : IMPORT_GRAPH_GREP_PATHS),
|
|
],
|
|
{
|
|
cwd,
|
|
encoding: "utf8",
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
},
|
|
);
|
|
}
|
|
if (result.status === 1) {
|
|
cachedImportGraphGrepMatches.set(cacheKey, []);
|
|
return [];
|
|
}
|
|
if (result.status !== 0) {
|
|
cachedImportGraphGrepMatches.set(cacheKey, null);
|
|
return null;
|
|
}
|
|
const trackedFiles = new Set(listImportGraphFilesForCwd(cwd, { tooling }));
|
|
const matches = result.stdout
|
|
.split("\n")
|
|
.map((line) => normalizePathPattern(line.trim()))
|
|
.filter(
|
|
(line) =>
|
|
line.length > 0 &&
|
|
trackedFiles.has(line) &&
|
|
(tooling
|
|
? TOOLING_IMPORTABLE_FILE_EXTENSIONS.some((ext) => line.endsWith(ext))
|
|
: isImportableGraphFile(line)),
|
|
)
|
|
.toSorted((left, right) => left.localeCompare(right));
|
|
cachedImportGraphGrepMatches.set(cacheKey, matches);
|
|
return matches;
|
|
}
|
|
|
|
function findDirectImportersWithGitGrep(
|
|
cwd: string,
|
|
importedFile: string,
|
|
fileSet: ReadonlySet<string>,
|
|
options: ImportGraphOptions = {},
|
|
) {
|
|
const tooling = options.tooling === true;
|
|
const cacheKey = `${cwd}\0${tooling ? "tooling" : "source"}\0${importedFile}`;
|
|
if (cachedDirectImporters.has(cacheKey)) {
|
|
return cachedDirectImporters.get(cacheKey) ?? null;
|
|
}
|
|
|
|
const extensions = tooling ? TOOLING_IMPORTABLE_FILE_EXTENSIONS : IMPORTABLE_FILE_EXTENSIONS;
|
|
const terms = resolveImportGraphSearchTerms(importedFile, extensions);
|
|
if (terms.length === 0) {
|
|
cachedDirectImporters.set(cacheKey, null);
|
|
return null;
|
|
}
|
|
|
|
let skippedBroadTerm = false;
|
|
const importers: string[] = [];
|
|
for (const term of terms) {
|
|
const candidates = listImportGraphGrepMatches(cwd, term, { tooling });
|
|
if (!candidates) {
|
|
cachedDirectImporters.set(cacheKey, null);
|
|
return null;
|
|
}
|
|
if (candidates.length > 800) {
|
|
skippedBroadTerm = true;
|
|
continue;
|
|
}
|
|
for (const file of candidates) {
|
|
if (file === importedFile || !fileSet.has(file) || importers.includes(file)) {
|
|
continue;
|
|
}
|
|
let source;
|
|
try {
|
|
source = fs.readFileSync(path.join(cwd, file), "utf8");
|
|
} catch {
|
|
continue;
|
|
}
|
|
for (const match of source.matchAll(IMPORT_SPECIFIER_PATTERN)) {
|
|
const imported = resolveImportSpecifier(
|
|
file,
|
|
match[1] ?? match[2] ?? "",
|
|
fileSet,
|
|
extensions,
|
|
);
|
|
if (imported === importedFile) {
|
|
importers.push(file);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (importedFile.startsWith("test/helpers/") && importers.length > 0 && term.includes("/")) {
|
|
break;
|
|
}
|
|
}
|
|
const result =
|
|
skippedBroadTerm && importers.length === 0 && !importedFile.startsWith("test/helpers/")
|
|
? null
|
|
: importers;
|
|
cachedDirectImporters.set(cacheKey, result);
|
|
return result;
|
|
}
|
|
|
|
function resolveAffectedTestsFromTargetedImportScan(
|
|
changedPath: string,
|
|
cwd: string,
|
|
options: ImportGraphOptions & { direct?: boolean } = {},
|
|
) {
|
|
const normalized = normalizePathPattern(changedPath);
|
|
const tooling = options.tooling === true;
|
|
const files = listImportGraphFilesForCwd(cwd, { tooling });
|
|
const fileSet = new Set(files);
|
|
if (!fileSet.has(normalized)) {
|
|
return [];
|
|
}
|
|
|
|
const testFiles = new Set(
|
|
files.filter((file) => isTestFileTarget(file) && !file.endsWith(".live.test.ts")),
|
|
);
|
|
const queue = [normalized];
|
|
const seen = new Set(queue);
|
|
const targets = [];
|
|
|
|
for (const current of queue) {
|
|
const importers = findDirectImportersWithGitGrep(cwd, current, fileSet, { tooling });
|
|
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);
|
|
continue;
|
|
}
|
|
if (options.direct !== true) {
|
|
queue.push(importer);
|
|
}
|
|
}
|
|
}
|
|
|
|
return [...new Set(targets)].toSorted((left, right) => left.localeCompare(right));
|
|
}
|
|
|
|
function getImportGraph(cwd: string) {
|
|
if (cachedImportGraph && cachedImportGraphCwd === cwd) {
|
|
return cachedImportGraph;
|
|
}
|
|
|
|
const files = listImportGraphFilesForCwd(cwd);
|
|
const fileSet = new Set(files);
|
|
const reverseImports = new Map<string, string[]>();
|
|
const reverseReexports = new Map<string, string[]>();
|
|
const testFiles = new Set(
|
|
files.filter((file) => isTestFileTarget(file) && !file.endsWith(".live.test.ts")),
|
|
);
|
|
|
|
for (const file of files) {
|
|
let source;
|
|
try {
|
|
source = fs.readFileSync(path.join(cwd, file), "utf8");
|
|
} catch {
|
|
continue;
|
|
}
|
|
for (const match of source.matchAll(IMPORT_SPECIFIER_PATTERN)) {
|
|
const imported = resolveImportSpecifier(file, match[1] ?? match[2] ?? "", fileSet);
|
|
if (!imported) {
|
|
continue;
|
|
}
|
|
const importers = reverseImports.get(imported) ?? [];
|
|
importers.push(file);
|
|
reverseImports.set(imported, importers);
|
|
}
|
|
for (const match of source.matchAll(REEXPORT_SPECIFIER_PATTERN)) {
|
|
const imported = resolveImportSpecifier(file, match[1] ?? "", fileSet);
|
|
if (!imported) {
|
|
continue;
|
|
}
|
|
const importers = reverseReexports.get(imported) ?? [];
|
|
importers.push(file);
|
|
reverseReexports.set(imported, importers);
|
|
}
|
|
}
|
|
|
|
cachedImportGraph = { reverseImports, reverseReexports, testFiles };
|
|
cachedImportGraphCwd = cwd;
|
|
return cachedImportGraph;
|
|
}
|
|
|
|
/** Returns whether any changed path reaches one of the requested import-graph targets. */
|
|
export function hasImportGraphImpactOnTargets(
|
|
changedPaths: string[],
|
|
targetPaths: string[],
|
|
cwd = process.cwd(),
|
|
) {
|
|
const targets = new Set(targetPaths.map(normalizePathPattern));
|
|
if (targets.size === 0) {
|
|
return false;
|
|
}
|
|
|
|
const { reverseImports, reverseReexports } = getImportGraph(cwd);
|
|
for (const changedPath of changedPaths) {
|
|
const queue = [normalizePathPattern(changedPath)];
|
|
const seen = new Set(queue);
|
|
for (const current of queue) {
|
|
if (targets.has(current)) {
|
|
return true;
|
|
}
|
|
const importers = [
|
|
...(reverseImports.get(current) ?? []),
|
|
...(reverseReexports.get(current) ?? []),
|
|
];
|
|
for (const importer of importers) {
|
|
if (!seen.has(importer)) {
|
|
seen.add(importer);
|
|
queue.push(importer);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function resolveAffectedTestsFromImportGraph(
|
|
changedPath: string,
|
|
cwd: string,
|
|
options: { forceFull?: boolean } = {},
|
|
) {
|
|
const normalized = normalizePathPattern(changedPath);
|
|
if (options.forceFull !== true) {
|
|
const targetedTargets = resolveAffectedTestsFromTargetedImportScan(normalized, cwd);
|
|
if (targetedTargets !== null) {
|
|
return targetedTargets;
|
|
}
|
|
}
|
|
|
|
const { reverseImports, testFiles } = getImportGraph(cwd);
|
|
const queue = [normalized];
|
|
const seen = new Set(queue);
|
|
const targets = [];
|
|
|
|
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);
|
|
}
|
|
queue.push(importer);
|
|
}
|
|
}
|
|
|
|
return [...new Set(targets)].toSorted((left, right) => left.localeCompare(right));
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
function isVitestConfigTargetForKind(kind: string, targetArg: string, cwd: string) {
|
|
return resolveVitestConfigTargetKind(toRepoRelativeTarget(targetArg, cwd)) === kind;
|
|
}
|
|
|
|
function isControlUiE2eTarget(relative: string) {
|
|
return (
|
|
relative === "ui/src/test-helpers/control-ui-e2e.ts" ||
|
|
relative === "ui/src/e2e" ||
|
|
relative.startsWith("ui/src/e2e/") ||
|
|
(relative.startsWith("ui/src/") && 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()) {
|
|
const targets = [];
|
|
for (const changedPath of changedPaths) {
|
|
const testTargets =
|
|
SOURCE_TEST_TARGETS.get(changedPath) ?? resolveToolingTestTargets(changedPath, cwd);
|
|
if (!testTargets) {
|
|
return null;
|
|
}
|
|
targets.push(...testTargets);
|
|
}
|
|
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-guards.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 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.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[]>([
|
|
[
|
|
".github/workflows/mantis-telegram-live.yml",
|
|
["mantis-telegram-desktop-proof-workflow", packageAcceptance, workflowGuards],
|
|
],
|
|
[
|
|
".github/workflows/openclaw-live-and-e2e-checks-reusable.yml",
|
|
[packageAcceptance, workflowGuards, "release-workflow-matrix-plan", installDocker],
|
|
],
|
|
[
|
|
".github/workflows/plugin-npm-release.yml",
|
|
[packageAcceptance, "plugin-npm-extended-stable-workflow", workflowGuards],
|
|
],
|
|
[".github/workflows/qa-live-transports-convex.yml", [packageAcceptance, workflowGuards]],
|
|
[".github/workflows/update-migration.yml", [packageAcceptance, workflowGuards]],
|
|
[
|
|
".github/actions/setup-node-env/action.yml",
|
|
["install-trufflehog", packageAcceptance, workflowGuards],
|
|
],
|
|
[".github/actions/setup-node-env/dependency-fingerprint.mjs", [workflowGuards]],
|
|
[".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",
|
|
["docker-e2e-seeds", "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/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",
|
|
"ios-version",
|
|
"openclaw-npm-extended-stable-release",
|
|
"openclaw-npm-publish",
|
|
"release-preflight",
|
|
"release-prepare",
|
|
"release-upgrade-baseline",
|
|
"release-version",
|
|
"upgrade-survivor-baselines",
|
|
"upgrade-survivor-config-recipe",
|
|
],
|
|
],
|
|
[
|
|
"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/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"]],
|
|
["scripts/lib/failed-trailer.mts", ["run-oxlint", "run-tsgo", "run-vitest", "changed-lanes"]],
|
|
["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/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",
|
|
"openclaw-test-state",
|
|
],
|
|
],
|
|
[
|
|
"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[]]> = [
|
|
[/^scripts\/pr$/u, ["pr-merge", "pr-operation-lock", "pr-wrappers"]],
|
|
[/^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,
|
|
"changed-lanes",
|
|
"check-workflows",
|
|
"plugin-contract-test-plan",
|
|
pluginPrerelease,
|
|
"verify-pr-hosted-gates",
|
|
],
|
|
],
|
|
[
|
|
/^\.github\/workflows\/ci-check-testbox\.yml$/u,
|
|
[workflowGuards, packageAcceptance, "changed-lanes", "install-trufflehog"],
|
|
],
|
|
[
|
|
/^\.github\/workflows\/ci-check-arm-testbox\.yml$/u,
|
|
[workflowGuards, packageAcceptance, "install-trufflehog"],
|
|
],
|
|
[/^\.github\/workflows\/crabbox-hydrate\.yml$/u, [workflowGuards, packageAcceptance]],
|
|
[
|
|
/^\.github\/workflows\/ci-build-artifacts-testbox\.yml$/u,
|
|
["install-trufflehog", packageAcceptance, workflowGuards],
|
|
],
|
|
[
|
|
/^\.github\/workflows\/full-release-validation\.yml$/u,
|
|
["src/dockerfile.test.ts", packageAcceptance, pluginPrerelease],
|
|
],
|
|
[
|
|
/^\.github\/workflows\/openclaw-release-checks\.yml$/u,
|
|
[packageAcceptance, crossOsReleaseChecks, pluginPrerelease, installDocker],
|
|
],
|
|
[/^\.github\/workflows\/docker-release\.yml$/u, ["src/dockerfile.test.ts"]],
|
|
[/^\.github\/workflows\/install-smoke\.yml$/u, ["install-smoke-no-push-workflow", installDocker]],
|
|
[/^\.github\/workflows\/openclaw-performance\.yml$/u, ["openclaw-performance-workflow"]],
|
|
[/^\.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|package-acceptance)\.yml$/u,
|
|
[packageAcceptance],
|
|
],
|
|
[
|
|
/^\.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-telegram-desktop-proof\.yml$/u,
|
|
["mantis-telegram-desktop-proof-workflow", packageAcceptance, workflowGuards],
|
|
],
|
|
[
|
|
/^\.github\/workflows\/mantis-web-ui-chat-proof\.yml$/u,
|
|
["mantis-web-ui-chat-proof-workflow", packageAcceptance, workflowGuards],
|
|
],
|
|
[/^\.github\/workflows\/android-release\.yml$/u, [packageAcceptance, workflowGuards]],
|
|
[/^\.github\/actions\/ensure-base-commit\/action\.yml$/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\.mjs$/u, ["direct-run-entrypoints"]],
|
|
[
|
|
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\.sh$/u, ["pr-merge"]],
|
|
[/^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$/u,
|
|
["build-all", "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", "dependency-guard-workflow"],
|
|
],
|
|
[
|
|
/^scripts\/github\/(?:security-sensitive-guard|guard-shared)\.mjs$/u,
|
|
["security-sensitive-guard-script", "security-sensitive-guard-workflow"],
|
|
],
|
|
[/^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,
|
|
[
|
|
"src/docker-setup.e2e.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"]],
|
|
[
|
|
/^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, ["plugin-publication-artifact"]],
|
|
[
|
|
/^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\.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/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",
|
|
],
|
|
],
|
|
[/^scripts\/e2e\/system-agent-(?:first-run|rescue)-docker\.sh$/u, ["docker-e2e-system-agent"]],
|
|
[
|
|
/^test\/e2e\/qa-lab\/runtime\/system-agent-first-run-docker-client\.ts$/u,
|
|
[
|
|
"docker-e2e-system-agent",
|
|
"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/setup-inference.test.ts",
|
|
"src/system-agent/audit.test.ts",
|
|
],
|
|
],
|
|
[
|
|
/^scripts\/e2e\/system-agent-first-run-spec\.json$/u,
|
|
[
|
|
"docker-e2e-system-agent",
|
|
"src/system-agent/operations.test.ts",
|
|
"src/system-agent/audit.test.ts",
|
|
],
|
|
],
|
|
[
|
|
/^scripts\/e2e\/system-agent-rescue-docker-client\.ts$/u,
|
|
[
|
|
"docker-e2e-system-agent",
|
|
"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|mcp-code-mode-gateway|cron-mcp-cleanup)-seed\.ts)$/u,
|
|
["docker-e2e-seeds"],
|
|
],
|
|
[
|
|
/^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", "docker-e2e-seeds"],
|
|
],
|
|
[
|
|
/^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 resolveGithubYamlGuardTargets(changedPath: string) {
|
|
if (/^\.github\/workflows\/[^/]+\.ya?ml$/u.test(changedPath)) {
|
|
return GITHUB_YAML_PINNING_GUARD_TEST_TARGETS;
|
|
}
|
|
if (/^\.github\/actions\/.+\.ya?ml$/u.test(changedPath)) {
|
|
return GITHUB_YAML_PINNING_GUARD_TEST_TARGETS;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function resolveDirectToolingReferenceTests(changedPath: string, cwd: string) {
|
|
const normalized = normalizePathPattern(changedPath);
|
|
return (listImportGraphGrepMatches(cwd, normalized, { tooling: true }) ?? []).filter(
|
|
(file) =>
|
|
file !== "test/scripts/test-projects.test.ts" &&
|
|
!file.endsWith(".live.test.ts") &&
|
|
isTestFileTarget(file) &&
|
|
fs
|
|
.readFileSync(path.join(cwd, file), "utf8")
|
|
.match(/[A-Za-z0-9_.@+/-]{4,}/gu)
|
|
?.includes(normalized),
|
|
);
|
|
}
|
|
|
|
function resolveToolingTestTargets(changedPath: string, cwd = process.cwd()) {
|
|
if (changedPath.startsWith("test/scripts/") && isTestFileTarget(changedPath)) {
|
|
return [changedPath];
|
|
}
|
|
if (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 exactOwners = EXACT_TOOLING_TARGETS.get(implementationPath);
|
|
if (exactOwners) {
|
|
return resolveToolingTestOwnerTargets(...exactOwners);
|
|
}
|
|
const semanticTargets = resolveSemanticToolingTargets(implementationPath);
|
|
const facts = getChangedPathFacts(changedPath);
|
|
const hasToolingOwner =
|
|
semanticTargets.length > 0 ||
|
|
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/"));
|
|
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(
|
|
explicitTargets?.length ||
|
|
githubYamlGuardTargets?.length ||
|
|
semanticTargets.length ||
|
|
conventionalTargets?.length,
|
|
);
|
|
const importGraphResult =
|
|
!hasDirectOwner &&
|
|
TOOLING_IMPORTABLE_FILE_EXTENSIONS.some((ext) => implementationPath.endsWith(ext))
|
|
? resolveAffectedTestsFromTargetedImportScan(implementationPath, cwd, {
|
|
tooling: true,
|
|
direct: true,
|
|
})
|
|
: [];
|
|
const importGraphTargets = importGraphResult ?? [];
|
|
const referenceTargets =
|
|
semanticTargets.length === 0 && (githubYamlGuardTargets || !hasDirectOwner)
|
|
? resolveDirectToolingReferenceTests(implementationPath, cwd)
|
|
: [];
|
|
const targets = [
|
|
...(explicitTargets ?? []),
|
|
...semanticTargets,
|
|
...(conventionalTargets ?? []),
|
|
...importGraphTargets,
|
|
...referenceTargets,
|
|
...(githubYamlGuardTargets ?? []),
|
|
];
|
|
if (targets.length > 0) {
|
|
return uniqueOrdered(targets);
|
|
}
|
|
return 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) {
|
|
return changedPath.startsWith("test/helpers/");
|
|
}
|
|
|
|
function shouldRouteChangedTargetWithoutImportGraph(changedPath: string) {
|
|
return changedPath.endsWith(".live.test.ts") || changedPath.startsWith("ui/src/");
|
|
}
|
|
|
|
function resolvePromptSnapshotFixtureTargets(changedPath: string) {
|
|
if (!/^test\/fixtures\/agents\/prompt-snapshots\/.+\.(?:json|md)$/u.test(changedPath)) {
|
|
return null;
|
|
}
|
|
return ["test/scripts/prompt-snapshots.test.ts"];
|
|
}
|
|
|
|
function resolveAppcastTargets(changedPath: string) {
|
|
return changedPath === "appcast.xml" ? APPCAST_TEST_TARGETS : null;
|
|
}
|
|
|
|
function resolvePreciseChangedTestTargets(
|
|
changedPath: string,
|
|
options: ChangedTestTargetOptions & { skipImportGraph?: boolean },
|
|
) {
|
|
const cwd = options.cwd ?? process.cwd();
|
|
const mappedTargets =
|
|
SOURCE_TEST_TARGETS.get(changedPath) ??
|
|
(/^extensions\/[^/]+\/openclaw\.plugin\.json$/u.test(changedPath)
|
|
? [changedPath, DOCS_CONFIG_EXAMPLES_TEST_TARGET]
|
|
: null) ??
|
|
resolveToolingTestTargets(changedPath, cwd) ??
|
|
resolveAppcastTargets(changedPath) ??
|
|
resolvePromptSnapshotFixtureTargets(changedPath);
|
|
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 (shouldRouteChangedTargetWithoutImportGraph(changedPath)) {
|
|
return changedPath.startsWith("ui/src/") ? [changedPath] : null;
|
|
}
|
|
if (options.skipImportGraph === true) {
|
|
return 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,
|
|
});
|
|
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));
|
|
}
|
|
|
|
/**
|
|
* 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();
|
|
const executableChangedPaths = changedPaths.filter(
|
|
(changedPath) => !isDeletedChangedTestTarget(changedPath, cwd),
|
|
);
|
|
const toolingTargets = resolveToolingChangedTestTargets(executableChangedPaths, cwd);
|
|
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: [...new Set(targets)],
|
|
};
|
|
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,
|
|
});
|
|
}
|
|
|
|
function classifyTarget(arg: string, cwd: string) {
|
|
const relative = toRepoRelativeTarget(arg, cwd);
|
|
const configTargetKind = resolveVitestConfigTargetKind(relative);
|
|
if (configTargetKind) {
|
|
return configTargetKind;
|
|
}
|
|
if (isAgentsCoreIsolatedTestFile(relative)) {
|
|
return agentVitestProjectOwners.coreIsolated.kind;
|
|
}
|
|
if (isControlUiE2eTarget(relative)) {
|
|
return "uiE2e";
|
|
}
|
|
if (relative === PACKAGE_DOCKER_TEST_TARGET) {
|
|
return "packageDocker";
|
|
}
|
|
if (isUiIsolatedTestFile(relative)) {
|
|
return "uiIsolated";
|
|
}
|
|
if (isPathAtOrUnder(relative, "ui/src")) {
|
|
return "ui";
|
|
}
|
|
if (relative.startsWith("src/tui/tui-pty-")) {
|
|
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" ||
|
|
relative === "src/gateway/sessions-history-http.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 (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("/");
|
|
const splitChannelShard = resolveSplitChannelExtensionShard(extensionRoot);
|
|
if (splitChannelShard) {
|
|
return splitChannelShard.kind;
|
|
}
|
|
if (isProviderOpenAiExtensionRoot(extensionRoot)) {
|
|
return "extensionProviderOpenAi";
|
|
}
|
|
if (isQaExtensionRoot(extensionRoot)) {
|
|
return "extensionQa";
|
|
}
|
|
if (isChannelSurfaceTestFile(relative)) {
|
|
return "extensionChannel";
|
|
}
|
|
if (isAcpxExtensionRoot(extensionRoot)) {
|
|
return "extensionAcpx";
|
|
}
|
|
if (isActiveMemoryExtensionRoot(extensionRoot)) {
|
|
return "extensionActiveMemory";
|
|
}
|
|
if (isCodexExtensionRoot(extensionRoot)) {
|
|
return "extensionCodex";
|
|
}
|
|
if (isDiffsExtensionRoot(extensionRoot)) {
|
|
return "extensionDiffs";
|
|
}
|
|
if (isBrowserExtensionRoot(extensionRoot)) {
|
|
return "extensionBrowser";
|
|
}
|
|
if (isFeishuExtensionRoot(extensionRoot)) {
|
|
return "extensionFeishu";
|
|
}
|
|
if (isIrcExtensionRoot(extensionRoot)) {
|
|
return "extensionIrc";
|
|
}
|
|
if (isMattermostExtensionRoot(extensionRoot)) {
|
|
return "extensionMattermost";
|
|
}
|
|
if (isTelegramExtensionRoot(extensionRoot)) {
|
|
return "extensionTelegram";
|
|
}
|
|
if (isVoiceCallExtensionRoot(extensionRoot)) {
|
|
return "extensionVoiceCall";
|
|
}
|
|
if (isWhatsAppExtensionRoot(extensionRoot)) {
|
|
return "extensionWhatsApp";
|
|
}
|
|
if (isZaloExtensionRoot(extensionRoot)) {
|
|
return "extensionZalo";
|
|
}
|
|
if (isMatrixExtensionRoot(extensionRoot)) {
|
|
return "extensionMatrix";
|
|
}
|
|
if (isMediaExtensionRoot(extensionRoot)) {
|
|
return "extensionMedia";
|
|
}
|
|
if (isMemoryExtensionRoot(extensionRoot)) {
|
|
return "extensionMemory";
|
|
}
|
|
if (isMsTeamsExtensionRoot(extensionRoot)) {
|
|
return "extensionMsTeams";
|
|
}
|
|
if (isMessagingExtensionRoot(extensionRoot)) {
|
|
return "extensionMessaging";
|
|
}
|
|
if (isMiscExtensionRoot(extensionRoot)) {
|
|
return "extensionMisc";
|
|
}
|
|
return isProviderExtensionRoot(extensionRoot) ? "extensionProvider" : "extension";
|
|
}
|
|
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 "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/tasks")) {
|
|
return "tasks";
|
|
}
|
|
if (isPathAtOrUnder(relative, "src/tui")) {
|
|
return "tui";
|
|
}
|
|
if (isPathAtOrUnder(relative, "src/acp")) {
|
|
return "acp";
|
|
}
|
|
if (isCliProcessTestFile(relative)) {
|
|
return "cliProcess";
|
|
}
|
|
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 isFileLikeTarget(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);
|
|
if (isTestFileTarget(relative)) {
|
|
return false;
|
|
}
|
|
return relative.startsWith("ui/src/");
|
|
}
|
|
|
|
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 = [];
|
|
const targetArgs = [];
|
|
let watchMode = false;
|
|
let passthrough = false;
|
|
|
|
for (const arg of args) {
|
|
if (arg === "--") {
|
|
if (targetArgs.length > 0) {
|
|
passthrough = true;
|
|
}
|
|
continue;
|
|
}
|
|
if (passthrough) {
|
|
if (arg === "--watch") {
|
|
watchMode = true;
|
|
}
|
|
forwardedArgs.push(arg);
|
|
continue;
|
|
}
|
|
if (arg === "--watch") {
|
|
watchMode = true;
|
|
continue;
|
|
}
|
|
if (isPathLikeTargetArg(arg, cwd)) {
|
|
targetArgs.push(arg);
|
|
}
|
|
forwardedArgs.push(arg);
|
|
}
|
|
|
|
return { forwardedArgs, targetArgs, watchMode };
|
|
}
|
|
|
|
export function buildVitestRunPlans(
|
|
args: string[],
|
|
cwd = process.cwd(),
|
|
listChangedPaths: (baseRef: string, cwd: string) => string[] = listChangedPathsFromGit,
|
|
options: ChangedTestTargetOptions = {},
|
|
) {
|
|
const { forwardedArgs, targetArgs, watchMode } = parseTestProjectsArgs(args, cwd);
|
|
const changedTargetArgs =
|
|
targetArgs.length === 0 ? resolveChangedTargetArgs(args, cwd, listChangedPaths, options) : null;
|
|
const requestedTargetArgs = changedTargetArgs ?? targetArgs;
|
|
const activeTargetArgs = expandBroadToolingScriptTargets(
|
|
expandExplicitSourceTestTargets(requestedTargetArgs, cwd),
|
|
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 = activeForwardedArgs.filter((arg) => !requestedTargetArgs.includes(arg));
|
|
const explicitConfigTargets = activeTargetArgs.map((targetArg) =>
|
|
toRepoRelativeTarget(targetArg, cwd),
|
|
);
|
|
if (explicitConfigTargets.every(isVitestConfigFileTarget)) {
|
|
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.flatMap((config) =>
|
|
createBoundedExtensionPlans(
|
|
{
|
|
config,
|
|
forwardedArgs: nonTargetArgs,
|
|
includePatterns: null,
|
|
watchMode,
|
|
},
|
|
options.env,
|
|
),
|
|
);
|
|
}
|
|
|
|
const groupedTargets = new Map<string, string[]>();
|
|
for (const targetArg of activeTargetArgs) {
|
|
if (!watchMode && toRepoRelativeTarget(targetArg, cwd) === 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;
|
|
}
|
|
|
|
const kind = classifyTarget(targetArg, cwd);
|
|
const current = groupedTargets.get(kind) ?? [];
|
|
current.push(targetArg);
|
|
groupedTargets.set(kind, 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 impliedToolingIsolatedTargets = !watchMode
|
|
? toolingIsolatedTestFiles.filter((file) =>
|
|
toolingTargets.some((targetArg) =>
|
|
includePatternMatchesAnyFile(toScopedIncludePattern(targetArg, cwd), [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 impliedUiIsolatedTargets = uiIsolatedTestFiles.filter((file) =>
|
|
uiTargets.some((targetArg) =>
|
|
includePatternMatchesAnyFile(toScopedIncludePattern(targetArg, cwd), [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);
|
|
}
|
|
const cliTargets = groupedTargets.get("cli") ?? [];
|
|
const impliedCliProcessTargets = cliProcessTestFiles.filter((file) =>
|
|
cliTargets.some((targetArg) =>
|
|
includePatternMatchesAnyFile(toScopedIncludePattern(targetArg, cwd), [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);
|
|
}
|
|
|
|
if (watchMode && groupedTargets.size > 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",
|
|
);
|
|
}
|
|
|
|
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, options.env));
|
|
}
|
|
continue;
|
|
}
|
|
const config = VITEST_CONFIG_BY_KIND[kind] ?? DEFAULT_VITEST_CONFIG;
|
|
const useCliTargetArgs =
|
|
kind === "e2e" ||
|
|
kind === "packageDocker" ||
|
|
grouped.every((targetArg) => isCanonicalAgentOwnerDirectoryTarget(targetArg, cwd)) ||
|
|
(kind === "default" &&
|
|
grouped.every((targetArg) => isFileLikeTarget(toRepoRelativeTarget(targetArg, cwd))));
|
|
const useWholeConfigTarget = grouped.some((targetArg) =>
|
|
shouldUseWholeConfigTarget(kind, targetArg, cwd),
|
|
);
|
|
const includePatterns = useCliTargetArgs
|
|
? null
|
|
: useWholeConfigTarget
|
|
? null
|
|
: uniqueOrdered(
|
|
grouped.flatMap((targetArg) => {
|
|
const lightLanePatterns = resolveLightLaneIncludePatterns(kind, targetArg, cwd);
|
|
return lightLanePatterns ?? [toScopedIncludePattern(targetArg, cwd)];
|
|
}),
|
|
);
|
|
const scopedTargetArgs = useCliTargetArgs ? uniqueOrdered(grouped) : [];
|
|
const forwardedPlanArgs = [...nonTargetArgs, ...scopedTargetArgs];
|
|
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 =
|
|
boundedExtensionRoots.length > 0 && boundedRootsCoverGroupedTargets
|
|
? createBoundedExtensionPlans(
|
|
{
|
|
config,
|
|
forwardedArgs: forwardedPlanArgs,
|
|
includePatterns,
|
|
watchMode,
|
|
},
|
|
options.env,
|
|
)
|
|
: null;
|
|
if (boundedExtensionPlans) {
|
|
plans.push(...boundedExtensionPlans);
|
|
continue;
|
|
}
|
|
plans.push({
|
|
config,
|
|
forwardedArgs: forwardedPlanArgs,
|
|
includePatterns,
|
|
watchMode,
|
|
});
|
|
}
|
|
return plans;
|
|
}
|
|
|
|
export function buildFullSuiteVitestRunPlans(args: string[], cwd = process.cwd()) {
|
|
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[][] = [];
|
|
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 === 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 = splitTargetChunks(
|
|
listGatewayServerTestTargets(cwd),
|
|
GATEWAY_SERVER_TEST_PROCESS_COUNT,
|
|
);
|
|
} else {
|
|
const roots = EXTENSION_TEST_PROCESS_ROOTS.get(config);
|
|
if (roots) {
|
|
chunks = createExtensionTestProcessTargetChunks(config, roots, forwardedArgs);
|
|
}
|
|
}
|
|
if (chunks.length > 0) {
|
|
return chunks.map((targets) => ({
|
|
config,
|
|
forwardedArgs: [...forwardedArgs, ...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);
|
|
}
|
|
|
|
function sanitizeVitestCachePathSegment(value: string) {
|
|
return (
|
|
value
|
|
.replace(/[^a-zA-Z0-9._-]+/gu, "-")
|
|
.replace(/^-+|-+$/gu, "")
|
|
.slice(0, 180) || "default"
|
|
);
|
|
}
|
|
|
|
export function applyParallelVitestCachePaths<T extends VitestSpecShape>(
|
|
specs: T[],
|
|
params: { cwd?: string; env?: NodeJS.ProcessEnv } = {},
|
|
): Array<Omit<T, "env"> & { env: NodeJS.ProcessEnv }> {
|
|
const baseEnv = params.env ?? process.env;
|
|
const cwd = params.cwd ?? process.cwd();
|
|
const configuredCacheRoot = baseEnv[FS_MODULE_CACHE_PATH_ENV_KEY]?.trim() || undefined;
|
|
// CI publishes a persistent cache root, not a writer-safe leaf. Every
|
|
// concurrent Vitest process still needs its own live directory below it.
|
|
const cacheRoot =
|
|
configuredCacheRoot ?? path.join(cwd, "node_modules", ".experimental-vitest-cache");
|
|
return specs.map((spec, index) => {
|
|
const specCachePath = spec.env?.[FS_MODULE_CACHE_PATH_ENV_KEY]?.trim();
|
|
if (specCachePath && specCachePath !== configuredCacheRoot) {
|
|
return spec;
|
|
}
|
|
const cacheSegment = sanitizeVitestCachePathSegment(`${index}-${spec.config}`);
|
|
return {
|
|
...spec,
|
|
env: {
|
|
...spec.env,
|
|
[FS_MODULE_CACHE_PATH_ENV_KEY]: path.join(cacheRoot, cacheSegment),
|
|
},
|
|
};
|
|
});
|
|
}
|
|
|
|
export function applyDefaultMultiSpecVitestCachePaths<T extends WatchableVitestSpecShape>(
|
|
specs: T[],
|
|
params: { cwd?: string; env?: NodeJS.ProcessEnv } = {},
|
|
): Array<Omit<T, "env"> & { env: NodeJS.ProcessEnv }> {
|
|
if (specs.length <= 1 || specs.some((spec) => spec.watchMode)) {
|
|
return specs;
|
|
}
|
|
// Same-config process lifetimes run one after another and must keep the
|
|
// restored CI seed. Isolating them would make every Telegram file pay a
|
|
// silent cold import.
|
|
if (specs.every((spec) => spec.config === specs[0]?.config)) {
|
|
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 shouldRetryVitestNoOutputTimeout(env = process.env) {
|
|
const value = env[VITEST_NO_OUTPUT_RETRY_ENV_KEY]?.trim().toLowerCase();
|
|
if (value === undefined && isCiLikeEnv(env)) {
|
|
return false;
|
|
}
|
|
return !["0", "false", "no", "off"].includes(value ?? "");
|
|
}
|
|
|
|
// Shards may pin a short no-output window so the known warm-cache stall dies
|
|
// fast (see AGENTS_CORE_RUNTIME_ENV in ci-node-test-plan.mts). A cold Vitest
|
|
// module cache makes those same imports legitimately silent for minutes, so
|
|
// the retry attempt must get the full watchdog window or it re-dies at the
|
|
// short limit and the job fails without ever running a test.
|
|
const RETRY_NO_OUTPUT_TIMEOUT_FLOOR_MS = 300_000;
|
|
|
|
export function withRetryNoOutputTimeout<T extends { env?: NodeJS.ProcessEnv }>(spec: T): T {
|
|
const current = Number(spec.env?.[VITEST_NO_OUTPUT_TIMEOUT_ENV_KEY]);
|
|
if (!Number.isFinite(current) || current <= 0 || current >= RETRY_NO_OUTPUT_TIMEOUT_FLOOR_MS) {
|
|
return spec;
|
|
}
|
|
return {
|
|
...spec,
|
|
env: {
|
|
...spec.env,
|
|
[VITEST_NO_OUTPUT_TIMEOUT_ENV_KEY]: String(RETRY_NO_OUTPUT_TIMEOUT_FLOOR_MS),
|
|
},
|
|
};
|
|
}
|
|
|
|
export function createVitestRunSpecs(
|
|
args: string[],
|
|
params: { baseEnv?: NodeJS.ProcessEnv; cwd?: string; tempDir?: string } = {},
|
|
) {
|
|
const cwd = params.cwd ?? process.cwd();
|
|
const baseEnv = params.baseEnv ?? process.env;
|
|
const plans = filterPlansForContractIncludeFile(
|
|
buildVitestRunPlans(args, cwd, listChangedPathsFromGit, { env: baseEnv }),
|
|
baseEnv,
|
|
);
|
|
return plans.map((plan, index) => {
|
|
const includeFilePath = plan.includePatterns
|
|
? path.join(
|
|
params.tempDir ?? os.tmpdir(),
|
|
`openclaw-vitest-include-${randomUUID()}-${index}.json`,
|
|
)
|
|
: null;
|
|
return {
|
|
config: plan.config,
|
|
env: includeFilePath
|
|
? {
|
|
...baseEnv,
|
|
[INCLUDE_FILE_ENV_KEY]: includeFilePath,
|
|
}
|
|
: 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) {
|
|
const includePatterns = loadIncludePatternsForSpecFilter(env);
|
|
if (!includePatterns) {
|
|
return plans;
|
|
}
|
|
return plans.filter((plan) => {
|
|
const configPatterns = CHANNEL_CONTRACT_CONFIG_PATTERNS.get(plan.config);
|
|
if (!configPatterns) {
|
|
return true;
|
|
}
|
|
return includePatterns.some((candidate) =>
|
|
includePatternMatchesConfig(candidate, configPatterns),
|
|
);
|
|
});
|
|
}
|
|
|
|
export function shouldAcquireLocalHeavyCheckLock(
|
|
runSpecs: Array<Pick<VitestRunSpec, "config" | "includePatterns" | "watchMode">>,
|
|
env = process.env,
|
|
) {
|
|
if (env.OPENCLAW_TEST_HEAVY_CHECK_LOCK_HELD === "1") {
|
|
return false;
|
|
}
|
|
|
|
if (env.OPENCLAW_TEST_PROJECTS_FORCE_LOCK === "1") {
|
|
return true;
|
|
}
|
|
|
|
const runSpec = runSpecs.length === 1 ? runSpecs[0] : undefined;
|
|
if (!runSpec) {
|
|
return true;
|
|
}
|
|
return !(
|
|
(runSpec.config === TOOLING_VITEST_CONFIG ||
|
|
runSpec.config === TOOLING_ISOLATED_VITEST_CONFIG) &&
|
|
!runSpec.watchMode &&
|
|
Array.isArray(runSpec.includePatterns) &&
|
|
runSpec.includePatterns.length > 0
|
|
);
|
|
}
|
|
|
|
function expandVitestIncludePatterns(includePatterns: string[], cwd: string) {
|
|
const candidateFiles = includePatterns.some(isGlobTarget)
|
|
? listExplicitTestTargetFilesForCwd(cwd)
|
|
: [];
|
|
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[],
|
|
options: { limit?: number } = {},
|
|
) {
|
|
if (failures.length === 0) {
|
|
return [];
|
|
}
|
|
|
|
const limit = options.limit ?? FAILED_SHARD_DIGEST_LIMIT;
|
|
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, 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;
|
|
}
|
|
|
|
export function buildVitestArgs(args: string[], cwd = process.cwd()) {
|
|
const [plan] = buildVitestRunPlans(args, cwd);
|
|
if (!plan) {
|
|
return createVitestArgs({
|
|
config: DEFAULT_VITEST_CONFIG,
|
|
forwardedArgs: [],
|
|
watchMode: false,
|
|
});
|
|
}
|
|
return createVitestArgs(plan);
|
|
}
|