mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-23 15:38:10 +00:00
* fix(scripts): render auth monitor unit before install Render the auth monitor service into temporary files instead of editing the tracked template. Quote the generated ExecStart safely, including spaces and literal dollars, then atomically install the rendered unit. * fix(scripts): avoid mutating tracked auth-monitor template during setup * fix(scripts): avoid mutating tracked auth-monitor template during setup * fix(scripts): avoid mutating tracked auth-monitor template during setup --------- Co-authored-by: JackWuGlobal <JackWuGlobal@users.noreply.github.com> Co-authored-by: openclaw-clownfish[bot] <280122609+openclaw-clownfish[bot]@users.noreply.github.com>
3352 lines
114 KiB
JavaScript
3352 lines
114 KiB
JavaScript
// Test-project planning helpers used by scripts/run-vitest.mjs,
|
|
// scripts/test-projects.mjs, 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 { isChannelSurfaceTestFile } from "../test/vitest/vitest.channel-paths.mjs";
|
|
import {
|
|
commandsLightTestFiles,
|
|
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,
|
|
pluginSdkLightTestFiles,
|
|
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 {
|
|
getUnitFastTestFiles,
|
|
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,
|
|
resolveDefaultVitestNoOutputTimeoutMs,
|
|
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_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_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_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],
|
|
[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)];
|
|
}
|
|
|
|
function isPathAtOrUnder(relative, root) {
|
|
return relative === root || relative.startsWith(`${root}/`);
|
|
}
|
|
|
|
/**
|
|
* Orders full-suite specs so expensive shards start first in parallel runs.
|
|
*/
|
|
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_DOCKER_VITEST_CONFIG = "test/vitest/vitest.tooling-docker.config.ts";
|
|
const TOOLING_ISOLATED_VITEST_CONFIG = "test/vitest/vitest.tooling-isolated.config.ts";
|
|
const TOOLING_VITEST_CONFIG = "test/vitest/vitest.tooling.config.ts";
|
|
const TOOLING_DOCKER_TEST_TARGET = "test/scripts/docker-build-helper.test.ts";
|
|
const 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,
|
|
toolingDocker: TOOLING_DOCKER_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 TOOLING_SOURCE_TEST_TARGETS = new Map([
|
|
[".crabbox.yaml", ["test/scripts/package-acceptance-workflow.test.ts"]],
|
|
[".github/workflows/ci.yml", ["test/scripts/ci-workflow-guards.test.ts"]],
|
|
[
|
|
".github/workflows/security-sensitive-guard.yml",
|
|
["test/scripts/security-sensitive-guard-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/ci-check-arm-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"],
|
|
],
|
|
[
|
|
".github/workflows/openclaw-release-checks.yml",
|
|
["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/e2e/agent-bundle-mcp-tools-docker.sh",
|
|
[
|
|
"test/scripts/docker-build-helper.test.ts",
|
|
"test/scripts/docker-e2e-plan.test.ts",
|
|
"test/scripts/plugin-prerelease-test-plan.test.ts",
|
|
"src/agents/agent-bundle-mcp-runtime.test.ts",
|
|
"src/agents/agent-bundle-mcp-tools.materialize.test.ts",
|
|
],
|
|
],
|
|
[
|
|
"scripts/e2e/agent-bundle-mcp-tools-docker-client.ts",
|
|
[
|
|
"src/agents/agent-bundle-mcp-runtime.test.ts",
|
|
"src/agents/agent-bundle-mcp-tools.materialize.test.ts",
|
|
],
|
|
],
|
|
[
|
|
"scripts/e2e/codex-media-path-docker.sh",
|
|
[
|
|
"test/scripts/docker-build-helper.test.ts",
|
|
"test/scripts/docker-e2e-plan.test.ts",
|
|
"test/scripts/codex-media-path-client.test.ts",
|
|
],
|
|
],
|
|
[
|
|
"scripts/e2e/codex-npm-plugin-live-docker.sh",
|
|
[
|
|
"test/scripts/docker-build-helper.test.ts",
|
|
"test/scripts/docker-e2e-plan.test.ts",
|
|
"test/scripts/package-acceptance-workflow.test.ts",
|
|
],
|
|
],
|
|
[
|
|
"scripts/e2e/codex-on-demand-docker.sh",
|
|
["test/scripts/docker-build-helper.test.ts", "test/scripts/docker-e2e-plan.test.ts"],
|
|
],
|
|
[
|
|
"scripts/e2e/crestodian-first-run-docker.sh",
|
|
[
|
|
"test/scripts/docker-build-helper.test.ts",
|
|
"test/scripts/docker-e2e-plan.test.ts",
|
|
"test/scripts/docker-e2e-crestodian.test.ts",
|
|
],
|
|
],
|
|
[
|
|
"scripts/e2e/crestodian-first-run-docker-client.ts",
|
|
[
|
|
"test/scripts/docker-e2e-crestodian.test.ts",
|
|
"src/cli/run-main.test.ts",
|
|
"src/cli/run-main.exit.test.ts",
|
|
"src/crestodian/crestodian.test.ts",
|
|
"src/crestodian/operations.test.ts",
|
|
"src/crestodian/overview.test.ts",
|
|
"src/crestodian/audit.test.ts",
|
|
],
|
|
],
|
|
[
|
|
"scripts/e2e/crestodian-first-run-spec.json",
|
|
[
|
|
"test/scripts/docker-e2e-crestodian.test.ts",
|
|
"src/crestodian/operations.test.ts",
|
|
"src/crestodian/audit.test.ts",
|
|
],
|
|
],
|
|
[
|
|
"scripts/e2e/crestodian-planner-docker.sh",
|
|
[
|
|
"test/scripts/docker-build-helper.test.ts",
|
|
"test/scripts/docker-e2e-plan.test.ts",
|
|
"test/scripts/docker-e2e-crestodian.test.ts",
|
|
],
|
|
],
|
|
[
|
|
"scripts/e2e/crestodian-planner-docker-client.mjs",
|
|
[
|
|
"test/scripts/docker-e2e-crestodian.test.ts",
|
|
"src/crestodian/assistant.test.ts",
|
|
"src/crestodian/crestodian.test.ts",
|
|
"src/crestodian/operations.test.ts",
|
|
"src/crestodian/audit.test.ts",
|
|
],
|
|
],
|
|
[
|
|
"scripts/e2e/crestodian-rescue-docker.sh",
|
|
[
|
|
"test/scripts/docker-build-helper.test.ts",
|
|
"test/scripts/docker-e2e-plan.test.ts",
|
|
"test/scripts/docker-e2e-crestodian.test.ts",
|
|
],
|
|
],
|
|
[
|
|
"scripts/e2e/crestodian-rescue-docker-client.ts",
|
|
[
|
|
"test/scripts/docker-e2e-crestodian.test.ts",
|
|
"src/crestodian/rescue-policy.test.ts",
|
|
"src/crestodian/rescue-message.test.ts",
|
|
"src/crestodian/operations.test.ts",
|
|
"src/crestodian/audit.test.ts",
|
|
],
|
|
],
|
|
[
|
|
"scripts/e2e/commitments-safety-docker-client.ts",
|
|
[
|
|
"test/scripts/docker-e2e-clients.test.ts",
|
|
"src/commitments/runtime.test.ts",
|
|
"src/commitments/store.test.ts",
|
|
],
|
|
],
|
|
[
|
|
"scripts/e2e/commitments-safety-docker.sh",
|
|
[
|
|
"test/scripts/docker-e2e-clients.test.ts",
|
|
"test/scripts/docker-e2e-plan.test.ts",
|
|
"src/commitments/runtime.test.ts",
|
|
"src/commitments/store.test.ts",
|
|
],
|
|
],
|
|
[
|
|
"scripts/e2e/session-runtime-context-docker-client.ts",
|
|
[
|
|
"test/scripts/docker-e2e-clients.test.ts",
|
|
"src/agents/embedded-agent-runner/run/runtime-context-prompt.test.ts",
|
|
"src/agents/embedded-agent-runner/transcript-rewrite.test.ts",
|
|
],
|
|
],
|
|
[
|
|
"scripts/e2e/session-runtime-context-docker.sh",
|
|
[
|
|
"test/scripts/docker-e2e-clients.test.ts",
|
|
"test/scripts/docker-e2e-plan.test.ts",
|
|
"src/agents/embedded-agent-runner/run/runtime-context-prompt.test.ts",
|
|
"src/agents/embedded-agent-runner/transcript-rewrite.test.ts",
|
|
],
|
|
],
|
|
[
|
|
"scripts/e2e/mcp-channels-docker.sh",
|
|
[
|
|
"test/scripts/docker-build-helper.test.ts",
|
|
"test/scripts/docker-e2e-observability.test.ts",
|
|
"test/scripts/docker-e2e-plan.test.ts",
|
|
"test/scripts/plugin-prerelease-test-plan.test.ts",
|
|
],
|
|
],
|
|
[
|
|
"scripts/e2e/mcp-channels-docker-client.ts",
|
|
["test/scripts/docker-e2e-plan.test.ts", "test/scripts/plugin-prerelease-test-plan.test.ts"],
|
|
],
|
|
["scripts/e2e/mcp-channels-seed.ts", ["test/scripts/docker-e2e-seeds.test.ts"]],
|
|
["scripts/e2e/docker-openai-seed.ts", ["test/scripts/docker-e2e-seeds.test.ts"]],
|
|
[
|
|
"scripts/e2e/mcp-code-mode-gateway-docker.sh",
|
|
[
|
|
"test/scripts/docker-build-helper.test.ts",
|
|
"test/scripts/docker-e2e-plan.test.ts",
|
|
"test/scripts/plugin-prerelease-test-plan.test.ts",
|
|
"test/scripts/mcp-code-mode-gateway-client.test.ts",
|
|
"test/scripts/session-log-mentions.test.ts",
|
|
],
|
|
],
|
|
[
|
|
"scripts/e2e/mcp-code-mode-gateway-live-docker.sh",
|
|
[
|
|
"test/scripts/docker-build-helper.test.ts",
|
|
"test/scripts/docker-e2e-plan.test.ts",
|
|
"test/scripts/plugin-prerelease-test-plan.test.ts",
|
|
"test/scripts/mcp-code-mode-gateway-client.test.ts",
|
|
"test/scripts/session-log-mentions.test.ts",
|
|
],
|
|
],
|
|
["scripts/e2e/mcp-code-mode-gateway-seed.ts", ["test/scripts/docker-e2e-seeds.test.ts"]],
|
|
[
|
|
"scripts/e2e/lib/mcp-code-mode-probe-server.ts",
|
|
["test/scripts/docker-e2e-seeds.test.ts", "test/scripts/mcp-code-mode-gateway-client.test.ts"],
|
|
],
|
|
[
|
|
"scripts/e2e/cron-mcp-cleanup-docker.sh",
|
|
[
|
|
"test/scripts/docker-build-helper.test.ts",
|
|
"test/scripts/docker-e2e-observability.test.ts",
|
|
"test/scripts/docker-e2e-plan.test.ts",
|
|
"test/scripts/plugin-prerelease-test-plan.test.ts",
|
|
"test/scripts/cron-mcp-cleanup-docker-client.test.ts",
|
|
"test/scripts/docker-e2e-seeds.test.ts",
|
|
],
|
|
],
|
|
[
|
|
"scripts/e2e/cron-mcp-cleanup-docker-client.ts",
|
|
[
|
|
"test/scripts/cron-mcp-cleanup-docker-client.test.ts",
|
|
"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/mcp-code-mode-gateway-e2e.ts",
|
|
[
|
|
"test/scripts/mcp-code-mode-gateway-client.test.ts",
|
|
"test/scripts/session-log-mentions.test.ts",
|
|
],
|
|
],
|
|
["scripts/dependency-changes-report.mjs", ["test/scripts/dependency-changes-report.test.ts"]],
|
|
[
|
|
"scripts/github/security-sensitive-guard.mjs",
|
|
["test/scripts/security-sensitive-guard-script.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/kova-report-gate.mjs", ["test/scripts/kova-report-gate.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/setup-auth-system.sh", ["test/scripts/test-projects.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/ios-run.sh", ["test/scripts/ios-run.test.ts"]],
|
|
["scripts/create-dmg.sh", ["test/scripts/create-dmg.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/e2e/qa-lab/runtime/package-openclaw-for-docker.e2e.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/verify-pr-hosted-gates.mjs", ["test/scripts/verify-pr-hosted-gates.test.ts"]],
|
|
["scripts/zai-fallback-repro.ts", ["test/scripts/zai-fallback-repro.test.ts"]],
|
|
["scripts/repro/code-mode-namespace-live.ts", ["test/scripts/code-mode-namespace-live.test.ts"]],
|
|
[
|
|
"scripts/repro/code-mode-namespace-live-docker.sh",
|
|
["test/scripts/code-mode-namespace-live.test.ts", "test/scripts/docker-build-helper.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/agents-delete-shared-workspace-docker.sh",
|
|
[
|
|
"test/scripts/docker-e2e-plan.test.ts",
|
|
"src/scripts/ci-changed-scope.test.ts",
|
|
"src/commands/agents.delete.test.ts",
|
|
],
|
|
],
|
|
[
|
|
"scripts/e2e/browser-cdp-snapshot-docker.sh",
|
|
[
|
|
"test/scripts/docker-build-helper.test.ts",
|
|
"test/scripts/browser-cdp-snapshot.test.ts",
|
|
"test/scripts/e2e-helper-env-limits.test.ts",
|
|
],
|
|
],
|
|
[
|
|
"scripts/e2e/channel-plugin-trust-docker.sh",
|
|
["test/scripts/docker-build-helper.test.ts", "test/scripts/test-projects.test.ts"],
|
|
],
|
|
[
|
|
"scripts/e2e/config-reload-source-docker.sh",
|
|
[
|
|
"test/scripts/docker-e2e-plan.test.ts",
|
|
"test/scripts/package-acceptance-workflow.test.ts",
|
|
"test/scripts/fixture-config.test.ts",
|
|
"test/scripts/e2e-mock-config-limits.test.ts",
|
|
"src/gateway/config-reload.test.ts",
|
|
],
|
|
],
|
|
[
|
|
"scripts/e2e/gateway-network-docker.sh",
|
|
[
|
|
"test/scripts/docker-build-helper.test.ts",
|
|
"test/scripts/docker-e2e-plan.test.ts",
|
|
"test/scripts/package-acceptance-workflow.test.ts",
|
|
"test/scripts/gateway-network-client.test.ts",
|
|
"src/scripts/ci-changed-scope.test.ts",
|
|
],
|
|
],
|
|
[
|
|
"scripts/e2e/npm-onboard-channel-agent-docker.sh",
|
|
[
|
|
"test/scripts/docker-build-helper.test.ts",
|
|
"test/scripts/docker-e2e-plan.test.ts",
|
|
"test/scripts/package-acceptance-workflow.test.ts",
|
|
"test/scripts/npm-onboard-channel-agent-assertions.test.ts",
|
|
"test/scripts/plugin-prerelease-test-plan.test.ts",
|
|
],
|
|
],
|
|
["scripts/e2e/npm-telegram-live-docker.sh", ["test/scripts/npm-telegram-live.test.ts"]],
|
|
[
|
|
"scripts/e2e/multi-node-update-docker.sh",
|
|
["test/scripts/docker-build-helper.test.ts", "test/scripts/docker-e2e-plan.test.ts"],
|
|
],
|
|
[
|
|
"scripts/e2e/doctor-install-switch-docker.sh",
|
|
[
|
|
"test/scripts/docker-build-helper.test.ts",
|
|
"test/scripts/docker-e2e-plan.test.ts",
|
|
"test/scripts/package-acceptance-workflow.test.ts",
|
|
],
|
|
],
|
|
[
|
|
"scripts/e2e/update-channel-switch-docker.sh",
|
|
[
|
|
"test/scripts/docker-build-helper.test.ts",
|
|
"test/scripts/docker-e2e-plan.test.ts",
|
|
"test/scripts/package-acceptance-workflow.test.ts",
|
|
],
|
|
],
|
|
[
|
|
"scripts/e2e/skill-install-docker.sh",
|
|
[
|
|
"test/scripts/docker-build-helper.test.ts",
|
|
"test/scripts/docker-e2e-plan.test.ts",
|
|
"test/scripts/package-acceptance-workflow.test.ts",
|
|
"test/scripts/e2e-shell-tempfiles.test.ts",
|
|
],
|
|
],
|
|
[
|
|
"scripts/e2e/upgrade-survivor-docker.sh",
|
|
[
|
|
"test/scripts/docker-build-helper.test.ts",
|
|
"test/scripts/docker-e2e-plan.test.ts",
|
|
"test/scripts/package-acceptance-workflow.test.ts",
|
|
"test/scripts/upgrade-survivor-probe-gateway.test.ts",
|
|
"test/scripts/upgrade-survivor-assertions.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/bundled-plugin-install-uninstall-docker.sh",
|
|
[
|
|
"test/scripts/docker-build-helper.test.ts",
|
|
"test/scripts/docker-e2e-plan.test.ts",
|
|
"test/scripts/plugin-prerelease-test-plan.test.ts",
|
|
"test/scripts/bundled-plugin-install-uninstall-probe.test.ts",
|
|
],
|
|
],
|
|
[
|
|
"scripts/e2e/lib/plugin-lifecycle-matrix/measure.mjs",
|
|
["test/scripts/plugin-lifecycle-measure.test.ts"],
|
|
],
|
|
[
|
|
"scripts/e2e/lib/bundled-plugin-install-uninstall/runtime-smoke.mjs",
|
|
["test/scripts/bundled-plugin-install-uninstall-probe.test.ts"],
|
|
],
|
|
[
|
|
"scripts/e2e/lib/bundled-plugin-install-uninstall/sweep.sh",
|
|
["test/scripts/bundled-plugin-install-uninstall-probe.test.ts"],
|
|
],
|
|
[
|
|
"scripts/e2e/lib/bun-global-install/assertions.mjs",
|
|
["test/scripts/test-install-sh-docker.test.ts"],
|
|
],
|
|
[
|
|
"scripts/e2e/lib/codex-npm-plugin-live/assertions.mjs",
|
|
["test/scripts/docker-build-helper.test.ts"],
|
|
],
|
|
["scripts/e2e/lib/config-reload/assert-log.mjs", ["test/scripts/e2e-mock-config-limits.test.ts"]],
|
|
[
|
|
"scripts/e2e/lib/docker-stats/assert-resource-ceiling.mjs",
|
|
["test/scripts/docker-stats-resource-ceiling.test.ts"],
|
|
],
|
|
[
|
|
"scripts/e2e/lib/doctor-install-switch/scenario.sh",
|
|
["test/scripts/docker-build-helper.test.ts"],
|
|
],
|
|
[
|
|
"scripts/e2e/lib/fixture.mjs",
|
|
["test/scripts/fixture-config.test.ts", "test/scripts/fixtures-workspace.test.ts"],
|
|
],
|
|
[
|
|
"scripts/e2e/lib/kitchen-sink-plugin/sweep.sh",
|
|
["test/scripts/kitchen-sink-plugin-assertions.test.ts"],
|
|
],
|
|
[
|
|
"scripts/e2e/lib/mcp-code-mode-validation.ts",
|
|
["test/scripts/mcp-code-mode-gateway-client.test.ts"],
|
|
],
|
|
[
|
|
"scripts/e2e/lib/onboard/scenario.sh",
|
|
["test/scripts/e2e-shell-tempfiles.test.ts", "test/scripts/openclaw-test-state.test.ts"],
|
|
],
|
|
["scripts/e2e/lib/package-compat.mjs", ["test/scripts/docker-build-helper.test.ts"]],
|
|
[
|
|
"scripts/e2e/lib/plugin-update/corrupt-update-scenario.sh",
|
|
["test/scripts/plugin-update-unchanged-docker.test.ts"],
|
|
],
|
|
[
|
|
"scripts/e2e/lib/plugin-update/probe.mjs",
|
|
["test/scripts/plugin-update-unchanged-docker.test.ts"],
|
|
],
|
|
[
|
|
"scripts/e2e/lib/plugin-update/unchanged-scenario.sh",
|
|
["test/scripts/plugin-update-unchanged-docker.test.ts"],
|
|
],
|
|
[
|
|
"scripts/e2e/plugin-update-unchanged-docker.sh",
|
|
[
|
|
"test/scripts/docker-build-helper.test.ts",
|
|
"test/scripts/docker-e2e-plan.test.ts",
|
|
"test/scripts/plugin-prerelease-test-plan.test.ts",
|
|
"test/scripts/plugin-update-unchanged-docker.test.ts",
|
|
],
|
|
],
|
|
[
|
|
"scripts/e2e/update-corrupt-plugin-docker.sh",
|
|
[
|
|
"test/scripts/docker-build-helper.test.ts",
|
|
"test/scripts/docker-e2e-plan.test.ts",
|
|
"test/scripts/package-acceptance-workflow.test.ts",
|
|
"test/scripts/plugin-update-unchanged-docker.test.ts",
|
|
],
|
|
],
|
|
[
|
|
"scripts/e2e/plugins-docker.sh",
|
|
[
|
|
"test/scripts/docker-build-helper.test.ts",
|
|
"test/scripts/docker-e2e-plan.test.ts",
|
|
"test/scripts/plugins-assertions.test.ts",
|
|
],
|
|
],
|
|
["scripts/e2e/lib/plugins/clawhub.sh", ["test/scripts/plugins-assertions.test.ts"]],
|
|
["scripts/e2e/lib/plugins/fixtures.sh", ["test/scripts/plugins-assertions.test.ts"]],
|
|
["scripts/e2e/lib/plugins/marketplace.sh", ["test/scripts/plugins-assertions.test.ts"]],
|
|
["scripts/e2e/lib/plugins/sweep.sh", ["test/scripts/plugins-assertions.test.ts"]],
|
|
[
|
|
"scripts/e2e/lib/release-plugin-marketplace/scenario.sh",
|
|
["test/scripts/docker-build-helper.test.ts"],
|
|
],
|
|
[
|
|
"scripts/e2e/lib/release-typed-onboarding/scenario.sh",
|
|
["test/scripts/docker-build-helper.test.ts"],
|
|
],
|
|
[
|
|
"scripts/e2e/lib/release-upgrade-user-journey/scenario.sh",
|
|
["test/scripts/docker-build-helper.test.ts"],
|
|
],
|
|
[
|
|
"scripts/e2e/release-plugin-marketplace-docker.sh",
|
|
[
|
|
"test/scripts/docker-build-helper.test.ts",
|
|
"test/scripts/docker-e2e-plan.test.ts",
|
|
"test/scripts/package-acceptance-workflow.test.ts",
|
|
],
|
|
],
|
|
[
|
|
"scripts/e2e/release-typed-onboarding-docker.sh",
|
|
[
|
|
"test/scripts/docker-build-helper.test.ts",
|
|
"test/scripts/docker-e2e-plan.test.ts",
|
|
"test/scripts/package-acceptance-workflow.test.ts",
|
|
],
|
|
],
|
|
[
|
|
"scripts/e2e/release-upgrade-user-journey-docker.sh",
|
|
[
|
|
"test/scripts/docker-build-helper.test.ts",
|
|
"test/scripts/docker-e2e-plan.test.ts",
|
|
"test/scripts/package-acceptance-workflow.test.ts",
|
|
],
|
|
],
|
|
[
|
|
"scripts/e2e/release-user-journey-docker.sh",
|
|
[
|
|
"test/scripts/docker-build-helper.test.ts",
|
|
"test/scripts/docker-e2e-plan.test.ts",
|
|
"test/scripts/package-acceptance-workflow.test.ts",
|
|
"test/scripts/release-user-journey-assertions.test.ts",
|
|
],
|
|
],
|
|
[
|
|
"scripts/e2e/lib/release-assertion-files.mjs",
|
|
[
|
|
"test/scripts/release-scenarios-assertions.test.ts",
|
|
"test/scripts/release-user-journey-assertions.test.ts",
|
|
],
|
|
],
|
|
["scripts/e2e/lib/skills/clawhub-install-proof.sh", ["test/scripts/e2e-shell-tempfiles.test.ts"]],
|
|
[
|
|
"scripts/e2e/lib/update-channel-switch/assertions.mjs",
|
|
["test/scripts/docker-build-helper.test.ts"],
|
|
],
|
|
[
|
|
"scripts/e2e/live-plugin-tool-docker.sh",
|
|
[
|
|
"test/scripts/docker-build-helper.test.ts",
|
|
"test/scripts/docker-e2e-plan.test.ts",
|
|
"test/scripts/live-plugin-tool-assertions.test.ts",
|
|
],
|
|
],
|
|
[
|
|
"scripts/e2e/openai-image-auth-docker.sh",
|
|
[
|
|
"test/scripts/docker-build-helper.test.ts",
|
|
"test/scripts/docker-e2e-plan.test.ts",
|
|
"test/scripts/openai-image-auth-docker-client.test.ts",
|
|
"extensions/openai/image-generation-provider.test.ts",
|
|
],
|
|
],
|
|
[
|
|
"scripts/e2e/openai-image-auth-docker-client.ts",
|
|
[
|
|
"test/scripts/openai-image-auth-docker-client.test.ts",
|
|
"extensions/openai/image-generation-provider.test.ts",
|
|
"src/image-generation/openai-compatible-image-provider.test.ts",
|
|
],
|
|
],
|
|
[
|
|
"scripts/e2e/openai-chat-tools-docker.sh",
|
|
["test/scripts/openai-chat-tools-client.test.ts", "test/scripts/docker-e2e-plan.test.ts"],
|
|
],
|
|
[
|
|
"scripts/e2e/openai-web-search-minimal-docker.sh",
|
|
[
|
|
"test/scripts/docker-build-helper.test.ts",
|
|
"test/scripts/docker-e2e-plan.test.ts",
|
|
"test/scripts/openai-web-search-minimal-client.test.ts",
|
|
"test/scripts/openai-web-search-minimal-assertions.test.ts",
|
|
],
|
|
],
|
|
[
|
|
"scripts/e2e/openwebui-docker.sh",
|
|
[
|
|
"test/scripts/docker-build-helper.test.ts",
|
|
"test/scripts/docker-e2e-plan.test.ts",
|
|
"test/scripts/openwebui-probe.test.ts",
|
|
"test/scripts/fixture-config.test.ts",
|
|
],
|
|
],
|
|
[
|
|
"scripts/e2e/plugin-binding-command-escape-docker.sh",
|
|
[
|
|
"test/scripts/docker-build-helper.test.ts",
|
|
"test/scripts/docker-e2e-plan.test.ts",
|
|
"test/scripts/package-acceptance-workflow.test.ts",
|
|
],
|
|
],
|
|
["scripts/e2e/qr-import-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/dev/gateway-smoke.ts", ["test/e2e/qa-lab/runtime/gateway-smoke.e2e.test.ts"]],
|
|
["scripts/e2e/cron-mcp-cleanup-seed.ts", ["test/scripts/docker-e2e-seeds.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";
|
|
/** 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,
|
|
);
|
|
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",
|
|
]);
|
|
function resolveTestProjectsVitestNoOutputTimeoutMs(config) {
|
|
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(
|
|
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 isExistingDirectoryTarget(arg, cwd) {
|
|
try {
|
|
return fs.statSync(path.resolve(cwd, arg)).isDirectory();
|
|
} 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 relative = toRepoRelativeTarget(targetArg, cwd);
|
|
if (relative === "src/commands" && isExistingDirectoryTarget(targetArg, cwd)) {
|
|
return [COMMANDS_LIGHT_VITEST_CONFIG, COMMANDS_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) {
|
|
return exactSourceDirectoryRoots.some((root) => isPathAtOrUnder(relative, root));
|
|
}
|
|
|
|
function resolveExactSourceDirectoryTestTargets(targetArg, cwd) {
|
|
if (!isExistingDirectoryTarget(targetArg, cwd)) {
|
|
return null;
|
|
}
|
|
const relative = toRepoRelativeTarget(targetArg, cwd).replace(/\/+$/u, "");
|
|
if (!isExactSourceDirectoryTarget(relative)) {
|
|
return null;
|
|
}
|
|
const prefix = `${relative}/`;
|
|
const lightTargets = uniqueOrdered([
|
|
...getUnitFastTestFiles(),
|
|
...pluginSdkLightTestFiles,
|
|
...commandsLightTestFiles,
|
|
]).filter((file) => file.startsWith(prefix));
|
|
return lightTargets.length > 0 ? [...lightTargets, targetArg] : null;
|
|
}
|
|
|
|
/**
|
|
* Finds explicit test path targets that do not match any known project plan.
|
|
*/
|
|
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 (const current of queue) {
|
|
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 (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) {
|
|
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)];
|
|
}
|
|
|
|
const TOOLING_SCRIPT_PATH_PATTERN = /^scripts\/(.+)\.(?:mjs|cjs|js|mts|cts|ts|sh|py|ps1)$/u;
|
|
|
|
function resolveConventionalToolingTestTargets(changedPath, cwd = process.cwd()) {
|
|
const match = TOOLING_SCRIPT_PATH_PATTERN.exec(changedPath);
|
|
if (!match) {
|
|
return null;
|
|
}
|
|
const stem = match[1];
|
|
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) {
|
|
return TOOLING_SCRIPT_PATH_PATTERN.test(changedPath);
|
|
}
|
|
|
|
function resolveParallelsToolingTestTargets(changedPath) {
|
|
if (!/^scripts\/e2e\/parallels\/[^/]+\.ts$/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 resolveToolingTestTargets(changedPath, cwd = process.cwd()) {
|
|
const explicitTargets =
|
|
TOOLING_SOURCE_TEST_TARGETS.get(changedPath) ??
|
|
TOOLING_TEST_TARGETS.get(changedPath) ??
|
|
resolveParallelsToolingTestTargets(changedPath);
|
|
const conventionalTargets = resolveConventionalToolingTestTargets(changedPath, cwd);
|
|
if (explicitTargets && conventionalTargets) {
|
|
return uniqueOrdered([...explicitTargets, ...conventionalTargets]);
|
|
}
|
|
return (
|
|
explicitTargets ??
|
|
conventionalTargets ??
|
|
(isToolingScriptPath(changedPath) ? [TOOLING_VITEST_CONFIG] : null)
|
|
);
|
|
}
|
|
|
|
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 shouldCombineSiblingTestWithImportGraph(changedPath) {
|
|
return changedPath.startsWith("test/helpers/");
|
|
}
|
|
|
|
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 && !shouldCombineSiblingTestWithImportGraph(changedPath)) {
|
|
return [siblingTest];
|
|
}
|
|
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 siblingTest ? uniqueOrdered([siblingTest, ...affectedTests]) : affectedTests;
|
|
}
|
|
}
|
|
return siblingTest ? [siblingTest] : null;
|
|
}
|
|
|
|
function isDeletedChangedTestTarget(changedPath, cwd) {
|
|
return isTestFileTarget(changedPath) && !fs.existsSync(path.join(cwd, changedPath));
|
|
}
|
|
|
|
/**
|
|
* Maps changed repo paths to the smallest useful Vitest target plan.
|
|
*/
|
|
export function resolveChangedTestTargetPlan(changedPaths, options = {}) {
|
|
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 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: [] };
|
|
}
|
|
skippedBroadFallbackPaths.push(changedPath);
|
|
continue;
|
|
}
|
|
if (isRoutableChangedTarget(changedPath)) {
|
|
targets.push(changedPath);
|
|
}
|
|
}
|
|
if (useBroadFallback && changedLanes.extensionImpactFromCore) {
|
|
targets.push("extensions");
|
|
}
|
|
const plan = { 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,
|
|
cwd = process.cwd(),
|
|
listChangedPaths = listChangedPathsFromGit,
|
|
options = {},
|
|
) {
|
|
const plan = resolveChangedTestTargetPlanForArgs(args, cwd, listChangedPaths, options);
|
|
if (!plan) {
|
|
return null;
|
|
}
|
|
if (plan.mode === "broad") {
|
|
return null;
|
|
}
|
|
return plan.targets;
|
|
}
|
|
|
|
export function resolveChangedTestTargetPlanForArgs(
|
|
args,
|
|
cwd = process.cwd(),
|
|
listChangedPaths = listChangedPathsFromGit,
|
|
options = {},
|
|
) {
|
|
const baseRef = extractChangedBaseRef(args);
|
|
if (!baseRef) {
|
|
return null;
|
|
}
|
|
const changedPaths = listChangedPaths(baseRef, cwd);
|
|
return resolveChangedTestTargetPlan(changedPaths, {
|
|
cwd,
|
|
...options,
|
|
});
|
|
}
|
|
|
|
function classifyTarget(arg, cwd) {
|
|
const relative = toRepoRelativeTarget(arg, cwd);
|
|
const configTargetKind = resolveVitestConfigTargetKind(relative);
|
|
if (configTargetKind) {
|
|
return configTargetKind;
|
|
}
|
|
if (isControlUiE2eTarget(relative)) {
|
|
return "uiE2e";
|
|
}
|
|
if (isPathAtOrUnder(relative, "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 === 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, "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 (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, "src/agents")) {
|
|
return "agent";
|
|
}
|
|
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, 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);
|
|
if (isTestFileTarget(relative)) {
|
|
return false;
|
|
}
|
|
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_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);
|
|
}
|
|
}
|
|
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",
|
|
"toolingDocker",
|
|
"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 = parsePositiveInt(
|
|
process.env.OPENCLAW_TEST_PROJECTS_PARALLEL,
|
|
"OPENCLAW_TEST_PROJECTS_PARALLEL",
|
|
);
|
|
const expandToProjectConfigs =
|
|
process.env.OPENCLAW_TEST_PROJECTS_LEAF_SHARDS === "1" ||
|
|
(Number.isFinite(parallelShardCount) && 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 [];
|
|
}
|
|
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"
|
|
);
|
|
}
|
|
|
|
export function shouldExpandLocalFullSuiteShardsByDefault(env = process.env) {
|
|
return env.CI !== "true" && env.GITHUB_ACTIONS !== "true";
|
|
}
|
|
|
|
function parsePositiveInt(value, label) {
|
|
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) {
|
|
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;
|
|
}
|
|
|
|
export function resolveParallelFullSuiteConcurrency(specCount, envInput, hostInfo) {
|
|
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) {
|
|
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] = 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 ?? "");
|
|
}
|
|
|
|
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-${randomUUID()}-${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);
|
|
}
|