Files
openclaw/scripts/test-projects.test-support.mjs
zhang-guiping 653292901a fix(tui): surface terminal lifecycle errors
Surface terminal TUI lifecycle errors after the chat stream ends, deduplicate delayed chat errors, and allow explicit runnable Vitest config targets to run through the target planner.

Fixes #85782.

Verification:
- pnpm exec oxfmt --check src/tui/tui-event-handlers.ts src/tui/tui-event-handlers.test.ts test/scripts/test-projects.test.ts scripts/test-projects.test-support.mjs src/agents/model-catalog-visibility.test.ts
- node scripts/run-vitest.mjs src/tui/tui-event-handlers.test.ts
- node scripts/run-vitest.mjs src/tui/tui-event-handlers.test.ts src/tui/tui-command-handlers.test.ts test/scripts/test-projects.test.ts src/agents/model-catalog-visibility.test.ts
- git diff --check
- autoreview --mode local: no accepted/actionable findings
- autoreview --mode branch --base origin/main: no accepted/actionable findings
- Required CI check dependency-guard passed

Co-authored-by: 张贵萍0668001030 <zhang.guiping@xydigit.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-05-30 21:59:27 +01:00

2575 lines
89 KiB
JavaScript

import { spawnSync } from "node:child_process";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { isChannelSurfaceTestFile } from "../test/vitest/vitest.channel-paths.mjs";
import {
isCommandsLightTarget,
resolveCommandsLightIncludePattern,
} from "../test/vitest/vitest.commands-light-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 } 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 } 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,
resolvePluginSdkLightIncludePattern,
} from "../test/vitest/vitest.plugin-sdk-paths.mjs";
import { fullSuiteVitestShards } from "../test/vitest/vitest.test-shards.mjs";
import { isUnitUiTestTarget } from "../test/vitest/vitest.ui-paths.mjs";
import {
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.mjs";
import { isCiLikeEnv, resolveLocalFullSuiteProfile } from "./lib/vitest-local-scheduling.mjs";
import {
DEFAULT_VITEST_NO_OUTPUT_HEARTBEAT_MS,
resolveVitestCliEntry,
resolveVitestNodeArgs,
} from "./run-vitest.mjs";
const DEFAULT_VITEST_CONFIG = "test/vitest/vitest.unit.config.ts";
const AGENTS_CORE_VITEST_CONFIG = "test/vitest/vitest.agents-core.config.ts";
const AGENTS_EMBEDDED_AGENT_VITEST_CONFIG = "test/vitest/vitest.agents-embedded-agent.config.ts";
const AGENTS_SUPPORT_VITEST_CONFIG = "test/vitest/vitest.agents-support.config.ts";
const AGENTS_TOOLS_VITEST_CONFIG = "test/vitest/vitest.agents-tools.config.ts";
const AGENTS_VITEST_CONFIG = "test/vitest/vitest.agents.config.ts";
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_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_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_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_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 UNIT_UI_VITEST_CONFIG = "test/vitest/vitest.unit-ui.config.ts";
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_SUPPORT_VITEST_CONFIG, 168],
[AGENTS_TOOLS_VITEST_CONFIG, 167],
[EXTENSION_CODEX_VITEST_CONFIG, 168],
[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],
[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_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],
[UNIT_UI_VITEST_CONFIG, 40],
[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, shardTimings) {
return shardTimings.get(config) ?? (FULL_SUITE_CONFIG_WEIGHT.get(config) ?? 0) * 1000;
}
function interleaveSlowAndFastSpecs(sortedSpecs) {
const ordered = [];
let slowIndex = 0;
let fastIndex = sortedSpecs.length - 1;
while (slowIndex <= fastIndex) {
ordered.push(sortedSpecs[slowIndex]);
slowIndex += 1;
if (slowIndex <= fastIndex) {
ordered.push(sortedSpecs[fastIndex]);
fastIndex -= 1;
}
}
return ordered;
}
function uniqueOrdered(values) {
return [...new Set(values)];
}
export function orderFullSuiteSpecsForParallelRun(specs, shardTimings = new Map()) {
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 TOOLING_ISOLATED_VITEST_CONFIG = "test/vitest/vitest.tooling-isolated.config.ts";
const TOOLING_VITEST_CONFIG = "test/vitest/vitest.tooling.config.ts";
const TOOLING_ISOLATED_TEST_TARGET = "test/scripts/openclaw-e2e-instance.test.ts";
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 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 = {
acp: ACP_VITEST_CONFIG,
agentCore: AGENTS_CORE_VITEST_CONFIG,
agentEmbedded: AGENTS_EMBEDDED_AGENT_VITEST_CONFIG,
agentSupport: AGENTS_SUPPORT_VITEST_CONFIG,
agentTools: AGENTS_TOOLS_VITEST_CONFIG,
agent: AGENTS_VITEST_CONFIG,
agentsCore: AGENTS_CORE_VITEST_CONFIG,
agentsSupport: AGENTS_SUPPORT_VITEST_CONFIG,
agentsTools: AGENTS_TOOLS_VITEST_CONFIG,
autoReplyCore: AUTO_REPLY_CORE_VITEST_CONFIG,
autoReplyReply: AUTO_REPLY_REPLY_VITEST_CONFIG,
autoReplyTopLevel: AUTO_REPLY_TOP_LEVEL_VITEST_CONFIG,
autoReply: AUTO_REPLY_VITEST_CONFIG,
boundary: BOUNDARY_VITEST_CONFIG,
bundled: BUNDLED_VITEST_CONFIG,
channel: CHANNEL_VITEST_CONFIG,
cli: CLI_VITEST_CONFIG,
command: COMMANDS_VITEST_CONFIG,
commandLight: COMMANDS_LIGHT_VITEST_CONFIG,
contractsChannelConfig: CONTRACTS_CHANNEL_CONFIG_VITEST_CONFIG,
contractsChannelRegistry: CONTRACTS_CHANNEL_REGISTRY_VITEST_CONFIG,
contractsChannelSession: CONTRACTS_CHANNEL_SESSION_VITEST_CONFIG,
contractsChannelSurface: CONTRACTS_CHANNEL_SURFACE_VITEST_CONFIG,
contractsPlugin: CONTRACTS_PLUGIN_VITEST_CONFIG,
cron: CRON_VITEST_CONFIG,
daemon: DAEMON_VITEST_CONFIG,
e2e: E2E_VITEST_CONFIG,
extension: EXTENSIONS_VITEST_CONFIG,
extensionFull: FULL_EXTENSIONS_VITEST_CONFIG,
extensionActiveMemory: EXTENSION_ACTIVE_MEMORY_VITEST_CONFIG,
extensionAcpx: EXTENSION_ACPX_VITEST_CONFIG,
extensionBrowser: EXTENSION_BROWSER_VITEST_CONFIG,
extensionChannel: EXTENSION_CHANNELS_VITEST_CONFIG,
extensionCodex: EXTENSION_CODEX_VITEST_CONFIG,
extensionDiffs: EXTENSION_DIFFS_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,
extensionMatrix: EXTENSION_MATRIX_VITEST_CONFIG,
extensionMattermost: EXTENSION_MATTERMOST_VITEST_CONFIG,
extensionMedia: EXTENSION_MEDIA_VITEST_CONFIG,
extensionMemory: EXTENSION_MEMORY_VITEST_CONFIG,
extensionMessaging: EXTENSION_MESSAGING_VITEST_CONFIG,
extensionMisc: EXTENSION_MISC_VITEST_CONFIG,
extensionMsTeams: EXTENSION_MSTEAMS_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,
extensionTelegram: EXTENSION_TELEGRAM_VITEST_CONFIG,
extensionVoiceCall: EXTENSION_VOICE_CALL_VITEST_CONFIG,
extensionWhatsApp: EXTENSION_WHATSAPP_VITEST_CONFIG,
extensionZalo: EXTENSION_ZALO_VITEST_CONFIG,
gatewayClient: GATEWAY_CLIENT_VITEST_CONFIG,
gatewayCore: GATEWAY_CORE_VITEST_CONFIG,
gatewayMethods: GATEWAY_METHODS_VITEST_CONFIG,
gatewayServer: GATEWAY_SERVER_VITEST_CONFIG,
gateway: GATEWAY_VITEST_CONFIG,
hooks: HOOKS_VITEST_CONFIG,
infra: INFRA_VITEST_CONFIG,
logging: LOGGING_VITEST_CONFIG,
media: MEDIA_VITEST_CONFIG,
mediaUnderstanding: MEDIA_UNDERSTANDING_VITEST_CONFIG,
plugin: PLUGINS_VITEST_CONFIG,
pluginSdk: PLUGIN_SDK_VITEST_CONFIG,
pluginSdkLight: PLUGIN_SDK_LIGHT_VITEST_CONFIG,
process: PROCESS_VITEST_CONFIG,
unitFast: UNIT_FAST_VITEST_CONFIG,
unitFastFakeTimers: UNIT_FAST_FAKE_TIMERS_VITEST_CONFIG,
unitSecurity: UNIT_SECURITY_VITEST_CONFIG,
unitSrc: UNIT_SRC_VITEST_CONFIG,
unitSupport: UNIT_SUPPORT_VITEST_CONFIG,
unitUi: UNIT_UI_VITEST_CONFIG,
runtimeConfig: RUNTIME_CONFIG_VITEST_CONFIG,
secrets: SECRETS_VITEST_CONFIG,
sharedCore: SHARED_CORE_VITEST_CONFIG,
tasks: TASKS_VITEST_CONFIG,
toolingIsolated: TOOLING_ISOLATED_VITEST_CONFIG,
tooling: TOOLING_VITEST_CONFIG,
tui: TUI_VITEST_CONFIG,
tuiPty: TUI_PTY_VITEST_CONFIG,
ui: UI_VITEST_CONFIG,
uiE2e: UI_E2E_VITEST_CONFIG,
utils: UTILS_VITEST_CONFIG,
wizard: WIZARD_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([
[
"src/plugins/contracts/tts-contract-suites.ts",
[
"src/plugins/contracts/core-extension-facade-boundary.test.ts",
"src/plugins/contracts/tts.contract.test.ts",
],
],
]);
const BROAD_ONLY_TEST_HELPERS = new Set(["test/helpers/poll.ts"]);
const TOOLING_SOURCE_TEST_TARGETS = new Map([
[".crabbox.yaml", ["test/scripts/package-acceptance-workflow.test.ts"]],
[
".github/workflows/ci-check-testbox.yml",
["test/scripts/ci-workflow-guards.test.ts", "test/scripts/package-acceptance-workflow.test.ts"],
],
[
".github/workflows/crabbox-hydrate.yml",
["test/scripts/ci-workflow-guards.test.ts", "test/scripts/package-acceptance-workflow.test.ts"],
],
["scripts/build-all.mjs", ["test/scripts/build-all.test.ts"]],
["scripts/crabbox-wrapper.mjs", ["test/scripts/crabbox-wrapper.test.ts"]],
["scripts/github/barnacle-auto-response.mjs", ["test/scripts/barnacle-auto-response.test.ts"]],
["scripts/changed-lanes.mjs", ["test/scripts/changed-lanes.test.ts"]],
["scripts/check.mjs", ["test/scripts/check.test.ts"]],
["scripts/check-changed.mjs", ["test/scripts/changed-lanes.test.ts"]],
[
"scripts/check-changelog-attributions.mjs",
["test/scripts/check-changelog-attributions.test.ts"],
],
[
"scripts/check-composite-action-input-interpolation.py",
["test/scripts/check-composite-action-input-interpolation.test.ts"],
],
["scripts/check-dependency-pins.mjs", ["test/scripts/check-dependency-pins.test.ts"]],
["scripts/check-deadcode-unused-files.mjs", ["test/scripts/check-deadcode-unused-files.test.ts"]],
["scripts/check-dynamic-import-warts.mjs", ["test/scripts/check-dynamic-import-warts.test.ts"]],
["scripts/check-no-conflict-markers.mjs", ["test/scripts/check-no-conflict-markers.test.ts"]],
[
"scripts/check-workflows.mjs",
[
"test/scripts/check-composite-action-input-interpolation.test.ts",
"test/scripts/check-no-conflict-markers.test.ts",
"test/scripts/ci-workflow-guards.test.ts",
],
],
["scripts/ci-changed-scope.mjs", ["src/scripts/ci-changed-scope.test.ts"]],
["scripts/ci-docker-pull-retry.sh", ["test/scripts/ci-docker-pull-retry.test.ts"]],
["scripts/control-ui-i18n.ts", ["test/scripts/control-ui-i18n.test.ts"]],
["scripts/dependency-changes-report.mjs", ["test/scripts/dependency-changes-report.test.ts"]],
[
"scripts/dependency-ownership-surface-report.mjs",
["test/scripts/dependency-ownership-surface-report.test.ts"],
],
[
"scripts/dependency-vulnerability-gate.mjs",
["test/scripts/dependency-vulnerability-gate.test.ts"],
],
[
"scripts/deadcode-unused-files.allowlist.mjs",
["test/scripts/check-deadcode-unused-files.test.ts"],
],
["scripts/docs-link-audit.mjs", ["src/scripts/docs-link-audit.test.ts"]],
["scripts/lib/arg-utils.mjs", ["test/scripts/arg-utils.test.ts"]],
[
"scripts/lib/bundled-plugin-build-entries.mjs",
["test/scripts/bundled-plugin-build-entries.test.ts"],
],
[
"scripts/lib/bundled-plugin-source-utils.mjs",
["test/scripts/bundled-plugin-source-utils.test.ts"],
],
["scripts/lib/dev-tooling-safety.ts", ["test/scripts/dev-tooling-safety.test.ts"]],
["scripts/lib/docker-e2e-container.sh", ["test/scripts/docker-build-helper.test.ts"]],
["scripts/lib/docker-e2e-package.sh", ["test/scripts/docker-build-helper.test.ts"]],
["scripts/lib/format-generated-module.mjs", ["test/scripts/format-generated-module.test.ts"]],
["scripts/lib/live-docker-stage.sh", ["test/scripts/live-docker-stage.test.ts"]],
["scripts/lib/local-heavy-check-runtime.mjs", ["test/scripts/local-heavy-check-runtime.test.ts"]],
["scripts/lib/managed-child-process.mjs", ["test/scripts/managed-child-process.test.ts"]],
["scripts/lib/npm-verify-exec.ts", ["test/scripts/npm-verify-exec.test.ts"]],
["scripts/lib/openclaw-test-state.mjs", ["test/scripts/openclaw-test-state.test.ts"]],
["scripts/lib/source-file-scan-cache.mjs", ["test/scripts/source-file-scan-cache.test.ts"]],
["scripts/lib/test-group-report.mjs", ["test/scripts/test-group-report.test.ts"]],
["scripts/lib/ts-guard-utils.mjs", ["test/scripts/ts-guard-utils.test.ts"]],
["scripts/lib/vitest-local-scheduling.mjs", ["test/scripts/vitest-local-scheduling.test.ts"]],
[
"scripts/mantis/build-telegram-evidence.mjs",
["test/scripts/mantis-build-telegram-evidence.test.ts"],
],
[
"scripts/mantis/build-telegram-desktop-proof-evidence.mjs",
["test/scripts/mantis-build-telegram-desktop-proof-evidence.test.ts"],
],
["scripts/mantis/publish-pr-evidence.mjs", ["test/scripts/mantis-publish-pr-evidence.test.ts"]],
["scripts/qa-lab-up.ts", ["test/scripts/qa-lab-up.test.ts"]],
[
"scripts/run-vitest.mjs",
[
"test/scripts/run-vitest.test.ts",
"test/scripts/test-projects.test.ts",
"test/scripts/vitest-local-scheduling.test.ts",
],
],
["scripts/run-oxlint.mjs", ["test/scripts/run-oxlint.test.ts"]],
["scripts/run-oxlint-shards.mjs", ["test/scripts/run-oxlint.test.ts"]],
["scripts/run-with-env.mjs", ["test/scripts/run-with-env.test.ts"]],
["scripts/run-node.mjs", ["src/infra/run-node.test.ts"]],
["scripts/ci-run-timings.mjs", ["test/scripts/ci-run-timings.test.ts"]],
["scripts/docker-e2e.mjs", ["test/scripts/docker-e2e-helper-cli.test.ts"]],
["scripts/docker-e2e-rerun.mjs", ["test/scripts/docker-e2e-helper-cli.test.ts"]],
["scripts/docker-e2e-timings.mjs", ["test/scripts/docker-e2e-helper-cli.test.ts"]],
["scripts/generate-npm-shrinkwrap.mjs", ["test/scripts/generate-npm-shrinkwrap.test.ts"]],
["scripts/kova-ci-summary.mjs", ["test/scripts/kova-ci-summary.test.ts"]],
["scripts/openclaw-npm-postpublish-verify.ts", ["test/openclaw-npm-postpublish-verify.test.ts"]],
["scripts/openclaw-npm-release-check.ts", ["test/openclaw-npm-release-check.test.ts"]],
["scripts/openclaw-prepack.ts", ["test/openclaw-prepack.test.ts"]],
["scripts/package-changelog.mjs", ["test/scripts/package-changelog.test.ts"]],
["scripts/package-mac-app.sh", ["test/scripts/package-mac-app.test.ts"]],
["scripts/package-mac-dist.sh", ["test/scripts/package-mac-dist.test.ts"]],
["scripts/package-openclaw-for-docker.mjs", ["test/scripts/package-openclaw-for-docker.test.ts"]],
["scripts/postinstall-bundled-plugins.mjs", ["test/scripts/postinstall-bundled-plugins.test.ts"]],
["scripts/prepare-git-hooks.mjs", ["test/scripts/prepare-git-hooks.test.ts"]],
[
"scripts/preinstall-package-manager-warning.mjs",
["test/scripts/preinstall-package-manager-warning.test.ts"],
],
["scripts/test-extension-batch.mjs", ["test/scripts/test-extension.test.ts"]],
["scripts/test-force.ts", ["test/scripts/test-force.test.ts"]],
["scripts/test-live.mjs", ["test/scripts/test-live.test.ts"]],
["scripts/tsdown-build.mjs", ["test/scripts/tsdown-build.test.ts"]],
["scripts/verify.mjs", ["test/scripts/verify.test.ts"]],
["scripts/zai-fallback-repro.ts", ["test/scripts/zai-fallback-repro.test.ts"]],
["scripts/lib/extension-test-plan.mjs", ["test/scripts/test-extension.test.ts"]],
["scripts/lib/vitest-batch-runner.mjs", ["test/scripts/test-extension.test.ts"]],
["scripts/lib/ci-node-test-plan.mjs", ["test/scripts/ci-node-test-plan.test.ts"]],
[
"scripts/lib/docker-e2e-scenarios.mjs",
["test/scripts/docker-e2e-plan.test.ts", "test/scripts/plugin-prerelease-test-plan.test.ts"],
],
[
"scripts/lib/plugin-prerelease-test-plan.mjs",
["test/scripts/plugin-prerelease-test-plan.test.ts"],
],
[
"scripts/e2e/kitchen-sink-plugin-docker.sh",
[
"test/scripts/docker-build-helper.test.ts",
"test/scripts/plugin-prerelease-test-plan.test.ts",
],
],
[
"scripts/e2e/kitchen-sink-rpc-docker.sh",
[
"test/scripts/docker-build-helper.test.ts",
"test/scripts/plugin-prerelease-test-plan.test.ts",
],
],
[
"scripts/e2e/kitchen-sink-rpc-walk.mjs",
[
"test/scripts/kitchen-sink-rpc-walk.test.ts",
"test/scripts/plugin-prerelease-test-plan.test.ts",
],
],
[
"scripts/e2e/onboard-docker.sh",
["test/scripts/docker-build-helper.test.ts", "test/scripts/openclaw-test-state.test.ts"],
],
["scripts/e2e/plugin-lifecycle-matrix-docker.sh", ["test/scripts/docker-build-helper.test.ts"]],
[
"scripts/e2e/release-media-memory-docker.sh",
["test/scripts/docker-e2e-plan.test.ts", "test/scripts/release-media-memory-scenario.test.ts"],
],
["scripts/lib/vitest-shard-timings.mjs", ["test/scripts/vitest-shard-timings.test.ts"]],
[
"scripts/plugin-prerelease-liveish-matrix.mjs",
["test/scripts/plugin-prerelease-test-plan.test.ts"],
],
["scripts/test-projects.mjs", ["test/scripts/test-projects.test.ts"]],
["scripts/test-projects.test-support.d.mts", ["test/scripts/test-projects.test.ts"]],
["scripts/test-projects.test-support.mjs", ["test/scripts/test-projects.test.ts"]],
["scripts/tsdown-build.mjs", ["test/scripts/tsdown-build.test.ts"]],
["scripts/bundled-plugin-assets.mjs", ["test/scripts/bundled-plugin-assets.test.ts"]],
["scripts/bundle-a2ui.mjs", ["test/scripts/bundled-plugin-assets.test.ts"]],
["scripts/build-diffs-viewer-runtime.mjs", ["test/scripts/build-diffs-viewer-runtime.test.ts"]],
["extensions/canvas/scripts/bundle-a2ui.mjs", ["extensions/canvas/scripts/bundle-a2ui.test.ts"]],
["extensions/canvas/scripts/copy-a2ui.mjs", ["extensions/canvas/scripts/copy-a2ui.test.ts"]],
]);
const TOOLING_TEST_TARGETS = new Map([
["test/scripts/barnacle-auto-response.test.ts", ["test/scripts/barnacle-auto-response.test.ts"]],
["test/scripts/changed-lanes.test.ts", ["test/scripts/changed-lanes.test.ts"]],
[
"test/scripts/check-deadcode-unused-files.test.ts",
["test/scripts/check-deadcode-unused-files.test.ts"],
],
["test/scripts/ci-docker-pull-retry.test.ts", ["test/scripts/ci-docker-pull-retry.test.ts"]],
["test/scripts/control-ui-i18n.test.ts", ["test/scripts/control-ui-i18n.test.ts"]],
["test/scripts/docker-build-helper.test.ts", ["test/scripts/docker-build-helper.test.ts"]],
["test/scripts/docker-e2e-helper-cli.test.ts", ["test/scripts/docker-e2e-helper-cli.test.ts"]],
["test/scripts/kova-ci-summary.test.ts", ["test/scripts/kova-ci-summary.test.ts"]],
["test/scripts/live-docker-stage.test.ts", ["test/scripts/live-docker-stage.test.ts"]],
["test/scripts/openclaw-test-state.test.ts", ["test/scripts/openclaw-test-state.test.ts"]],
["test/scripts/qa-lab-up.test.ts", ["test/scripts/qa-lab-up.test.ts"]],
[
"test/scripts/mantis-publish-pr-evidence.test.ts",
["test/scripts/mantis-publish-pr-evidence.test.ts"],
],
[
"test/scripts/mantis-build-telegram-evidence.test.ts",
["test/scripts/mantis-build-telegram-evidence.test.ts"],
],
[
"test/scripts/mantis-build-telegram-desktop-proof-evidence.test.ts",
["test/scripts/mantis-build-telegram-desktop-proof-evidence.test.ts"],
],
[
"test/scripts/plugin-prerelease-test-plan.test.ts",
["test/scripts/plugin-prerelease-test-plan.test.ts"],
],
["test/scripts/test-projects.test.ts", ["test/scripts/test-projects.test.ts"]],
[
"test/scripts/vitest-local-scheduling.test.ts",
["test/scripts/vitest-local-scheduling.test.ts"],
],
["test/scripts/zai-fallback-repro.test.ts", ["test/scripts/zai-fallback-repro.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 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 SOURCE_TEST_TARGETS = new Map([
...PRECISE_SOURCE_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],
["ui/config/control-ui-chunking.ts", ["ui/src/ui/control-ui-chunking.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"]],
[
"test/helpers/channels/directory-ids.ts",
[
"extensions/discord/src/directory-contract.test.ts",
"extensions/slack/src/directory-contract.test.ts",
"extensions/telegram/src/directory-contract.test.ts",
],
],
["extensions/google-meet/index.ts", ["extensions/google-meet/index.test.ts"]],
["extensions/google-meet/src/cli.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/commitments/model-selection.runtime.ts",
["src/commitments/runtime.test.ts", "src/agents/model-selection.test.ts"],
],
["src/agents/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"],
],
[
"src/memory-host-sdk/host/embedding-defaults.ts",
["packages/memory-host-sdk/src/host/embeddings.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/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 IMPORT_SPECIFIER_PATTERN =
/\b(?:import|export)\s+(?:type\s+)?(?:[^'"]*?\s+from\s+)?["']([^"']+)["']|\bimport\s*\(\s*["']([^"']+)["']\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";
export const DEFAULT_TEST_PROJECTS_VITEST_NO_OUTPUT_TIMEOUT_MS = String(900_000);
export const DEFAULT_TEST_PROJECTS_VITEST_NO_OUTPUT_HEARTBEAT_MS = String(
DEFAULT_VITEST_NO_OUTPUT_HEARTBEAT_MS,
);
const EXPLICIT_SOURCE_FULL_IMPORT_GRAPH_THRESHOLD = 12;
const GATEWAY_SERVER_FULL_SUITE_TARGET_CHUNK_COUNT = 4;
const GATEWAY_SERVER_BACKED_HTTP_TEST_TARGETS = new Set([
"src/gateway/embeddings-http.test.ts",
"src/gateway/models-http.test.ts",
"src/gateway/openai-http.test.ts",
"src/gateway/openresponses-http.test.ts",
"src/gateway/probe.auth.integration.test.ts",
]);
const GATEWAY_SERVER_EXCLUDED_TEST_TARGETS = new Set([
"src/gateway/gateway.test.ts",
"src/gateway/server.startup-matrix-migration.integration.test.ts",
"src/gateway/sessions-history-http.test.ts",
]);
const VITEST_CONFIG_TARGET_KIND_BY_PATH = new Map(
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]),
]);
const CHANNEL_CONTRACT_CONFIG_PATTERNS = new Map([
[
CONTRACTS_CHANNEL_SURFACE_VITEST_CONFIG,
[
"src/channels/plugins/contracts/channel-catalog.contract.test.ts",
"src/channels/plugins/contracts/channel-import-guardrails.test.ts",
"src/channels/plugins/contracts/group-policy.fallback.contract.test.ts",
"src/channels/plugins/contracts/outbound-payload.contract.test.ts",
"src/channels/plugins/contracts/*-shard-a.contract.test.ts",
"src/channels/plugins/contracts/*-shard-e.contract.test.ts",
],
],
[
CONTRACTS_CHANNEL_CONFIG_VITEST_CONFIG,
[
"src/channels/plugins/contracts/plugins-core.authorize-config-write.policy.contract.test.ts",
"src/channels/plugins/contracts/plugins-core.authorize-config-write.targets.contract.test.ts",
"src/channels/plugins/contracts/plugins-core.catalog.entries.contract.test.ts",
"src/channels/plugins/contracts/*-shard-b.contract.test.ts",
"src/channels/plugins/contracts/*-shard-f.contract.test.ts",
],
],
[
CONTRACTS_CHANNEL_REGISTRY_VITEST_CONFIG,
[
"src/channels/plugins/contracts/plugins-core.catalog.paths.contract.test.ts",
"src/channels/plugins/contracts/plugins-core.loader.contract.test.ts",
"src/channels/plugins/contracts/plugins-core.registry.contract.test.ts",
"src/channels/plugins/contracts/*-shard-c.contract.test.ts",
"src/channels/plugins/contracts/*-shard-g.contract.test.ts",
],
],
[
CONTRACTS_CHANNEL_SESSION_VITEST_CONFIG,
[
"src/channels/plugins/contracts/plugins-core.resolve-config-writes.contract.test.ts",
"src/channels/plugins/contracts/registry.contract.test.ts",
"src/channels/plugins/contracts/session-binding.registry-backed.contract.test.ts",
"src/channels/plugins/contracts/*-shard-d.contract.test.ts",
"src/channels/plugins/contracts/*-shard-h.contract.test.ts",
],
],
]);
function normalizePathPattern(value) {
return value.replaceAll("\\", "/");
}
function listRepoFilesRecursive(root, cwd) {
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))];
});
}
function listGatewayFilesFromGit(cwd) {
const result = spawnSync("git", ["ls-files", "--", "src/gateway"], {
cwd,
encoding: "utf8",
stdio: ["ignore", "pipe", "ignore"],
});
if (result.status !== 0) {
return null;
}
return result.stdout
.split("\n")
.map((line) => normalizePathPattern(line.trim()))
.filter((line) => line.length > 0);
}
function isGatewayServerFullSuiteTarget(relative) {
if (
GATEWAY_SERVER_EXCLUDED_TEST_TARGETS.has(relative) ||
relative.startsWith("src/gateway/server-methods/")
) {
return false;
}
return (
GATEWAY_SERVER_BACKED_HTTP_TEST_TARGETS.has(relative) ||
(relative.startsWith("src/gateway/") &&
path.posix.basename(relative).includes("server") &&
relative.endsWith(".test.ts"))
);
}
function resolveGatewayServerFullSuiteTargets(cwd) {
const gatewayDir = path.join(cwd, "src/gateway");
if (!fs.existsSync(gatewayDir)) {
return [];
}
return (listGatewayFilesFromGit(cwd) ?? listRepoFilesRecursive(gatewayDir, cwd))
.filter(isGatewayServerFullSuiteTarget)
.toSorted((a, b) => a.localeCompare(b));
}
function splitTargetChunks(targets, chunkCount) {
if (targets.length === 0) {
return [];
}
const normalizedChunkCount = Math.min(chunkCount, targets.length);
const baseSize = Math.floor(targets.length / normalizedChunkCount);
const remainder = targets.length % normalizedChunkCount;
const chunks = [];
let offset = 0;
for (let index = 0; index < normalizedChunkCount; index += 1) {
const chunkSize = baseSize + (index < remainder ? 1 : 0);
chunks.push(targets.slice(offset, offset + chunkSize));
offset += chunkSize;
}
return chunks;
}
function isExistingPathTarget(arg, cwd) {
return fs.existsSync(path.resolve(cwd, arg));
}
function isExistingFileTarget(arg, cwd) {
try {
return fs.statSync(path.resolve(cwd, arg)).isFile();
} catch {
return false;
}
}
function isGlobTarget(arg) {
return /[*?[\]{}]/u.test(arg);
}
function isFileLikeTarget(arg) {
return /\.(?:test|spec)\.[cm]?[jt]sx?$/u.test(arg);
}
function isTestFileTarget(arg) {
return /\.(?:test|spec)\.[cm]?[jt]sx?$/u.test(arg);
}
function isTestSupportFileTarget(arg) {
if (/(?:^|\/)(?:test-helpers|test-support)(?:\/|$)/u.test(arg)) {
return true;
}
const basename = path.posix.basename(arg).replace(/\.[cm]?[jt]sx?$/u, "");
return /(?:^|[._-])test-(?:helpers|support)(?:[._-]|$)/u.test(basename);
}
function isLikelyFileTarget(arg) {
return /(?:^|\/)[^/]+\.[A-Za-z0-9]+$/u.test(arg);
}
function isPathLikeTargetArg(arg, cwd) {
if (!arg || arg === "--" || arg.startsWith("-")) {
return false;
}
const relative = toRepoRelativeTarget(arg, cwd);
return (
isGlobTarget(arg) ||
isFileLikeTarget(arg) ||
isVitestConfigPathLikeTarget(relative) ||
isExistingPathTarget(arg, cwd)
);
}
function toRepoRelativeTarget(arg, cwd) {
if (isGlobTarget(arg)) {
return normalizePathPattern(arg.replace(/^\.\//u, ""));
}
const absolute = path.resolve(cwd, arg);
return normalizePathPattern(path.relative(cwd, absolute));
}
function toScopedIncludePattern(arg, cwd) {
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 = null;
let cachedExplicitTestTargetFilesCwd = null;
function listExplicitTestTargetFilesFromGit(cwd) {
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) {
if (cachedExplicitTestTargetFiles && cachedExplicitTestTargetFilesCwd === cwd) {
return cachedExplicitTestTargetFiles;
}
cachedExplicitTestTargetFiles =
listExplicitTestTargetFilesFromGit(cwd) ??
EXPLICIT_TEST_TARGET_ROOTS.flatMap((root) => listImportGraphFiles(cwd, root));
cachedExplicitTestTargetFilesCwd = cwd;
return cachedExplicitTestTargetFiles;
}
function includePatternMatchesAnyFile(pattern, files) {
return files.some((file) => file === pattern || path.matchesGlob(file, pattern));
}
function resolveExplicitSourceTestTargets(targetArg, cwd, options = {}) {
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, cwd) {
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 targets = resolveExplicitSourceTestTargets(targetArg, cwd, {
forceFullImportGraph,
});
return targets && targets.length > 0 ? targets : [targetArg];
});
}
export function findUnmatchedExplicitTestTargets(args, cwd = process.cwd()) {
const { targetArgs } = parseTestProjectsArgs(args, cwd);
if (targetArgs.length === 0) {
return [];
}
let candidateFiles = null;
const getCandidateFiles = () => {
candidateFiles ??= listExplicitTestTargetFilesForCwd(cwd);
return candidateFiles;
};
const unmatched = [];
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)) {
unmatched.push({
target: targetArg,
reason: "path-does-not-exist",
});
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) {
return name === ".git" || name === "dist" || name === "node_modules" || name === "vendor";
}
function listImportGraphFiles(cwd, directory, files = []) {
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);
}
continue;
}
if (entry.isFile() && IMPORTABLE_FILE_EXTENSIONS.some((ext) => relative.endsWith(ext))) {
files.push(relative);
}
}
return files;
}
function resolveImportSpecifier(importer, specifier, fileSet) {
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(
...IMPORTABLE_FILE_EXTENSIONS.map((candidateExt) => `${withoutExt}${candidateExt}`),
);
}
} else {
candidates.push(
...IMPORTABLE_FILE_EXTENSIONS.map((candidateExt) => `${base}${candidateExt}`),
...IMPORTABLE_FILE_EXTENSIONS.map((candidateExt) => `${base}/index${candidateExt}`),
);
}
return candidates.find((candidate) => fileSet.has(candidate)) ?? null;
}
let cachedImportGraph = null;
let cachedImportGraphCwd = null;
let cachedImportGraphFiles = null;
let cachedImportGraphFilesCwd = null;
const cachedImportGraphGrepMatches = new Map();
const cachedDirectImporters = new Map();
function isImportableGraphFile(relative) {
return IMPORTABLE_FILE_EXTENSIONS.some((ext) => relative.endsWith(ext));
}
function listImportGraphFilesFromGit(cwd) {
const result = spawnSync("git", ["ls-files", "--", ...SOURCE_ROOTS_FOR_IMPORT_GRAPH], {
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 && isImportableGraphFile(line));
}
function listImportGraphFilesForCwd(cwd) {
if (cachedImportGraphFiles && cachedImportGraphFilesCwd === cwd) {
return cachedImportGraphFiles;
}
cachedImportGraphFiles =
listImportGraphFilesFromGit(cwd) ??
SOURCE_ROOTS_FOR_IMPORT_GRAPH.flatMap((root) => listImportGraphFiles(cwd, root));
cachedImportGraphFilesCwd = cwd;
return cachedImportGraphFiles;
}
function stripImportableGraphExtension(relative) {
for (const ext of IMPORTABLE_FILE_EXTENSIONS) {
if (relative.endsWith(ext)) {
return relative.slice(0, -ext.length);
}
}
return relative;
}
function resolveImportGraphSearchTerms(relative) {
const withoutExtension = stripImportableGraphExtension(relative);
const basename = path.posix.basename(stripImportableGraphExtension(relative));
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, term) {
const cacheKey = `${cwd}\0${term}`;
if (cachedImportGraphGrepMatches.has(cacheKey)) {
return cachedImportGraphGrepMatches.get(cacheKey);
}
const result = spawnSync(
"git",
["grep", "-l", "--fixed-strings", term, "--", ...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 matches = result.stdout
.split("\n")
.map((line) => normalizePathPattern(line.trim()))
.filter((line) => line.length > 0 && isImportableGraphFile(line));
cachedImportGraphGrepMatches.set(cacheKey, matches);
return matches;
}
function findDirectImportersWithGitGrep(cwd, importedFile, fileSet) {
const cacheKey = `${cwd}\0${importedFile}`;
if (cachedDirectImporters.has(cacheKey)) {
return cachedDirectImporters.get(cacheKey);
}
const terms = resolveImportGraphSearchTerms(importedFile);
if (terms.length === 0) {
cachedDirectImporters.set(cacheKey, null);
return null;
}
let skippedBroadTerm = false;
const importers = [];
for (const term of terms) {
const candidates = listImportGraphGrepMatches(cwd, term);
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);
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, cwd) {
const normalized = normalizePathPattern(changedPath);
const files = listImportGraphFilesForCwd(cwd);
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 (let index = 0; index < queue.length; index += 1) {
const current = queue[index];
const importers = findDirectImportersWithGitGrep(cwd, current, fileSet);
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;
}
queue.push(importer);
}
}
return [...new Set(targets)].toSorted((left, right) => left.localeCompare(right));
}
function getImportGraph(cwd) {
if (cachedImportGraph && cachedImportGraphCwd === cwd) {
return cachedImportGraph;
}
const files = listImportGraphFilesForCwd(cwd);
const fileSet = new Set(files);
const reverseImports = new Map();
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);
}
}
cachedImportGraph = { reverseImports, testFiles };
cachedImportGraphCwd = cwd;
return cachedImportGraph;
}
function resolveAffectedTestsFromImportGraph(changedPath, cwd, options = {}) {
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 (let index = 0; index < queue.length; index += 1) {
const current = queue[index];
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) {
return VITEST_CONFIG_TARGET_KIND_BY_PATH.get(relative) ?? null;
}
function isVitestConfigPathLikeTarget(relative) {
return (
relative === "vitest.config.ts" || /^test\/vitest\/vitest\..+\.config\.ts$/u.test(relative)
);
}
function isVitestConfigFileTarget(relative) {
return RUNNABLE_VITEST_CONFIG_TARGETS.has(relative);
}
function isVitestConfigTargetForKind(kind, targetArg, cwd) {
return resolveVitestConfigTargetKind(toRepoRelativeTarget(targetArg, cwd)) === kind;
}
function isControlUiE2eTarget(relative) {
return (
relative === "ui/src/test-helpers/control-ui-e2e.ts" ||
relative === "ui/src/ui/e2e" ||
relative.startsWith("ui/src/ui/e2e/") ||
(relative.startsWith("ui/src/") && relative.endsWith(".e2e.test.ts"))
);
}
function resolveChannelContractTargetKind(relative) {
if (!relative.startsWith("src/channels/plugins/contracts/")) {
return null;
}
const name = path.posix.basename(relative);
if (/-shard-[ae]\.contract\.test\.ts$/u.test(name)) {
return "contractsChannelSurface";
}
if (/-shard-[bf]\.contract\.test\.ts$/u.test(name)) {
return "contractsChannelConfig";
}
if (/-shard-[cg]\.contract\.test\.ts$/u.test(name)) {
return "contractsChannelRegistry";
}
if (/-shard-[dh]\.contract\.test\.ts$/u.test(name)) {
return "contractsChannelSession";
}
if (
[
"channel-catalog.contract.test.ts",
"channel-import-guardrails.test.ts",
"group-policy.fallback.contract.test.ts",
"outbound-payload.contract.test.ts",
].includes(name)
) {
return "contractsChannelSurface";
}
if (
[
"plugins-core.authorize-config-write.policy.contract.test.ts",
"plugins-core.authorize-config-write.targets.contract.test.ts",
"plugins-core.catalog.entries.contract.test.ts",
].includes(name)
) {
return "contractsChannelConfig";
}
if (
[
"plugins-core.catalog.paths.contract.test.ts",
"plugins-core.loader.contract.test.ts",
"plugins-core.registry.contract.test.ts",
].includes(name)
) {
return "contractsChannelRegistry";
}
return "contractsChannelSession";
}
function listChangedPathsFromGit(baseRef, cwd) {
return listChangedPathsFromGitSource({ base: baseRef, cwd });
}
function extractChangedBaseRef(args) {
for (let index = 0; index < args.length; index += 1) {
const arg = args[index];
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) {
const strippedArgs = [];
for (let index = 0; index < args.length; index += 1) {
const arg = args[index];
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) {
return changedPaths.some((changedPath) =>
PRECISE_SOURCE_TEST_TARGETS.has(changedPath)
? false
: BROAD_CHANGED_FALLBACK_PATTERNS.some((pattern) => pattern.test(changedPath)),
);
}
function resolveToolingChangedTestTargets(changedPaths, cwd = process.cwd()) {
const targets = [];
for (const changedPath of changedPaths) {
const testTargets = resolveToolingTestTargets(changedPath, cwd);
if (!testTargets) {
return null;
}
targets.push(...testTargets);
}
return [...new Set(targets)];
}
function resolveConventionalToolingTestTargets(changedPath, cwd = process.cwd()) {
const match = /^scripts\/(.+)\.(?:mjs|ts|js|sh|py)$/u.exec(changedPath);
if (!match) {
return null;
}
const stem = match[1];
const basename = path.posix.basename(stem);
const dashedStem = stem.replaceAll("/", "-");
const candidates = [
`test/scripts/${stem}.test.ts`,
`test/scripts/${dashedStem}.test.ts`,
`test/scripts/${basename}.test.ts`,
`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 resolveToolingTestTargets(changedPath, cwd = process.cwd()) {
const explicitTargets =
TOOLING_SOURCE_TEST_TARGETS.get(changedPath) ?? TOOLING_TEST_TARGETS.get(changedPath);
const conventionalTargets = resolveConventionalToolingTestTargets(changedPath, cwd);
if (explicitTargets && conventionalTargets) {
return uniqueOrdered([...explicitTargets, ...conventionalTargets]);
}
return explicitTargets ?? conventionalTargets;
}
function shouldUseBroadChangedTargets(env = process.env) {
const value = env[BROAD_CHANGED_ENV_KEY]?.trim().toLowerCase();
return ["1", "true", "yes", "on"].includes(value ?? "");
}
function isRoutableChangedTarget(changedPath) {
if (GENERATED_CHANGED_TEST_TARGET_PATTERNS.some((pattern) => pattern.test(changedPath))) {
return false;
}
if (changedPath.endsWith(".live.test.ts")) {
return false;
}
return /^(?:src|test|extensions|ui|packages)(?:\/|$)/u.test(changedPath);
}
function resolveSiblingTestTarget(changedPath, cwd) {
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 shouldRouteChangedTargetWithoutImportGraph(changedPath) {
return (
changedPath.endsWith(".live.test.ts") ||
(changedPath.startsWith("ui/src/") && !changedPath.startsWith("ui/src/ui/"))
);
}
function resolvePreciseChangedTestTargets(changedPath, options) {
const cwd = options.cwd ?? process.cwd();
const mappedTargets =
resolveToolingTestTargets(changedPath) ?? SOURCE_TEST_TARGETS.get(changedPath);
if (mappedTargets) {
return mappedTargets;
}
if (isRoutableChangedTarget(changedPath) && isTestFileTarget(changedPath)) {
return [changedPath];
}
const siblingTest = resolveSiblingTestTarget(changedPath, cwd);
if (siblingTest) {
return [siblingTest];
}
if (BROAD_ONLY_TEST_HELPERS.has(changedPath)) {
return null;
}
if (shouldRouteChangedTargetWithoutImportGraph(changedPath)) {
return changedPath.startsWith("ui/src/") ? [changedPath] : null;
}
if (options.skipImportGraph === true) {
return null;
}
if (/^(?:src|test\/helpers|extensions|packages|ui\/src|ui\/config)\//u.test(changedPath)) {
const affectedTests = resolveAffectedTestsFromImportGraph(changedPath, cwd, {
forceFull: options.forceFullImportGraph === true,
});
if (affectedTests.length > 0) {
return affectedTests;
}
}
return null;
}
export function resolveChangedTestTargetPlan(changedPaths, options = {}) {
if (changedPaths.length === 0) {
return { mode: "none", targets: [] };
}
const cwd = options.cwd ?? process.cwd();
const toolingTargets = resolveToolingChangedTestTargets(changedPaths, cwd);
if (toolingTargets) {
return { mode: "targets", targets: toolingTargets };
}
const changedLanes = detectChangedLanes(changedPaths);
const env = options.env ?? {};
const useBroadFallback = options.broad ?? shouldUseBroadChangedTargets(env);
const skipImportGraph = changedLanes.lanes.all && !useBroadFallback;
const targets = [];
for (const changedPath of changedPaths) {
const preciseTargets = resolvePreciseChangedTestTargets(changedPath, {
...options,
skipImportGraph,
});
if (preciseTargets) {
targets.push(...preciseTargets);
continue;
}
const needsBroadFallback = shouldKeepBroadChangedRun([changedPath]) || changedLanes.lanes.all;
if (needsBroadFallback) {
if (useBroadFallback) {
return { mode: "broad", targets: [] };
}
continue;
}
if (isRoutableChangedTarget(changedPath)) {
targets.push(changedPath);
}
}
if (useBroadFallback && changedLanes.extensionImpactFromCore) {
targets.push("extensions");
}
return { mode: "targets", targets: [...new Set(targets)] };
}
export function listFullExtensionVitestProjectConfigs() {
return (
fullSuiteVitestShards.find((shard) => shard.config === FULL_EXTENSIONS_VITEST_CONFIG)
?.projects ?? []
);
}
export function resolveChangedTargetArgs(
args,
cwd = process.cwd(),
listChangedPaths = listChangedPathsFromGit,
options = {},
) {
const baseRef = extractChangedBaseRef(args);
if (!baseRef) {
return null;
}
const changedPaths = listChangedPaths(baseRef, cwd);
const plan = resolveChangedTestTargetPlan(changedPaths, {
cwd,
...options,
});
if (plan.mode === "broad") {
return null;
}
return plan.targets;
}
function classifyTarget(arg, cwd) {
const relative = toRepoRelativeTarget(arg, cwd);
const configTargetKind = resolveVitestConfigTargetKind(relative);
if (configTargetKind) {
return configTargetKind;
}
if (isControlUiE2eTarget(relative)) {
return "uiE2e";
}
if (relative.startsWith("ui/src/")) {
if (isUnitUiTestTarget(relative)) {
return "unitUi";
}
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";
}
if (resolveUnitFastTimerTestIncludePattern(relative)) {
return "unitFastFakeTimers";
}
if (resolveUnitFastTestIncludePattern(relative)) {
return "unitFast";
}
if (relative === "extensions") {
return "extensionFull";
}
if (relative.startsWith("extensions/")) {
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_ISOLATED_TEST_TARGET) {
return "toolingIsolated";
}
if (
relative.startsWith("test/") ||
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 (relative.startsWith("src/channels/")) {
return "channel";
}
if (relative.startsWith("src/gateway/")) {
return "gateway";
}
if (relative.startsWith("src/hooks/")) {
return "hooks";
}
if (relative.startsWith("src/infra/")) {
return "infra";
}
if (relative.startsWith("src/config/")) {
return "runtimeConfig";
}
if (relative.startsWith("src/cron/")) {
return "cron";
}
if (relative.startsWith("src/daemon/")) {
return "daemon";
}
if (relative.startsWith("src/media-understanding/")) {
return "mediaUnderstanding";
}
if (relative.startsWith("src/media/")) {
return "media";
}
if (relative.startsWith("src/logging/")) {
return "logging";
}
if (relative.startsWith("src/plugin-sdk/")) {
return isPluginSdkLightTarget(relative) ? "pluginSdkLight" : "pluginSdk";
}
if (relative.startsWith("src/process/")) {
return "process";
}
if (relative.startsWith("src/secrets/")) {
return "secrets";
}
if (relative.startsWith("src/shared/")) {
return "sharedCore";
}
if (relative.startsWith("src/tasks/")) {
return "tasks";
}
if (relative.startsWith("src/tui/")) {
return "tui";
}
if (relative.startsWith("src/acp/")) {
return "acp";
}
if (relative.startsWith("src/cli/")) {
return "cli";
}
if (relative.startsWith("src/commands/")) {
return isCommandsLightTarget(relative) ? "commandLight" : "command";
}
if (relative.startsWith("src/auto-reply/")) {
return "autoReply";
}
if (relative.startsWith("src/agents/")) {
return "agent";
}
if (relative.startsWith("src/plugins/")) {
return "plugin";
}
if (relative.startsWith("src/utils/")) {
return "utils";
}
if (relative.startsWith("src/wizard/")) {
return "wizard";
}
return "default";
}
function resolveLightLaneIncludePatterns(kind, targetArg, cwd) {
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 === "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, targetArg, cwd) {
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);
return relative.startsWith("ui/src/") && !relative.startsWith("ui/src/ui/");
}
function createVitestArgs(params) {
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 parseTestProjectsArgs(args, 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,
cwd = process.cwd(),
listChangedPaths = listChangedPathsFromGit,
options = {},
) {
const { forwardedArgs, targetArgs, watchMode } = parseTestProjectsArgs(args, cwd);
const changedTargetArgs =
targetArgs.length === 0 ? resolveChangedTargetArgs(args, cwd, listChangedPaths, options) : null;
const requestedTargetArgs = changedTargetArgs ?? targetArgs;
const activeTargetArgs = expandExplicitSourceTestTargets(requestedTargetArgs, cwd);
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.map((config) => ({
config,
forwardedArgs: nonTargetArgs,
includePatterns: null,
watchMode,
}));
}
const groupedTargets = new Map();
for (const targetArg of activeTargetArgs) {
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_ISOLATED_TEST_TARGET,
]),
)
) {
const current = groupedTargets.get("toolingIsolated") ?? [];
if (!current.includes(TOOLING_ISOLATED_TEST_TARGET)) {
current.push(TOOLING_ISOLATED_TEST_TARGET);
groupedTargets.set("toolingIsolated", 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 = [
"unitFast",
"unitFastFakeTimers",
"default",
"boundary",
"toolingIsolated",
"tooling",
"contractsChannelSurface",
"contractsChannelConfig",
"contractsChannelRegistry",
"contractsChannelSession",
"contractsPlugin",
"bundled",
"gateway",
"gatewayCore",
"gatewayClient",
"gatewayMethods",
"gatewayServer",
"hooks",
"infra",
"runtimeConfig",
"cron",
"daemon",
"media",
"logging",
"pluginSdkLight",
"pluginSdk",
"process",
"secrets",
"sharedCore",
"tasks",
"tui",
"tuiPty",
"mediaUnderstanding",
"acp",
"cli",
"commandLight",
"command",
"autoReply",
"autoReplyCore",
"autoReplyReply",
"autoReplyTopLevel",
"agentCore",
"agentEmbedded",
"agentSupport",
"agentTools",
"agent",
"agentsCore",
"agentsSupport",
"agentsTools",
"plugin",
"ui",
"uiE2e",
"unitSrc",
"unitSecurity",
"unitSupport",
"unitUi",
"utils",
"wizard",
"e2e",
"extensionActiveMemory",
"extensionAcpx",
"extensionCodex",
"extensionDiffs",
"extensionBrowser",
"extensionDiscord",
"extensionFeishu",
"extensionImessage",
"extensionIrc",
"extensionLine",
"extensionMattermost",
"extensionChannel",
"extensionTelegram",
"extensionVoiceCall",
"extensionWhatsApp",
"extensionZalo",
"extensionMatrix",
"extensionMedia",
"extensionMemory",
"extensionMisc",
"extensionMsTeams",
"extensionMessaging",
"extensionProviderOpenAi",
"extensionProvider",
"extensionQa",
"extensionSignal",
"extensionSlack",
"extensionFull",
"channel",
"extension",
];
const plans = [];
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) {
plans.push({
config,
forwardedArgs: nonTargetArgs,
includePatterns: null,
watchMode,
});
}
continue;
}
const config = VITEST_CONFIG_BY_KIND[kind] ?? DEFAULT_VITEST_CONFIG;
const useCliTargetArgs =
kind === "e2e" ||
(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) : [];
plans.push({
config,
forwardedArgs: [...nonTargetArgs, ...scopedTargetArgs],
includePatterns,
watchMode,
});
}
return plans;
}
export function buildFullSuiteVitestRunPlans(args, cwd = process.cwd()) {
const { forwardedArgs, targetArgs, watchMode } = parseTestProjectsArgs(args, cwd);
if (watchMode) {
return [
{
config: "vitest.config.ts",
forwardedArgs,
includePatterns: null,
watchMode,
},
];
}
const parallelShardCount = Number.parseInt(process.env.OPENCLAW_TEST_PROJECTS_PARALLEL ?? "", 10);
const expandToProjectConfigs =
process.env.OPENCLAW_TEST_PROJECTS_LEAF_SHARDS === "1" ||
(Number.isFinite(parallelShardCount) && parallelShardCount > 1) ||
shouldUseLocalFullSuiteParallelByDefault(process.env);
return fullSuiteVitestShards.flatMap((shard) => {
if (
process.env.OPENCLAW_TEST_SKIP_FULL_EXTENSIONS_SHARD === "1" &&
shard.config === FULL_EXTENSIONS_VITEST_CONFIG
) {
return [];
}
const expandShard = expandToProjectConfigs;
const configs = expandShard ? shard.projects : [shard.config];
return configs.flatMap((config) => {
if (expandShard && targetArgs.length === 0 && config === GATEWAY_SERVER_VITEST_CONFIG) {
const chunks = splitTargetChunks(
resolveGatewayServerFullSuiteTargets(cwd),
GATEWAY_SERVER_FULL_SUITE_TARGET_CHUNK_COUNT,
);
if (chunks.length > 0) {
return chunks.map((targets) => ({
config,
forwardedArgs: [...forwardedArgs, ...targets],
includePatterns: null,
watchMode: false,
}));
}
}
return [
{
config,
forwardedArgs,
includePatterns: null,
watchMode: false,
},
];
});
});
}
export function shouldUseLocalFullSuiteParallelByDefault(env = process.env) {
if (hasConservativeVitestWorkerBudget(env)) {
return false;
}
return (
env.OPENCLAW_TEST_PROJECTS_SERIAL !== "1" && env.CI !== "true" && env.GITHUB_ACTIONS !== "true"
);
}
function parsePositiveInt(value) {
const parsed = Number.parseInt(value ?? "", 10);
return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
}
function hasConservativeVitestWorkerBudget(env) {
const workerBudget = parsePositiveInt(
env.OPENCLAW_VITEST_MAX_WORKERS ?? env.OPENCLAW_TEST_WORKERS,
);
return workerBudget !== null && workerBudget <= 1;
}
export function resolveParallelFullSuiteConcurrency(specCount, env, hostInfo) {
env ??= process.env;
const override = parsePositiveInt(env.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) {
return (
value
.replace(/[^a-zA-Z0-9._-]+/gu, "-")
.replace(/^-+|-+$/gu, "")
.slice(0, 180) || "default"
);
}
export function applyParallelVitestCachePaths(specs, params = {}) {
const baseEnv = params.env ?? process.env;
if (baseEnv[FS_MODULE_CACHE_PATH_ENV_KEY]?.trim()) {
return specs;
}
const cwd = params.cwd ?? process.cwd();
return specs.map((spec, index) => {
if (spec.env?.[FS_MODULE_CACHE_PATH_ENV_KEY]?.trim()) {
return spec;
}
const cacheSegment = sanitizeVitestCachePathSegment(`${index}-${spec.config}`);
return {
...spec,
env: {
...spec.env,
[FS_MODULE_CACHE_PATH_ENV_KEY]: path.join(
cwd,
"node_modules",
".experimental-vitest-cache",
cacheSegment,
),
},
};
});
}
export function applyDefaultMultiSpecVitestCachePaths(specs, params = {}) {
if (specs.length <= 1 || specs.some((spec) => spec.watchMode)) {
return specs;
}
return applyParallelVitestCachePaths(specs, params);
}
export function applyDefaultVitestNoOutputTimeout(specs, params = {}) {
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] = DEFAULT_TEST_PROJECTS_VITEST_NO_OUTPUT_TIMEOUT_MS;
}
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 ?? "");
}
export function createVitestRunSpecs(args, params = {}) {
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-${process.pid}-${Date.now()}-${index}.json`,
)
: null;
return {
config: plan.config,
env: includeFilePath
? {
...baseEnv,
[INCLUDE_FILE_ENV_KEY]: includeFilePath,
}
: baseEnv,
includeFilePath,
includePatterns: plan.includePatterns,
pnpmArgs: createVitestArgs(plan),
watchMode: plan.watchMode,
};
});
}
function loadIncludePatternsForSpecFilter(env) {
const filePath = env[INCLUDE_FILE_ENV_KEY]?.trim();
if (!filePath) {
return null;
}
const parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
if (!Array.isArray(parsed)) {
return [];
}
return parsed.filter((value) => typeof value === "string" && value.length > 0);
}
function includePatternMatchesConfig(candidate, configPatterns) {
return configPatterns.some(
(pattern) => path.matchesGlob(candidate, pattern) || path.matchesGlob(pattern, candidate),
);
}
function filterPlansForContractIncludeFile(plans, env) {
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, env = process.env) {
if (env.OPENCLAW_TEST_HEAVY_CHECK_LOCK_HELD === "1") {
return false;
}
if (env.OPENCLAW_TEST_PROJECTS_FORCE_LOCK === "1") {
return true;
}
return !(
runSpecs.length === 1 &&
(runSpecs[0]?.config === TOOLING_VITEST_CONFIG ||
runSpecs[0]?.config === TOOLING_ISOLATED_VITEST_CONFIG) &&
runSpecs[0]?.watchMode === false &&
Array.isArray(runSpecs[0]?.includePatterns) &&
runSpecs[0].includePatterns.length > 0
);
}
export function writeVitestIncludeFile(filePath, includePatterns) {
fs.writeFileSync(filePath, `${JSON.stringify(includePatterns, null, 2)}\n`);
}
function shellQuote(value) {
const text = `${value}`;
if (text === "") {
return "''";
}
if (/^[A-Za-z0-9_./:=@%+-]+$/u.test(text)) {
return text;
}
return `'${text.replaceAll("'", "'\\''")}'`;
}
function formatFailedShardRerunCommand(failure) {
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) {
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, options = {}) {
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 includes =
failure.includePatterns?.length > 0
? ` includes=${failure.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, cwd = process.cwd()) {
const [plan] = buildVitestRunPlans(args, cwd);
if (!plan) {
return createVitestArgs({
config: DEFAULT_VITEST_CONFIG,
forwardedArgs: [],
watchMode: false,
});
}
return createVitestArgs(plan);
}