mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-02 21:21:32 +00:00
4113 lines
143 KiB
JavaScript
4113 lines
143 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 {
|
|
agentVitestProjectOwners,
|
|
embeddedAgentVitestProjectOwners,
|
|
isAgentsCoreIsolatedTestFile,
|
|
} from "../test/vitest/vitest.agents-paths.mjs";
|
|
import { isChannelSurfaceTestFile } from "../test/vitest/vitest.channel-paths.mjs";
|
|
import {
|
|
cliProcessTestFiles,
|
|
isCliProcessTestFile,
|
|
} from "../test/vitest/vitest.cli-process-paths.mjs";
|
|
import {
|
|
commandsLightTestFiles,
|
|
isCommandsLightTarget,
|
|
resolveCommandsLightIncludePattern,
|
|
} from "../test/vitest/vitest.commands-light-paths.mjs";
|
|
import {
|
|
channelConfigContractPatterns,
|
|
channelRegistryContractPatterns,
|
|
channelSessionContractPatterns,
|
|
channelSurfaceContractPatterns,
|
|
} from "../test/vitest/vitest.contracts-paths.mjs";
|
|
import { isAcpxExtensionRoot } from "../test/vitest/vitest.extension-acpx-paths.mjs";
|
|
import { isActiveMemoryExtensionRoot } from "../test/vitest/vitest.extension-active-memory-paths.mjs";
|
|
import { isBrowserExtensionRoot } from "../test/vitest/vitest.extension-browser-paths.mjs";
|
|
import { resolveSplitChannelExtensionShard } from "../test/vitest/vitest.extension-channel-split-paths.mjs";
|
|
import { isCodexExtensionRoot } from "../test/vitest/vitest.extension-codex-paths.mjs";
|
|
import { isDiffsExtensionRoot } from "../test/vitest/vitest.extension-diffs-paths.mjs";
|
|
import { isFeishuExtensionRoot } from "../test/vitest/vitest.extension-feishu-paths.mjs";
|
|
import { isIrcExtensionRoot } from "../test/vitest/vitest.extension-irc-paths.mjs";
|
|
import {
|
|
isMatrixExtensionRoot,
|
|
matrixExtensionTestRoots,
|
|
} from "../test/vitest/vitest.extension-matrix-paths.mjs";
|
|
import { isMattermostExtensionRoot } from "../test/vitest/vitest.extension-mattermost-paths.mjs";
|
|
import { isMediaExtensionRoot } from "../test/vitest/vitest.extension-media-paths.mjs";
|
|
import { isMemoryExtensionRoot } from "../test/vitest/vitest.extension-memory-paths.mjs";
|
|
import { isMessagingExtensionRoot } from "../test/vitest/vitest.extension-messaging-paths.mjs";
|
|
import { isMiscExtensionRoot } from "../test/vitest/vitest.extension-misc-paths.mjs";
|
|
import { isMsTeamsExtensionRoot } from "../test/vitest/vitest.extension-msteams-paths.mjs";
|
|
import {
|
|
isProviderExtensionRoot,
|
|
isProviderOpenAiExtensionRoot,
|
|
} from "../test/vitest/vitest.extension-provider-paths.mjs";
|
|
import { isQaExtensionRoot } from "../test/vitest/vitest.extension-qa-paths.mjs";
|
|
import { isTelegramExtensionRoot } from "../test/vitest/vitest.extension-telegram-paths.mjs";
|
|
import { isVoiceCallExtensionRoot } from "../test/vitest/vitest.extension-voice-call-paths.mjs";
|
|
import { isWhatsAppExtensionRoot } from "../test/vitest/vitest.extension-whatsapp-paths.mjs";
|
|
import { isZaloExtensionRoot } from "../test/vitest/vitest.extension-zalo-paths.mjs";
|
|
import {
|
|
isPluginSdkLightTarget,
|
|
pluginSdkLightTestFiles,
|
|
resolvePluginSdkLightIncludePattern,
|
|
} from "../test/vitest/vitest.plugin-sdk-paths.mjs";
|
|
import { fullSuiteVitestShards } from "../test/vitest/vitest.test-shards.mjs";
|
|
import {
|
|
isToolingIsolatedTestFile,
|
|
toolingIsolatedTestFiles,
|
|
} from "../test/vitest/vitest.tooling-isolated-paths.mjs";
|
|
import {
|
|
isUiIsolatedTestFile,
|
|
uiIsolatedTestFiles,
|
|
} from "../test/vitest/vitest.ui-isolated-paths.mjs";
|
|
import {
|
|
getUnitFastIsolatedTestFiles,
|
|
getUnitFastTestFiles,
|
|
getUnitFastTimerTestFiles,
|
|
resolveUnitFastIsolatedTestIncludePattern,
|
|
resolveUnitFastTestIncludePattern,
|
|
resolveUnitFastTimerTestIncludePattern,
|
|
} from "../test/vitest/vitest.unit-fast-paths.mjs";
|
|
import {
|
|
isBoundaryTestFile,
|
|
isBundledPluginDependentUnitTestFile,
|
|
} from "../test/vitest/vitest.unit-paths.mjs";
|
|
import {
|
|
detectChangedLanes,
|
|
listChangedPathsFromGit as listChangedPathsFromGitSource,
|
|
} from "./changed-lanes.mjs";
|
|
import { getChangedPathFacts } from "./lib/changed-path-facts.mjs";
|
|
import { createExtensionTestProcessTargetChunks } from "./lib/extension-test-plan.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_EMBEDDED_AGENT_TEST_ROOT = agentVitestProjectOwners.embedded.root;
|
|
const AGENTS_CORE_ISOLATED_VITEST_CONFIG = agentVitestProjectOwners.coreIsolated.config;
|
|
const AGENTS_CORE_VITEST_CONFIG = agentVitestProjectOwners.core.config;
|
|
const AGENTS_EMBEDDED_AGENT_VITEST_CONFIG = agentVitestProjectOwners.embedded.config;
|
|
const AGENTS_EMBEDDED_AGENT_INCOMPLETE_TURN_VITEST_CONFIG =
|
|
agentVitestProjectOwners.embeddedIncompleteTurn.config;
|
|
const AGENTS_EMBEDDED_AGENT_OVERFLOW_COMPACTION_VITEST_CONFIG =
|
|
agentVitestProjectOwners.embeddedOverflowCompaction.config;
|
|
const AGENTS_EMBEDDED_AGENT_RUN_VITEST_CONFIG = agentVitestProjectOwners.embeddedRun.config;
|
|
const AGENTS_SUPPORT_VITEST_CONFIG = agentVitestProjectOwners.support.config;
|
|
const AGENTS_TOOLS_VITEST_CONFIG = agentVitestProjectOwners.tools.config;
|
|
const AGENTS_VITEST_CONFIG = agentVitestProjectOwners.all.config;
|
|
const ACP_VITEST_CONFIG = "test/vitest/vitest.acp.config.ts";
|
|
const AUTO_REPLY_CORE_VITEST_CONFIG = "test/vitest/vitest.auto-reply-core.config.ts";
|
|
const AUTO_REPLY_VITEST_CONFIG = "test/vitest/vitest.auto-reply.config.ts";
|
|
const AUTO_REPLY_REPLY_VITEST_CONFIG = "test/vitest/vitest.auto-reply-reply.config.ts";
|
|
const AUTO_REPLY_TOP_LEVEL_VITEST_CONFIG = "test/vitest/vitest.auto-reply-top-level.config.ts";
|
|
const BOUNDARY_VITEST_CONFIG = "test/vitest/vitest.boundary.config.ts";
|
|
const BUNDLED_VITEST_CONFIG = "test/vitest/vitest.bundled.config.ts";
|
|
const CHANNEL_VITEST_CONFIG = "test/vitest/vitest.channels.config.ts";
|
|
const CLI_PROCESS_VITEST_CONFIG = "test/vitest/vitest.cli-process.config.ts";
|
|
const CLI_VITEST_CONFIG = "test/vitest/vitest.cli.config.ts";
|
|
const COMMANDS_LIGHT_VITEST_CONFIG = "test/vitest/vitest.commands-light.config.ts";
|
|
const COMMANDS_VITEST_CONFIG = "test/vitest/vitest.commands.config.ts";
|
|
const CONTRACTS_CHANNEL_CONFIG_VITEST_CONFIG =
|
|
"test/vitest/vitest.contracts-channel-config.config.ts";
|
|
const CONTRACTS_CHANNEL_REGISTRY_VITEST_CONFIG =
|
|
"test/vitest/vitest.contracts-channel-registry.config.ts";
|
|
const CONTRACTS_CHANNEL_SESSION_VITEST_CONFIG =
|
|
"test/vitest/vitest.contracts-channel-session.config.ts";
|
|
const CONTRACTS_CHANNEL_SURFACE_VITEST_CONFIG =
|
|
"test/vitest/vitest.contracts-channel-surface.config.ts";
|
|
const CONTRACTS_PLUGIN_VITEST_CONFIG = "test/vitest/vitest.contracts-plugin.config.ts";
|
|
const CRON_VITEST_CONFIG = "test/vitest/vitest.cron.config.ts";
|
|
const DAEMON_VITEST_CONFIG = "test/vitest/vitest.daemon.config.ts";
|
|
const E2E_VITEST_CONFIG = "test/vitest/vitest.e2e.config.ts";
|
|
const EXTENSION_ACTIVE_MEMORY_VITEST_CONFIG =
|
|
"test/vitest/vitest.extension-active-memory.config.ts";
|
|
const EXTENSION_ACPX_VITEST_CONFIG = "test/vitest/vitest.extension-acpx.config.ts";
|
|
const EXTENSION_BROWSER_VITEST_CONFIG = "test/vitest/vitest.extension-browser.config.ts";
|
|
const EXTENSION_CODEX_VITEST_CONFIG = "test/vitest/vitest.extension-codex.config.ts";
|
|
const EXTENSION_CODEX_APP_SERVER_ATTEMPT_VITEST_CONFIG =
|
|
"test/vitest/vitest.extension-codex-app-server-attempt.config.ts";
|
|
const EXTENSION_CODEX_APP_SERVER_ATTEMPT_EXTRA_VITEST_CONFIG =
|
|
"test/vitest/vitest.extension-codex-app-server-attempt-extra.config.ts";
|
|
const EXTENSION_CODEX_APP_SERVER_ATTEMPT_LIGHT_VITEST_CONFIG =
|
|
"test/vitest/vitest.extension-codex-app-server-attempt-light.config.ts";
|
|
const EXTENSION_CODEX_APP_SERVER_ATTEMPT_SUPPORT_VITEST_CONFIG =
|
|
"test/vitest/vitest.extension-codex-app-server-attempt-support.config.ts";
|
|
const EXTENSION_CODEX_APP_SERVER_RUNTIME_VITEST_CONFIG =
|
|
"test/vitest/vitest.extension-codex-app-server-runtime.config.ts";
|
|
const EXTENSION_CODEX_APP_SERVER_SUPPORT_VITEST_CONFIG =
|
|
"test/vitest/vitest.extension-codex-app-server-support.config.ts";
|
|
const EXTENSION_CODEX_APP_SERVER_TOOLS_VITEST_CONFIG =
|
|
"test/vitest/vitest.extension-codex-app-server-tools.config.ts";
|
|
const EXTENSION_CODEX_SURFACE_VITEST_CONFIG =
|
|
"test/vitest/vitest.extension-codex-surface.config.ts";
|
|
const EXTENSION_CHANNELS_VITEST_CONFIG = "test/vitest/vitest.extension-channels.config.ts";
|
|
const EXTENSION_DIFFS_VITEST_CONFIG = "test/vitest/vitest.extension-diffs.config.ts";
|
|
const EXTENSION_DISCORD_VITEST_CONFIG = "test/vitest/vitest.extension-discord.config.ts";
|
|
const EXTENSION_FEISHU_VITEST_CONFIG = "test/vitest/vitest.extension-feishu.config.ts";
|
|
const EXTENSION_IMESSAGE_VITEST_CONFIG = "test/vitest/vitest.extension-imessage.config.ts";
|
|
const EXTENSION_IRC_VITEST_CONFIG = "test/vitest/vitest.extension-irc.config.ts";
|
|
const EXTENSION_LINE_VITEST_CONFIG = "test/vitest/vitest.extension-line.config.ts";
|
|
const EXTENSION_MATTERMOST_VITEST_CONFIG = "test/vitest/vitest.extension-mattermost.config.ts";
|
|
const EXTENSION_MEDIA_VITEST_CONFIG = "test/vitest/vitest.extension-media.config.ts";
|
|
const EXTENSION_MATRIX_VITEST_CONFIG = "test/vitest/vitest.extension-matrix.config.ts";
|
|
const EXTENSION_MEMORY_VITEST_CONFIG = "test/vitest/vitest.extension-memory.config.ts";
|
|
const EXTENSION_MSTEAMS_VITEST_CONFIG = "test/vitest/vitest.extension-msteams.config.ts";
|
|
const EXTENSION_MESSAGING_VITEST_CONFIG = "test/vitest/vitest.extension-messaging.config.ts";
|
|
const EXTENSION_MISC_VITEST_CONFIG = "test/vitest/vitest.extension-misc.config.ts";
|
|
const EXTENSION_PROVIDER_OPENAI_VITEST_CONFIG =
|
|
"test/vitest/vitest.extension-provider-openai.config.ts";
|
|
const EXTENSION_PROVIDERS_VITEST_CONFIG = "test/vitest/vitest.extension-providers.config.ts";
|
|
const EXTENSION_QA_VITEST_CONFIG = "test/vitest/vitest.extension-qa.config.ts";
|
|
const EXTENSION_SIGNAL_VITEST_CONFIG = "test/vitest/vitest.extension-signal.config.ts";
|
|
const EXTENSION_SLACK_VITEST_CONFIG = "test/vitest/vitest.extension-slack.config.ts";
|
|
const EXTENSION_TELEGRAM_VITEST_CONFIG = "test/vitest/vitest.extension-telegram.config.ts";
|
|
const EXTENSION_VOICE_CALL_VITEST_CONFIG = "test/vitest/vitest.extension-voice-call.config.ts";
|
|
const EXTENSION_WHATSAPP_VITEST_CONFIG = "test/vitest/vitest.extension-whatsapp.config.ts";
|
|
const EXTENSION_ZALO_VITEST_CONFIG = "test/vitest/vitest.extension-zalo.config.ts";
|
|
const EXTENSIONS_VITEST_CONFIG = "test/vitest/vitest.extensions.config.ts";
|
|
const FULL_AGENTIC_VITEST_CONFIG = "test/vitest/vitest.full-agentic.config.ts";
|
|
const FULL_EXTENSIONS_VITEST_CONFIG = "test/vitest/vitest.full-extensions.config.ts";
|
|
const GATEWAY_CLIENT_VITEST_CONFIG = "test/vitest/vitest.gateway-client.config.ts";
|
|
const GATEWAY_CORE_VITEST_CONFIG = "test/vitest/vitest.gateway-core.config.ts";
|
|
const GATEWAY_METHODS_VITEST_CONFIG = "test/vitest/vitest.gateway-methods.config.ts";
|
|
const GATEWAY_SERVER_VITEST_CONFIG = "test/vitest/vitest.gateway-server.config.ts";
|
|
const GATEWAY_VITEST_CONFIG = "test/vitest/vitest.gateway.config.ts";
|
|
const HOOKS_VITEST_CONFIG = "test/vitest/vitest.hooks.config.ts";
|
|
const INFRA_VITEST_CONFIG = "test/vitest/vitest.infra.config.ts";
|
|
const MEDIA_VITEST_CONFIG = "test/vitest/vitest.media.config.ts";
|
|
const MEDIA_UNDERSTANDING_VITEST_CONFIG = "test/vitest/vitest.media-understanding.config.ts";
|
|
const LOGGING_VITEST_CONFIG = "test/vitest/vitest.logging.config.ts";
|
|
const PLUGIN_SDK_LIGHT_VITEST_CONFIG = "test/vitest/vitest.plugin-sdk-light.config.ts";
|
|
const PLUGIN_SDK_VITEST_CONFIG = "test/vitest/vitest.plugin-sdk.config.ts";
|
|
const PLUGINS_VITEST_CONFIG = "test/vitest/vitest.plugins.config.ts";
|
|
const UNIT_FAST_VITEST_CONFIG = "test/vitest/vitest.unit-fast.config.ts";
|
|
const UNIT_FAST_ISOLATED_VITEST_CONFIG = "test/vitest/vitest.unit-fast-isolated.config.ts";
|
|
const UNIT_FAST_FAKE_TIMERS_VITEST_CONFIG = "test/vitest/vitest.unit-fast-fake-timers.config.ts";
|
|
const UNIT_SECURITY_VITEST_CONFIG = "test/vitest/vitest.unit-security.config.ts";
|
|
const UNIT_SRC_VITEST_CONFIG = "test/vitest/vitest.unit-src.config.ts";
|
|
const UNIT_SUPPORT_VITEST_CONFIG = "test/vitest/vitest.unit-support.config.ts";
|
|
|
|
const FULL_SUITE_CONFIG_WEIGHT = new Map([
|
|
[GATEWAY_VITEST_CONFIG, 180],
|
|
[GATEWAY_SERVER_VITEST_CONFIG, 180],
|
|
[GATEWAY_CORE_VITEST_CONFIG, 179],
|
|
[GATEWAY_CLIENT_VITEST_CONFIG, 178],
|
|
[GATEWAY_METHODS_VITEST_CONFIG, 177],
|
|
[COMMANDS_VITEST_CONFIG, 175],
|
|
[AGENTS_CORE_VITEST_CONFIG, 170],
|
|
[AGENTS_EMBEDDED_AGENT_VITEST_CONFIG, 169],
|
|
[AGENTS_EMBEDDED_AGENT_INCOMPLETE_TURN_VITEST_CONFIG, 169],
|
|
[AGENTS_EMBEDDED_AGENT_OVERFLOW_COMPACTION_VITEST_CONFIG, 169],
|
|
[AGENTS_EMBEDDED_AGENT_RUN_VITEST_CONFIG, 169],
|
|
[AGENTS_SUPPORT_VITEST_CONFIG, 168],
|
|
[AGENTS_TOOLS_VITEST_CONFIG, 167],
|
|
[EXTENSION_CODEX_VITEST_CONFIG, 168],
|
|
[EXTENSION_CODEX_APP_SERVER_ATTEMPT_VITEST_CONFIG, 168],
|
|
[EXTENSION_CODEX_APP_SERVER_ATTEMPT_EXTRA_VITEST_CONFIG, 118],
|
|
[EXTENSION_CODEX_APP_SERVER_ATTEMPT_LIGHT_VITEST_CONFIG, 82],
|
|
[EXTENSION_CODEX_APP_SERVER_ATTEMPT_SUPPORT_VITEST_CONFIG, 80],
|
|
[EXTENSION_CODEX_APP_SERVER_RUNTIME_VITEST_CONFIG, 88],
|
|
[EXTENSION_CODEX_APP_SERVER_TOOLS_VITEST_CONFIG, 78],
|
|
[EXTENSION_CODEX_APP_SERVER_SUPPORT_VITEST_CONFIG, 72],
|
|
[EXTENSION_CODEX_SURFACE_VITEST_CONFIG, 68],
|
|
[EXTENSION_VOICE_CALL_VITEST_CONFIG, 169],
|
|
[EXTENSIONS_VITEST_CONFIG, 168],
|
|
[EXTENSION_PROVIDER_OPENAI_VITEST_CONFIG, 167],
|
|
["test/vitest/vitest.runtime-config.config.ts", 166],
|
|
[CONTRACTS_CHANNEL_CONFIG_VITEST_CONFIG, 85],
|
|
[CONTRACTS_CHANNEL_SURFACE_VITEST_CONFIG, 60],
|
|
[CONTRACTS_CHANNEL_SESSION_VITEST_CONFIG, 50],
|
|
[CONTRACTS_CHANNEL_REGISTRY_VITEST_CONFIG, 35],
|
|
[CONTRACTS_PLUGIN_VITEST_CONFIG, 20],
|
|
["test/vitest/vitest.tasks.config.ts", 165],
|
|
[CHANNEL_VITEST_CONFIG, 164],
|
|
[UNIT_FAST_VITEST_CONFIG, 160],
|
|
[UNIT_FAST_ISOLATED_VITEST_CONFIG, 159],
|
|
[AUTO_REPLY_REPLY_VITEST_CONFIG, 155],
|
|
[INFRA_VITEST_CONFIG, 145],
|
|
["test/vitest/vitest.secrets.config.ts", 140],
|
|
[CRON_VITEST_CONFIG, 135],
|
|
["test/vitest/vitest.wizard.config.ts", 130],
|
|
[UNIT_SRC_VITEST_CONFIG, 125],
|
|
[EXTENSION_MATRIX_VITEST_CONFIG, 100],
|
|
[EXTENSION_DISCORD_VITEST_CONFIG, 98],
|
|
[EXTENSION_PROVIDERS_VITEST_CONFIG, 96],
|
|
[EXTENSION_TELEGRAM_VITEST_CONFIG, 94],
|
|
[EXTENSION_WHATSAPP_VITEST_CONFIG, 92],
|
|
[AUTO_REPLY_CORE_VITEST_CONFIG, 90],
|
|
[CLI_PROCESS_VITEST_CONFIG, 87],
|
|
[CLI_VITEST_CONFIG, 86],
|
|
[MEDIA_VITEST_CONFIG, 84],
|
|
[PLUGINS_VITEST_CONFIG, 82],
|
|
[BUNDLED_VITEST_CONFIG, 80],
|
|
[EXTENSION_SLACK_VITEST_CONFIG, 78],
|
|
[COMMANDS_LIGHT_VITEST_CONFIG, 48],
|
|
[PLUGIN_SDK_VITEST_CONFIG, 46],
|
|
[AUTO_REPLY_TOP_LEVEL_VITEST_CONFIG, 45],
|
|
[PLUGIN_SDK_LIGHT_VITEST_CONFIG, 38],
|
|
[DAEMON_VITEST_CONFIG, 36],
|
|
[BOUNDARY_VITEST_CONFIG, 34],
|
|
["test/vitest/vitest.tooling.config.ts", 32],
|
|
["test/vitest/vitest.tooling-isolated.config.ts", 1],
|
|
[UNIT_SECURITY_VITEST_CONFIG, 30],
|
|
[UNIT_SUPPORT_VITEST_CONFIG, 28],
|
|
[EXTENSION_ZALO_VITEST_CONFIG, 24],
|
|
[EXTENSION_IRC_VITEST_CONFIG, 20],
|
|
[EXTENSION_FEISHU_VITEST_CONFIG, 18],
|
|
[EXTENSION_MATTERMOST_VITEST_CONFIG, 16],
|
|
[EXTENSION_MESSAGING_VITEST_CONFIG, 14],
|
|
[EXTENSION_IMESSAGE_VITEST_CONFIG, 13],
|
|
[EXTENSION_LINE_VITEST_CONFIG, 12],
|
|
[EXTENSION_SIGNAL_VITEST_CONFIG, 11],
|
|
[EXTENSION_ACPX_VITEST_CONFIG, 10],
|
|
[EXTENSION_DIFFS_VITEST_CONFIG, 8],
|
|
[EXTENSION_ACTIVE_MEMORY_VITEST_CONFIG, 7],
|
|
[EXTENSION_MEMORY_VITEST_CONFIG, 6],
|
|
[EXTENSION_MSTEAMS_VITEST_CONFIG, 4],
|
|
]);
|
|
|
|
function resolveConfigSortWeight(config, 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 BROAD_TOOLING_SCRIPT_TEST_PATTERNS = new Set([
|
|
"test/scripts/**/*.test.ts",
|
|
"test/scripts/*.test.ts",
|
|
]);
|
|
const BROAD_TOOLING_SCRIPT_TEST_TARGET_CHUNK_SIZE = 60;
|
|
const FULL_SUITE_AGENTS_CORE_TEST_TARGET_CHUNK_COUNT = 6;
|
|
const FULL_SUITE_TOOLING_TEST_TARGET_CHUNK_SIZE = 2;
|
|
const FULL_SUITE_UNIT_FAST_TEST_TARGET_CHUNK_SIZE = 70;
|
|
const TUI_VITEST_CONFIG = "test/vitest/vitest.tui.config.ts";
|
|
const TUI_PTY_VITEST_CONFIG = "test/vitest/vitest.tui-pty.config.ts";
|
|
const UI_VITEST_CONFIG = "test/vitest/vitest.ui.config.ts";
|
|
const UI_E2E_VITEST_CONFIG = "test/vitest/vitest.ui-e2e.config.ts";
|
|
const UI_ISOLATED_VITEST_CONFIG = "test/vitest/vitest.ui-isolated.config.ts";
|
|
const UTILS_VITEST_CONFIG = "test/vitest/vitest.utils.config.ts";
|
|
const WIZARD_VITEST_CONFIG = "test/vitest/vitest.wizard.config.ts";
|
|
const INCLUDE_FILE_ENV_KEY = "OPENCLAW_VITEST_INCLUDE_FILE";
|
|
const FS_MODULE_CACHE_PATH_ENV_KEY = "OPENCLAW_VITEST_FS_MODULE_CACHE_PATH";
|
|
const FAILED_SHARD_DIGEST_LIMIT = 12;
|
|
const CHANGED_ARGS_PATTERN = /^--changed(?:=(.+))?$/u;
|
|
const VITEST_CONFIG_BY_KIND = {
|
|
unitFast: UNIT_FAST_VITEST_CONFIG,
|
|
unitFastIsolated: UNIT_FAST_ISOLATED_VITEST_CONFIG,
|
|
unitFastFakeTimers: UNIT_FAST_FAKE_TIMERS_VITEST_CONFIG,
|
|
boundary: BOUNDARY_VITEST_CONFIG,
|
|
toolingDocker: TOOLING_DOCKER_VITEST_CONFIG,
|
|
toolingIsolated: TOOLING_ISOLATED_VITEST_CONFIG,
|
|
tooling: TOOLING_VITEST_CONFIG,
|
|
contractsChannelSurface: CONTRACTS_CHANNEL_SURFACE_VITEST_CONFIG,
|
|
contractsChannelConfig: CONTRACTS_CHANNEL_CONFIG_VITEST_CONFIG,
|
|
contractsChannelRegistry: CONTRACTS_CHANNEL_REGISTRY_VITEST_CONFIG,
|
|
contractsChannelSession: CONTRACTS_CHANNEL_SESSION_VITEST_CONFIG,
|
|
contractsPlugin: CONTRACTS_PLUGIN_VITEST_CONFIG,
|
|
bundled: BUNDLED_VITEST_CONFIG,
|
|
gateway: GATEWAY_VITEST_CONFIG,
|
|
gatewayCore: GATEWAY_CORE_VITEST_CONFIG,
|
|
gatewayClient: GATEWAY_CLIENT_VITEST_CONFIG,
|
|
gatewayMethods: GATEWAY_METHODS_VITEST_CONFIG,
|
|
gatewayServer: GATEWAY_SERVER_VITEST_CONFIG,
|
|
hooks: HOOKS_VITEST_CONFIG,
|
|
infra: INFRA_VITEST_CONFIG,
|
|
runtimeConfig: RUNTIME_CONFIG_VITEST_CONFIG,
|
|
cron: CRON_VITEST_CONFIG,
|
|
daemon: DAEMON_VITEST_CONFIG,
|
|
media: MEDIA_VITEST_CONFIG,
|
|
logging: LOGGING_VITEST_CONFIG,
|
|
pluginSdkLight: PLUGIN_SDK_LIGHT_VITEST_CONFIG,
|
|
pluginSdk: PLUGIN_SDK_VITEST_CONFIG,
|
|
process: PROCESS_VITEST_CONFIG,
|
|
secrets: SECRETS_VITEST_CONFIG,
|
|
sharedCore: SHARED_CORE_VITEST_CONFIG,
|
|
tasks: TASKS_VITEST_CONFIG,
|
|
tui: TUI_VITEST_CONFIG,
|
|
tuiPty: TUI_PTY_VITEST_CONFIG,
|
|
mediaUnderstanding: MEDIA_UNDERSTANDING_VITEST_CONFIG,
|
|
acp: ACP_VITEST_CONFIG,
|
|
cliProcess: CLI_PROCESS_VITEST_CONFIG,
|
|
cli: CLI_VITEST_CONFIG,
|
|
commandLight: COMMANDS_LIGHT_VITEST_CONFIG,
|
|
command: COMMANDS_VITEST_CONFIG,
|
|
autoReply: AUTO_REPLY_VITEST_CONFIG,
|
|
autoReplyCore: AUTO_REPLY_CORE_VITEST_CONFIG,
|
|
autoReplyReply: AUTO_REPLY_REPLY_VITEST_CONFIG,
|
|
autoReplyTopLevel: AUTO_REPLY_TOP_LEVEL_VITEST_CONFIG,
|
|
agentCore: AGENTS_CORE_VITEST_CONFIG,
|
|
agentEmbedded: AGENTS_EMBEDDED_AGENT_VITEST_CONFIG,
|
|
agentEmbeddedIncompleteTurn: AGENTS_EMBEDDED_AGENT_INCOMPLETE_TURN_VITEST_CONFIG,
|
|
agentEmbeddedOverflowCompaction: AGENTS_EMBEDDED_AGENT_OVERFLOW_COMPACTION_VITEST_CONFIG,
|
|
agentEmbeddedRun: AGENTS_EMBEDDED_AGENT_RUN_VITEST_CONFIG,
|
|
agentSupport: AGENTS_SUPPORT_VITEST_CONFIG,
|
|
agentTools: AGENTS_TOOLS_VITEST_CONFIG,
|
|
agent: AGENTS_VITEST_CONFIG,
|
|
agentsCoreIsolated: AGENTS_CORE_ISOLATED_VITEST_CONFIG,
|
|
agentsCore: AGENTS_CORE_VITEST_CONFIG,
|
|
agentsSupport: AGENTS_SUPPORT_VITEST_CONFIG,
|
|
agentsTools: AGENTS_TOOLS_VITEST_CONFIG,
|
|
plugin: PLUGINS_VITEST_CONFIG,
|
|
ui: UI_VITEST_CONFIG,
|
|
uiIsolated: UI_ISOLATED_VITEST_CONFIG,
|
|
uiE2e: UI_E2E_VITEST_CONFIG,
|
|
unitSrc: UNIT_SRC_VITEST_CONFIG,
|
|
unitSecurity: UNIT_SECURITY_VITEST_CONFIG,
|
|
unitSupport: UNIT_SUPPORT_VITEST_CONFIG,
|
|
utils: UTILS_VITEST_CONFIG,
|
|
wizard: WIZARD_VITEST_CONFIG,
|
|
e2e: E2E_VITEST_CONFIG,
|
|
extensionActiveMemory: EXTENSION_ACTIVE_MEMORY_VITEST_CONFIG,
|
|
extensionAcpx: EXTENSION_ACPX_VITEST_CONFIG,
|
|
extensionCodex: EXTENSION_CODEX_VITEST_CONFIG,
|
|
extensionDiffs: EXTENSION_DIFFS_VITEST_CONFIG,
|
|
extensionBrowser: EXTENSION_BROWSER_VITEST_CONFIG,
|
|
extensionDiscord: EXTENSION_DISCORD_VITEST_CONFIG,
|
|
extensionFeishu: EXTENSION_FEISHU_VITEST_CONFIG,
|
|
extensionImessage: EXTENSION_IMESSAGE_VITEST_CONFIG,
|
|
extensionIrc: EXTENSION_IRC_VITEST_CONFIG,
|
|
extensionLine: EXTENSION_LINE_VITEST_CONFIG,
|
|
extensionMattermost: EXTENSION_MATTERMOST_VITEST_CONFIG,
|
|
extensionChannel: EXTENSION_CHANNELS_VITEST_CONFIG,
|
|
extensionTelegram: EXTENSION_TELEGRAM_VITEST_CONFIG,
|
|
extensionVoiceCall: EXTENSION_VOICE_CALL_VITEST_CONFIG,
|
|
extensionWhatsApp: EXTENSION_WHATSAPP_VITEST_CONFIG,
|
|
extensionZalo: EXTENSION_ZALO_VITEST_CONFIG,
|
|
extensionMatrix: EXTENSION_MATRIX_VITEST_CONFIG,
|
|
extensionMedia: EXTENSION_MEDIA_VITEST_CONFIG,
|
|
extensionMemory: EXTENSION_MEMORY_VITEST_CONFIG,
|
|
extensionMisc: EXTENSION_MISC_VITEST_CONFIG,
|
|
extensionMsTeams: EXTENSION_MSTEAMS_VITEST_CONFIG,
|
|
extensionMessaging: EXTENSION_MESSAGING_VITEST_CONFIG,
|
|
extensionProviderOpenAi: EXTENSION_PROVIDER_OPENAI_VITEST_CONFIG,
|
|
extensionProvider: EXTENSION_PROVIDERS_VITEST_CONFIG,
|
|
extensionQa: EXTENSION_QA_VITEST_CONFIG,
|
|
extensionSignal: EXTENSION_SIGNAL_VITEST_CONFIG,
|
|
extensionSlack: EXTENSION_SLACK_VITEST_CONFIG,
|
|
extensionFull: FULL_EXTENSIONS_VITEST_CONFIG,
|
|
channel: CHANNEL_VITEST_CONFIG,
|
|
extension: EXTENSIONS_VITEST_CONFIG,
|
|
};
|
|
const BROAD_CHANGED_FALLBACK_PATTERNS = [
|
|
/^package\.json$/u,
|
|
/^pnpm-lock\.yaml$/u,
|
|
/^test\/setup(?:\.shared|\.extensions|-openclaw-runtime)?\.ts$/u,
|
|
/^vitest(?:\..+)?\.(?:config\.ts|paths\.mjs)$/u,
|
|
/^test\/vitest\/vitest\.(?:config|shared\.config|scoped-config|performance-config)\.ts$/u,
|
|
/^test\/helpers\//u,
|
|
];
|
|
const PRECISE_SOURCE_TEST_TARGETS = new Map([
|
|
...[
|
|
"src/system-agent/setup-inference-persist.ts",
|
|
"src/agents/embedded-agent-runner/run/attempt-dispatch-preparation.ts",
|
|
"src/agents/embedded-agent-runner/run/run-attempt-dispatch.ts",
|
|
].map((sourcePath) => [
|
|
sourcePath,
|
|
[
|
|
"src/agents/embedded-agent-runner/run.overflow-compaction.loop.test.ts",
|
|
"src/commands/onboard-guided.inference.e2e.test.ts",
|
|
],
|
|
]),
|
|
[
|
|
"src/plugins/contracts/tts-contract-suites.ts",
|
|
[
|
|
"src/plugins/contracts/core-extension-facade-boundary.test.ts",
|
|
"src/plugins/contracts/tts.contract.test.ts",
|
|
],
|
|
],
|
|
]);
|
|
const RUNTIME_SIDECAR_BASELINE_OWNER_TEST_TARGETS = ["src/plugins/bundled-plugin-metadata.test.ts"];
|
|
const RUNTIME_SIDECAR_PATH_CONSUMER_TEST_TARGETS = [
|
|
...RUNTIME_SIDECAR_BASELINE_OWNER_TEST_TARGETS,
|
|
"src/infra/update-global.test.ts",
|
|
"src/infra/update-runner.test.ts",
|
|
"test/openclaw-npm-postpublish-verify.test.ts",
|
|
];
|
|
const GITHUB_YAML_PINNING_GUARD_TEST_TARGETS = ["test/scripts/ci-workflow-guards.test.ts"];
|
|
const GROUP_VISIBLE_REPLY_TEST_TARGETS = [
|
|
"src/auto-reply/reply/dispatch-acp.test.ts",
|
|
"src/auto-reply/reply/dispatch-from-config.test.ts",
|
|
"src/auto-reply/reply/followup-runner.test.ts",
|
|
"src/auto-reply/reply/groups.test.ts",
|
|
"extensions/discord/src/monitor/message-handler.process.test.ts",
|
|
"extensions/slack/src/monitor.tool-result.test.ts",
|
|
];
|
|
const GROUP_VISIBLE_REPLY_PROMPT_TEST_TARGETS = [
|
|
"src/agents/system-prompt.test.ts",
|
|
...GROUP_VISIBLE_REPLY_TEST_TARGETS,
|
|
];
|
|
const CHANNEL_CONTRACT_REGISTRY_BACKED_TARGETS = [
|
|
"directory",
|
|
"plugin",
|
|
"surfaces-only",
|
|
"threading",
|
|
].flatMap((suite) =>
|
|
"abcdefgh"
|
|
.split("")
|
|
.map(
|
|
(shard) =>
|
|
`src/channels/plugins/contracts/${suite}.registry-backed-shard-${shard}.contract.test.ts`,
|
|
),
|
|
);
|
|
const TEST_HELPER_NORMALIZE_TEXT_TARGETS = [
|
|
"src/auto-reply/reply/commands-status.test.ts",
|
|
"src/auto-reply/status.test.ts",
|
|
"src/tui/components/chat-log.test.ts",
|
|
];
|
|
const HAPPY_PATH_PROMPT_SNAPSHOT_HELPER_TEST_TARGETS = ["test/scripts/prompt-snapshots.test.ts"];
|
|
const APPCAST_TEST_TARGETS = ["test/appcast.test.ts", "test/scripts/make-appcast.test.ts"];
|
|
const CODEX_VERSION_CONTRACT_TEST_TARGETS = [
|
|
"extensions/codex/src/manifest.test.ts",
|
|
"extensions/openai/openai-provider.test.ts",
|
|
"test/scripts/codex-client-version-contract.test.ts",
|
|
];
|
|
const SOURCE_TEST_TARGETS = new Map([
|
|
...PRECISE_SOURCE_TEST_TARGETS,
|
|
["extensions/codex/package.json", CODEX_VERSION_CONTRACT_TEST_TARGETS],
|
|
["extensions/codex/src/app-server/version.ts", CODEX_VERSION_CONTRACT_TEST_TARGETS],
|
|
["src/test-utils/openclaw-test-state.ts", ["src/test-utils/openclaw-test-state.test.ts"]],
|
|
[
|
|
"src/channels/plugins/contracts/test-helpers/manifest.ts",
|
|
[
|
|
...CHANNEL_CONTRACT_REGISTRY_BACKED_TARGETS,
|
|
"src/channels/plugins/contracts/registry.contract.test.ts",
|
|
"src/channels/plugins/contracts/session-binding.registry-backed.contract.test.ts",
|
|
],
|
|
],
|
|
[
|
|
"src/channels/plugins/contracts/test-helpers/registry-backed-contract-shards.ts",
|
|
CHANNEL_CONTRACT_REGISTRY_BACKED_TARGETS,
|
|
],
|
|
["test/helpers/normalize-text.ts", TEST_HELPER_NORMALIZE_TEXT_TARGETS],
|
|
[
|
|
"test/helpers/agents/happy-path-prompt-snapshots.ts",
|
|
HAPPY_PATH_PROMPT_SNAPSHOT_HELPER_TEST_TARGETS,
|
|
],
|
|
[
|
|
"test/e2e/qa-lab/runtime/qa-otel-smoke-runtime.ts",
|
|
["test/e2e/qa-lab/runtime/qa-otel-smoke.e2e.test.ts"],
|
|
],
|
|
[
|
|
"test/e2e/qa-lab/runtime/heartbeat-active-hours-runtime.ts",
|
|
["test/e2e/qa-lab/runtime/heartbeat-active-hours-runtime.test.ts"],
|
|
],
|
|
[
|
|
"test/e2e/qa-lab/runtime/telegram-bot-token-runtime.ts",
|
|
["test/e2e/qa-lab/runtime/telegram-bot-token-runtime.test.ts"],
|
|
],
|
|
["src/plugins/runtime-sidecar-paths-baseline.ts", RUNTIME_SIDECAR_BASELINE_OWNER_TEST_TARGETS],
|
|
["src/plugins/runtime-sidecar-paths.ts", RUNTIME_SIDECAR_PATH_CONSUMER_TEST_TARGETS],
|
|
["ui/config/control-ui-chunking.ts", ["ui/src/app/control-ui-chunking.test.ts"]],
|
|
["ui/config/control-ui-locales.ts", ["ui/src/app/vite-config.node.test.ts"]],
|
|
[
|
|
"src/plugin-sdk/test-helpers/directory-ids.ts",
|
|
[
|
|
"extensions/discord/src/directory-contract.test.ts",
|
|
"extensions/slack/src/directory-contract.test.ts",
|
|
"extensions/telegram/src/directory-contract.test.ts",
|
|
],
|
|
],
|
|
[
|
|
"src/plugin-sdk/channel-reply-pipeline.ts",
|
|
["src/plugins/contracts/plugin-sdk-subpaths.test.ts", ...GROUP_VISIBLE_REPLY_TEST_TARGETS],
|
|
],
|
|
["src/plugin-sdk/reply-runtime.ts", ["src/plugins/contracts/plugin-sdk-subpaths.test.ts"]],
|
|
["extensions/google-meet/index.ts", ["extensions/google-meet/index.test.ts"]],
|
|
[
|
|
"extensions/google-meet/src/cli.ts",
|
|
[
|
|
"extensions/google-meet/src/cli-artifacts.test.ts",
|
|
"extensions/google-meet/src/cli-runtime.test.ts",
|
|
"extensions/google-meet/src/cli.test.ts",
|
|
],
|
|
],
|
|
[
|
|
"extensions/google-meet/src/cli-artifact-commands.ts",
|
|
["extensions/google-meet/src/cli-artifacts.test.ts"],
|
|
],
|
|
[
|
|
"extensions/google-meet/src/cli-export.ts",
|
|
["extensions/google-meet/src/cli-artifacts.test.ts"],
|
|
],
|
|
[
|
|
"extensions/google-meet/src/cli-space-commands.ts",
|
|
["extensions/google-meet/src/cli-artifacts.test.ts"],
|
|
],
|
|
[
|
|
"extensions/google-meet/src/cli-runtime-commands.ts",
|
|
["extensions/google-meet/src/cli-runtime.test.ts"],
|
|
],
|
|
["extensions/google-meet/src/cli-doctor.ts", ["extensions/google-meet/src/cli.test.ts"]],
|
|
[
|
|
"extensions/google-meet/src/cli-command-context.ts",
|
|
[
|
|
"extensions/google-meet/src/cli-artifacts.test.ts",
|
|
"extensions/google-meet/src/cli-runtime.test.ts",
|
|
"extensions/google-meet/src/cli.test.ts",
|
|
],
|
|
],
|
|
[
|
|
"extensions/google-meet/src/cli-shared.ts",
|
|
[
|
|
"extensions/google-meet/src/cli-artifacts.test.ts",
|
|
"extensions/google-meet/src/cli-runtime.test.ts",
|
|
"extensions/google-meet/src/cli.test.ts",
|
|
],
|
|
],
|
|
["extensions/google-meet/src/create.ts", ["extensions/google-meet/index.test.ts"]],
|
|
["extensions/google-meet/src/oauth.ts", ["extensions/google-meet/src/oauth.test.ts"]],
|
|
[
|
|
"extensions/discord/src/monitor/message-handler.ts",
|
|
[
|
|
"extensions/discord/src/channel-actions.contract.test.ts",
|
|
"extensions/discord/src/channel.message-adapter.test.ts",
|
|
"extensions/discord/src/channel.test.ts",
|
|
"extensions/discord/src/durable-delivery.test.ts",
|
|
"extensions/discord/src/monitor/message-handler.bot-self-filter.test.ts",
|
|
"extensions/discord/src/monitor/message-handler.queue.test.ts",
|
|
"extensions/discord/src/monitor/provider.skill-dedupe.test.ts",
|
|
"extensions/discord/src/monitor/provider.test.ts",
|
|
],
|
|
],
|
|
["src/commands/doctor-memory-search.ts", ["src/commands/doctor-memory-search.test.ts"]],
|
|
[
|
|
"src/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"],
|
|
],
|
|
[
|
|
"packages/memory-host-sdk/src/host/embedding-defaults.ts",
|
|
["packages/memory-host-sdk/src/host/embeddings.test.ts"],
|
|
],
|
|
["src/auto-reply/reply/dispatch-from-config.ts", GROUP_VISIBLE_REPLY_TEST_TARGETS],
|
|
["src/auto-reply/reply/source-reply-delivery-mode.ts", GROUP_VISIBLE_REPLY_TEST_TARGETS],
|
|
[
|
|
"src/auto-reply/reply/effective-reply-route.ts",
|
|
[
|
|
"src/auto-reply/reply/effective-reply-route.test.ts",
|
|
"src/auto-reply/reply/dispatch-from-config.test.ts",
|
|
],
|
|
],
|
|
["src/auto-reply/reply/get-reply-run.ts", ["src/auto-reply/reply/followup-runner.test.ts"]],
|
|
["src/auto-reply/reply/groups.ts", GROUP_VISIBLE_REPLY_TEST_TARGETS],
|
|
["src/auto-reply/get-reply-options.types.ts", GROUP_VISIBLE_REPLY_TEST_TARGETS],
|
|
["src/agents/system-prompt.ts", GROUP_VISIBLE_REPLY_PROMPT_TEST_TARGETS],
|
|
["src/config/types.messages.ts", GROUP_VISIBLE_REPLY_TEST_TARGETS],
|
|
["src/config/zod-schema.core.ts", GROUP_VISIBLE_REPLY_TEST_TARGETS],
|
|
["src/auto-reply/reply/commands-acp.ts", ["src/auto-reply/reply/commands-acp.test.ts"]],
|
|
[
|
|
"src/auto-reply/reply/dispatch-acp-command-bypass.ts",
|
|
["src/auto-reply/reply/dispatch-acp-command-bypass.test.ts"],
|
|
],
|
|
]);
|
|
const GENERATED_CHANGED_TEST_TARGET_PATTERNS = [
|
|
/^extensions\/[^/]+\/src\/host\/.+\/\.bundle\.hash$/u,
|
|
/^extensions\/[^/]+\/src\/host\/.+\/[^/]+\.bundle\.js$/u,
|
|
];
|
|
const SOURCE_ROOTS_FOR_IMPORT_GRAPH = [
|
|
"src",
|
|
"extensions",
|
|
"packages",
|
|
"ui/src",
|
|
"ui/config",
|
|
"test",
|
|
];
|
|
const IMPORTABLE_FILE_EXTENSIONS = [".ts", ".tsx", ".mts", ".cts"];
|
|
const IMPORT_GRAPH_GREP_PATHS = SOURCE_ROOTS_FOR_IMPORT_GRAPH.flatMap((root) =>
|
|
IMPORTABLE_FILE_EXTENSIONS.map((ext) => `:(glob)${root}/**/*${ext}`),
|
|
);
|
|
const TOOLING_IMPORT_GRAPH_ROOTS = [...SOURCE_ROOTS_FOR_IMPORT_GRAPH, "scripts"];
|
|
const TOOLING_IMPORTABLE_FILE_EXTENSIONS = [
|
|
...IMPORTABLE_FILE_EXTENSIONS,
|
|
".js",
|
|
".jsx",
|
|
".mjs",
|
|
".cjs",
|
|
];
|
|
const TOOLING_IMPORT_GRAPH_GREP_PATHS = TOOLING_IMPORT_GRAPH_ROOTS.flatMap((root) =>
|
|
TOOLING_IMPORTABLE_FILE_EXTENSIONS.map((ext) => `:(glob)${root}/**/*${ext}`),
|
|
);
|
|
const IMPORT_SPECIFIER_PATTERN =
|
|
/\b(?:import|export)\s+(?:type\s+)?(?:[^'"]*?\s+from\s+)?["']([^"']+)["']|\bimport\s*\(\s*["']([^"']+)["']\s*\)/gu;
|
|
const REEXPORT_SPECIFIER_PATTERN =
|
|
/\bexport\s+(?:type\s+)?(?:\*\s+(?:as\s+\w+\s+)?from\s+|[^"']+?\s+from\s+)["']([^"']+)["']/gu;
|
|
const BROAD_CHANGED_ENV_KEY = "OPENCLAW_TEST_CHANGED_BROAD";
|
|
const VITEST_NO_OUTPUT_TIMEOUT_ENV_KEY = "OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS";
|
|
const VITEST_NO_OUTPUT_HEARTBEAT_ENV_KEY = "OPENCLAW_VITEST_NO_OUTPUT_HEARTBEAT_MS";
|
|
const VITEST_NO_OUTPUT_RETRY_ENV_KEY = "OPENCLAW_VITEST_NO_OUTPUT_RETRY";
|
|
/** Default no-output timeout applied to test-projects Vitest children. */
|
|
export const DEFAULT_TEST_PROJECTS_VITEST_NO_OUTPUT_TIMEOUT_MS = String(900_000);
|
|
/** Default heartbeat interval applied to test-projects Vitest children. */
|
|
export const DEFAULT_TEST_PROJECTS_VITEST_NO_OUTPUT_HEARTBEAT_MS = String(
|
|
DEFAULT_VITEST_NO_OUTPUT_HEARTBEAT_MS,
|
|
);
|
|
|
|
export function formatNoChangedTestTargetLines(skippedBroadFallbackPaths) {
|
|
if (skippedBroadFallbackPaths.length === 0) {
|
|
return ["[test] no changed test targets; skipping Vitest."];
|
|
}
|
|
|
|
return [
|
|
"[test] no precise changed test targets; skipping Vitest.",
|
|
`[test] ${skippedBroadFallbackPaths.length} changed path${
|
|
skippedBroadFallbackPaths.length === 1 ? "" : "s"
|
|
} require broad Vitest fallback:`,
|
|
...skippedBroadFallbackPaths.map((changedPath) => `[test] ${changedPath}`),
|
|
"[test] run `OPENCLAW_TEST_CHANGED_BROAD=1 pnpm test:changed` for broad coverage.",
|
|
];
|
|
}
|
|
|
|
const EXPLICIT_SOURCE_FULL_IMPORT_GRAPH_THRESHOLD = 12;
|
|
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]),
|
|
]);
|
|
export const CHANNEL_CONTRACT_CONFIG_PATTERNS = new Map([
|
|
[CONTRACTS_CHANNEL_SURFACE_VITEST_CONFIG, channelSurfaceContractPatterns],
|
|
[CONTRACTS_CHANNEL_CONFIG_VITEST_CONFIG, channelConfigContractPatterns],
|
|
[CONTRACTS_CHANNEL_REGISTRY_VITEST_CONFIG, channelRegistryContractPatterns],
|
|
[CONTRACTS_CHANNEL_SESSION_VITEST_CONFIG, channelSessionContractPatterns],
|
|
]);
|
|
|
|
function normalizePathPattern(value) {
|
|
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;
|
|
}
|
|
|
|
let cachedBroadScriptTestTargets = null;
|
|
let cachedBroadScriptTestTargetsCwd = null;
|
|
|
|
function listBroadScriptTestTargets(pattern, cwd) {
|
|
const root = path.join(cwd, "test/scripts");
|
|
if (cachedBroadScriptTestTargetsCwd !== cwd) {
|
|
// Broad-target expansion can ask for the same process-stable checkout twice.
|
|
// Keep one inventory so planning does not repeat the directory walk.
|
|
cachedBroadScriptTestTargets = fs.existsSync(root)
|
|
? listRepoFilesRecursive(root, cwd)
|
|
.filter((file) => file.endsWith(".test.ts"))
|
|
.toSorted((left, right) => left.localeCompare(right))
|
|
: [];
|
|
cachedBroadScriptTestTargetsCwd = cwd;
|
|
}
|
|
return cachedBroadScriptTestTargets.filter((file) => path.matchesGlob(file, pattern));
|
|
}
|
|
|
|
function listBroadToolingScriptTestTargets(pattern, cwd) {
|
|
return listBroadScriptTestTargets(pattern, cwd).filter(
|
|
(file) => classifyTarget(file, cwd) === "tooling",
|
|
);
|
|
}
|
|
|
|
let cachedToolingFullSuiteTestTargets = null;
|
|
let cachedToolingFullSuiteTestTargetsCwd = null;
|
|
|
|
function listToolingFullSuiteTestTargets(cwd) {
|
|
if (cachedToolingFullSuiteTestTargets && cachedToolingFullSuiteTestTargetsCwd === cwd) {
|
|
return cachedToolingFullSuiteTestTargets;
|
|
}
|
|
// The CLI plans against one process-stable checkout. Reuse its inventory when
|
|
// callers compare full-suite modes instead of walking the tree for every mode.
|
|
cachedToolingFullSuiteTestTargets = uniqueOrdered(
|
|
[path.join(cwd, "test"), path.join(cwd, "src", "scripts")].flatMap((root) =>
|
|
fs.existsSync(root) ? listRepoFilesRecursive(root, cwd) : [],
|
|
),
|
|
)
|
|
// Explicit leaf targets bypass the config's live-test exclusion and produce an empty shard.
|
|
.filter(
|
|
(file) =>
|
|
file.endsWith(".test.ts") &&
|
|
!file.endsWith(".live.test.ts") &&
|
|
classifyTarget(file, cwd) === "tooling",
|
|
)
|
|
.toSorted((left, right) => left.localeCompare(right));
|
|
cachedToolingFullSuiteTestTargetsCwd = cwd;
|
|
return cachedToolingFullSuiteTestTargets;
|
|
}
|
|
|
|
function listUnitFastFullSuiteTestTargets() {
|
|
const timerTargets = new Set(getUnitFastTimerTestFiles());
|
|
const isolatedTargets = new Set(getUnitFastIsolatedTestFiles());
|
|
return getUnitFastTestFiles().filter(
|
|
(file) => !timerTargets.has(file) && !isolatedTargets.has(file),
|
|
);
|
|
}
|
|
|
|
function listAgentsCoreFullSuiteTestTargets(cwd) {
|
|
const isolatedTests = new Set(agentVitestProjectOwners.coreIsolated.include);
|
|
const agentsDir = path.join(cwd, "src/agents");
|
|
if (!fs.existsSync(agentsDir)) {
|
|
return [];
|
|
}
|
|
return fs
|
|
.readdirSync(agentsDir, { withFileTypes: true })
|
|
.filter((entry) => entry.isFile() && entry.name.endsWith(".test.ts"))
|
|
.map((entry) => `src/agents/${entry.name}`)
|
|
.filter((file) => !isolatedTests.has(file))
|
|
.toSorted((left, right) => left.localeCompare(right));
|
|
}
|
|
|
|
function createBroadToolingScriptPlans({ config, forwardedArgs, includePatterns, watchMode, cwd }) {
|
|
if (watchMode || config !== TOOLING_VITEST_CONFIG || !includePatterns) {
|
|
return null;
|
|
}
|
|
const [pattern] = includePatterns;
|
|
const targets =
|
|
includePatterns.length === 1 && BROAD_TOOLING_SCRIPT_TEST_PATTERNS.has(pattern)
|
|
? listBroadToolingScriptTestTargets(pattern, cwd)
|
|
: includePatterns.every((target) => target.startsWith("test/scripts/"))
|
|
? includePatterns
|
|
: [];
|
|
if (targets.length <= BROAD_TOOLING_SCRIPT_TEST_TARGET_CHUNK_SIZE) {
|
|
return null;
|
|
}
|
|
const chunkCount = Math.ceil(targets.length / BROAD_TOOLING_SCRIPT_TEST_TARGET_CHUNK_SIZE);
|
|
const chunks = splitTargetChunks(targets, chunkCount);
|
|
return chunks.length > 0
|
|
? chunks.map((chunk) => ({
|
|
config,
|
|
forwardedArgs,
|
|
includePatterns: chunk,
|
|
watchMode,
|
|
}))
|
|
: null;
|
|
}
|
|
|
|
function createBoundedExtensionPlans({ config, forwardedArgs, roots, watchMode }) {
|
|
if (watchMode) {
|
|
return null;
|
|
}
|
|
const chunks = createExtensionTestProcessTargetChunks(config, roots, forwardedArgs);
|
|
if (chunks.length <= 1) {
|
|
return null;
|
|
}
|
|
return chunks.map((includePatterns) => ({
|
|
config,
|
|
forwardedArgs,
|
|
includePatterns,
|
|
watchMode,
|
|
}));
|
|
}
|
|
|
|
function expandBroadToolingScriptTargets(targetArgs, cwd, watchMode) {
|
|
if (watchMode) {
|
|
return targetArgs;
|
|
}
|
|
return uniqueOrdered(
|
|
targetArgs.flatMap((targetArg) => {
|
|
const pattern = toScopedIncludePattern(targetArg, cwd);
|
|
if (!BROAD_TOOLING_SCRIPT_TEST_PATTERNS.has(pattern)) {
|
|
return [targetArg];
|
|
}
|
|
const targets = listBroadScriptTestTargets(pattern, cwd);
|
|
return targets.length > 0 ? targets : [targetArg];
|
|
}),
|
|
);
|
|
}
|
|
|
|
function isExistingPathTarget(arg, 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);
|
|
}
|
|
|
|
export 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) ||
|
|
(path.posix.extname(relative) === "" &&
|
|
/^(?:src|test|extensions|ui|packages|apps)\//u.test(relative)) ||
|
|
Boolean(resolveExplicitTestPrefixTargets(arg, cwd)?.length)
|
|
);
|
|
}
|
|
|
|
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 resolveExplicitTestPrefixTargets(targetArg, cwd) {
|
|
if (isExistingPathTarget(targetArg, cwd) || isGlobTarget(targetArg)) {
|
|
return null;
|
|
}
|
|
const relative = toRepoRelativeTarget(targetArg, cwd).replace(/\/+$/u, "");
|
|
if (!relative || isLikelyFileTarget(relative)) {
|
|
return null;
|
|
}
|
|
const directory = path.posix.dirname(relative);
|
|
const prefix = `${relative}.`;
|
|
const targets = listExplicitTestTargetFilesForCwd(cwd).filter(
|
|
(file) =>
|
|
fs.existsSync(path.join(cwd, file)) &&
|
|
path.posix.dirname(file) === directory &&
|
|
file.startsWith(prefix) &&
|
|
isTestFileTarget(file),
|
|
);
|
|
return targets.length > 0 ? targets.toSorted((left, right) => left.localeCompare(right)) : null;
|
|
}
|
|
|
|
function includePatternMatchesAnyFile(pattern, 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);
|
|
const prefixTargets = resolveExplicitTestPrefixTargets(targetArg, cwd);
|
|
if (prefixTargets) {
|
|
return prefixTargets;
|
|
}
|
|
if (relative === "src/commands" && isExistingDirectoryTarget(targetArg, cwd)) {
|
|
return [COMMANDS_LIGHT_VITEST_CONFIG, COMMANDS_VITEST_CONFIG];
|
|
}
|
|
// Contract directory targets must fan out to the owning contract lanes; the
|
|
// generic channels/plugins projects exclude contracts/**, so routing a
|
|
// contracts directory there silently runs zero tests (passWithNoTests).
|
|
if (isExistingDirectoryTarget(targetArg, cwd)) {
|
|
if (isPathAtOrUnder(relative, "src/channels/plugins/contracts")) {
|
|
return [
|
|
CONTRACTS_CHANNEL_SURFACE_VITEST_CONFIG,
|
|
CONTRACTS_CHANNEL_CONFIG_VITEST_CONFIG,
|
|
CONTRACTS_CHANNEL_REGISTRY_VITEST_CONFIG,
|
|
CONTRACTS_CHANNEL_SESSION_VITEST_CONFIG,
|
|
];
|
|
}
|
|
if (isPathAtOrUnder(relative, "src/plugins/contracts")) {
|
|
return [CONTRACTS_PLUGIN_VITEST_CONFIG];
|
|
}
|
|
}
|
|
const exactDirectoryTargets = resolveExactSourceDirectoryTestTargets(targetArg, cwd);
|
|
if (exactDirectoryTargets) {
|
|
return exactDirectoryTargets;
|
|
}
|
|
const targets = resolveExplicitSourceTestTargets(targetArg, cwd, {
|
|
forceFullImportGraph,
|
|
});
|
|
return targets && targets.length > 0 ? targets : [targetArg];
|
|
});
|
|
}
|
|
|
|
const exactSourceDirectoryRoots = [
|
|
"src/acp",
|
|
"src/agents",
|
|
"src/auto-reply",
|
|
"src/channels",
|
|
"src/cli",
|
|
"src/commands",
|
|
"src/config",
|
|
"src/cron",
|
|
"src/daemon",
|
|
"src/gateway",
|
|
"src/hooks",
|
|
"src/infra",
|
|
"src/logging",
|
|
"src/media",
|
|
"src/media-understanding",
|
|
"src/plugin-sdk",
|
|
"src/plugins",
|
|
"src/process",
|
|
"src/secrets",
|
|
"src/shared",
|
|
"src/tasks",
|
|
"src/tui",
|
|
"src/utils",
|
|
"src/wizard",
|
|
"ui/src",
|
|
];
|
|
|
|
function isExactSourceDirectoryTarget(relative) {
|
|
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)) {
|
|
if (resolveExplicitTestPrefixTargets(targetArg, cwd)) {
|
|
continue;
|
|
}
|
|
unmatched.push({
|
|
target: targetArg,
|
|
reason: "path-does-not-exist",
|
|
...(path.posix.extname(relative) === ""
|
|
? { includePattern: `${relative}{,.*}.{test,spec}.{js,jsx,ts,tsx,mjs,cjs,mts,cts}` }
|
|
: {}),
|
|
});
|
|
continue;
|
|
}
|
|
|
|
if (isTestFileTarget(relative)) {
|
|
continue;
|
|
}
|
|
|
|
const explicitSupportTargets = resolveExplicitSourceTestTargets(targetArg, cwd);
|
|
if (explicitSupportTargets) {
|
|
if (explicitSupportTargets.length === 0) {
|
|
unmatched.push({
|
|
target: targetArg,
|
|
reason: "target-matched-no-test-files",
|
|
});
|
|
}
|
|
continue;
|
|
}
|
|
|
|
const includePattern = toScopedIncludePattern(targetArg, cwd);
|
|
if (!includePatternMatchesAnyFile(includePattern, getCandidateFiles())) {
|
|
unmatched.push({
|
|
target: targetArg,
|
|
reason: "target-matched-no-test-files",
|
|
includePattern,
|
|
});
|
|
}
|
|
}
|
|
return unmatched;
|
|
}
|
|
|
|
function isSkippedImportGraphDirectory(name) {
|
|
return name === ".git" || name === "dist" || name === "node_modules" || name === "vendor";
|
|
}
|
|
|
|
function listImportGraphFiles(cwd, directory, files = [], extensions = IMPORTABLE_FILE_EXTENSIONS) {
|
|
let entries;
|
|
try {
|
|
entries = fs.readdirSync(path.join(cwd, directory), { withFileTypes: true });
|
|
} catch {
|
|
return files;
|
|
}
|
|
|
|
for (const entry of entries) {
|
|
const relative = normalizePathPattern(path.posix.join(directory, entry.name));
|
|
if (entry.isDirectory()) {
|
|
if (!isSkippedImportGraphDirectory(entry.name)) {
|
|
listImportGraphFiles(cwd, relative, files, extensions);
|
|
}
|
|
continue;
|
|
}
|
|
if (entry.isFile() && extensions.some((ext) => relative.endsWith(ext))) {
|
|
files.push(relative);
|
|
}
|
|
}
|
|
return files;
|
|
}
|
|
|
|
function resolveImportSpecifier(
|
|
importer,
|
|
specifier,
|
|
fileSet,
|
|
extensions = IMPORTABLE_FILE_EXTENSIONS,
|
|
) {
|
|
if (!specifier.startsWith(".")) {
|
|
return null;
|
|
}
|
|
|
|
const importerDir = path.posix.dirname(importer);
|
|
const base = normalizePathPattern(path.posix.normalize(path.posix.join(importerDir, specifier)));
|
|
const candidates = [];
|
|
const ext = path.posix.extname(base);
|
|
if (ext) {
|
|
candidates.push(base);
|
|
if ([".js", ".jsx", ".mjs", ".cjs"].includes(ext)) {
|
|
const withoutExt = base.slice(0, -ext.length);
|
|
candidates.push(...extensions.map((candidateExt) => `${withoutExt}${candidateExt}`));
|
|
}
|
|
} else {
|
|
candidates.push(
|
|
...extensions.map((candidateExt) => `${base}${candidateExt}`),
|
|
...extensions.map((candidateExt) => `${base}/index${candidateExt}`),
|
|
);
|
|
}
|
|
|
|
return candidates.find((candidate) => fileSet.has(candidate)) ?? null;
|
|
}
|
|
|
|
let cachedImportGraph = null;
|
|
let cachedImportGraphCwd = null;
|
|
const cachedImportGraphFiles = new Map();
|
|
const cachedImportGraphGrepMatches = new Map();
|
|
const cachedDirectImporters = new Map();
|
|
|
|
function isImportableGraphFile(relative) {
|
|
return IMPORTABLE_FILE_EXTENSIONS.some((ext) => relative.endsWith(ext));
|
|
}
|
|
|
|
function listImportGraphFilesFromGit(cwd, roots, extensions) {
|
|
const result = spawnSync("git", ["ls-files", "--", ...roots], {
|
|
cwd,
|
|
encoding: "utf8",
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
});
|
|
if (result.status !== 0) {
|
|
return null;
|
|
}
|
|
return result.stdout
|
|
.split("\n")
|
|
.map((line) => normalizePathPattern(line.trim()))
|
|
.filter((line) => line.length > 0 && extensions.some((ext) => line.endsWith(ext)));
|
|
}
|
|
|
|
function listImportGraphFilesForCwd(cwd, options = {}) {
|
|
const tooling = options.tooling === true;
|
|
const cacheKey = `${cwd}\0${tooling ? "tooling" : "source"}`;
|
|
if (cachedImportGraphFiles.has(cacheKey)) {
|
|
return cachedImportGraphFiles.get(cacheKey);
|
|
}
|
|
const roots = tooling ? TOOLING_IMPORT_GRAPH_ROOTS : SOURCE_ROOTS_FOR_IMPORT_GRAPH;
|
|
const extensions = tooling ? TOOLING_IMPORTABLE_FILE_EXTENSIONS : IMPORTABLE_FILE_EXTENSIONS;
|
|
const files =
|
|
listImportGraphFilesFromGit(cwd, roots, extensions) ??
|
|
roots.flatMap((root) => listImportGraphFiles(cwd, root, [], extensions));
|
|
cachedImportGraphFiles.set(cacheKey, files);
|
|
return files;
|
|
}
|
|
|
|
function stripImportableGraphExtension(relative, extensions = IMPORTABLE_FILE_EXTENSIONS) {
|
|
for (const ext of extensions) {
|
|
if (relative.endsWith(ext)) {
|
|
return relative.slice(0, -ext.length);
|
|
}
|
|
}
|
|
return relative;
|
|
}
|
|
|
|
function resolveImportGraphSearchTerms(relative, extensions = IMPORTABLE_FILE_EXTENSIONS) {
|
|
const withoutExtension = stripImportableGraphExtension(relative, extensions);
|
|
const basename = path.posix.basename(stripImportableGraphExtension(relative, extensions));
|
|
if (basename === "index" || basename.length < 3) {
|
|
return [];
|
|
}
|
|
const terms = [];
|
|
const segments = withoutExtension.split("/");
|
|
if (segments.length > 1) {
|
|
terms.push(segments.slice(-2).join("/"), withoutExtension);
|
|
}
|
|
if (relative.startsWith("test/helpers/")) {
|
|
return [...new Set(terms)];
|
|
}
|
|
terms.push(basename);
|
|
return [...new Set(terms)];
|
|
}
|
|
|
|
function listImportGraphGrepMatches(cwd, term, options = {}) {
|
|
const tooling = options.tooling === true;
|
|
const cacheKey = `${cwd}\0${tooling ? "tooling" : "source"}\0${term}`;
|
|
if (cachedImportGraphGrepMatches.has(cacheKey)) {
|
|
return cachedImportGraphGrepMatches.get(cacheKey);
|
|
}
|
|
|
|
const result = spawnSync(
|
|
"git",
|
|
[
|
|
"grep",
|
|
"-l",
|
|
"--fixed-strings",
|
|
term,
|
|
"--",
|
|
...(tooling ? TOOLING_IMPORT_GRAPH_GREP_PATHS : IMPORT_GRAPH_GREP_PATHS),
|
|
],
|
|
{
|
|
cwd,
|
|
encoding: "utf8",
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
},
|
|
);
|
|
if (result.status === 1) {
|
|
cachedImportGraphGrepMatches.set(cacheKey, []);
|
|
return [];
|
|
}
|
|
if (result.status !== 0) {
|
|
cachedImportGraphGrepMatches.set(cacheKey, null);
|
|
return null;
|
|
}
|
|
const matches = result.stdout
|
|
.split("\n")
|
|
.map((line) => normalizePathPattern(line.trim()))
|
|
.filter(
|
|
(line) =>
|
|
line.length > 0 &&
|
|
(tooling
|
|
? TOOLING_IMPORTABLE_FILE_EXTENSIONS.some((ext) => line.endsWith(ext))
|
|
: isImportableGraphFile(line)),
|
|
);
|
|
cachedImportGraphGrepMatches.set(cacheKey, matches);
|
|
return matches;
|
|
}
|
|
|
|
function findDirectImportersWithGitGrep(cwd, importedFile, fileSet, options = {}) {
|
|
const tooling = options.tooling === true;
|
|
const cacheKey = `${cwd}\0${tooling ? "tooling" : "source"}\0${importedFile}`;
|
|
if (cachedDirectImporters.has(cacheKey)) {
|
|
return cachedDirectImporters.get(cacheKey);
|
|
}
|
|
|
|
const extensions = tooling ? TOOLING_IMPORTABLE_FILE_EXTENSIONS : IMPORTABLE_FILE_EXTENSIONS;
|
|
const terms = resolveImportGraphSearchTerms(importedFile, extensions);
|
|
if (terms.length === 0) {
|
|
cachedDirectImporters.set(cacheKey, null);
|
|
return null;
|
|
}
|
|
|
|
let skippedBroadTerm = false;
|
|
const importers = [];
|
|
for (const term of terms) {
|
|
const candidates = listImportGraphGrepMatches(cwd, term, { tooling });
|
|
if (!candidates) {
|
|
cachedDirectImporters.set(cacheKey, null);
|
|
return null;
|
|
}
|
|
if (candidates.length > 800) {
|
|
skippedBroadTerm = true;
|
|
continue;
|
|
}
|
|
for (const file of candidates) {
|
|
if (file === importedFile || !fileSet.has(file) || importers.includes(file)) {
|
|
continue;
|
|
}
|
|
let source;
|
|
try {
|
|
source = fs.readFileSync(path.join(cwd, file), "utf8");
|
|
} catch {
|
|
continue;
|
|
}
|
|
for (const match of source.matchAll(IMPORT_SPECIFIER_PATTERN)) {
|
|
const imported = resolveImportSpecifier(
|
|
file,
|
|
match[1] ?? match[2] ?? "",
|
|
fileSet,
|
|
extensions,
|
|
);
|
|
if (imported === importedFile) {
|
|
importers.push(file);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (importedFile.startsWith("test/helpers/") && importers.length > 0 && term.includes("/")) {
|
|
break;
|
|
}
|
|
}
|
|
const result =
|
|
skippedBroadTerm && importers.length === 0 && !importedFile.startsWith("test/helpers/")
|
|
? null
|
|
: importers;
|
|
cachedDirectImporters.set(cacheKey, result);
|
|
return result;
|
|
}
|
|
|
|
function resolveAffectedTestsFromTargetedImportScan(changedPath, cwd, options = {}) {
|
|
const normalized = normalizePathPattern(changedPath);
|
|
const tooling = options.tooling === true;
|
|
const files = listImportGraphFilesForCwd(cwd, { tooling });
|
|
const fileSet = new Set(files);
|
|
if (!fileSet.has(normalized)) {
|
|
return [];
|
|
}
|
|
|
|
const testFiles = new Set(
|
|
files.filter((file) => isTestFileTarget(file) && !file.endsWith(".live.test.ts")),
|
|
);
|
|
const queue = [normalized];
|
|
const seen = new Set(queue);
|
|
const targets = [];
|
|
|
|
for (const current of queue) {
|
|
const importers = findDirectImportersWithGitGrep(cwd, current, fileSet, { tooling });
|
|
if (importers === null) {
|
|
return null;
|
|
}
|
|
for (const importer of importers) {
|
|
if (seen.has(importer)) {
|
|
continue;
|
|
}
|
|
seen.add(importer);
|
|
if (testFiles.has(importer)) {
|
|
targets.push(importer);
|
|
continue;
|
|
}
|
|
if (options.direct !== true) {
|
|
queue.push(importer);
|
|
}
|
|
}
|
|
}
|
|
|
|
return [...new Set(targets)].toSorted((left, right) => left.localeCompare(right));
|
|
}
|
|
|
|
function getImportGraph(cwd) {
|
|
if (cachedImportGraph && cachedImportGraphCwd === cwd) {
|
|
return cachedImportGraph;
|
|
}
|
|
|
|
const files = listImportGraphFilesForCwd(cwd);
|
|
const fileSet = new Set(files);
|
|
const reverseImports = new Map();
|
|
const reverseReexports = 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);
|
|
}
|
|
for (const match of source.matchAll(REEXPORT_SPECIFIER_PATTERN)) {
|
|
const imported = resolveImportSpecifier(file, match[1] ?? "", fileSet);
|
|
if (!imported) {
|
|
continue;
|
|
}
|
|
const importers = reverseReexports.get(imported) ?? [];
|
|
importers.push(file);
|
|
reverseReexports.set(imported, importers);
|
|
}
|
|
}
|
|
|
|
cachedImportGraph = { reverseImports, reverseReexports, testFiles };
|
|
cachedImportGraphCwd = cwd;
|
|
return cachedImportGraph;
|
|
}
|
|
|
|
/** Returns whether any changed path reaches one of the requested import-graph targets. */
|
|
export function hasImportGraphImpactOnTargets(changedPaths, targetPaths, cwd = process.cwd()) {
|
|
const targets = new Set(targetPaths.map(normalizePathPattern));
|
|
if (targets.size === 0) {
|
|
return false;
|
|
}
|
|
|
|
const { reverseImports, reverseReexports } = getImportGraph(cwd);
|
|
for (const changedPath of changedPaths) {
|
|
const queue = [normalizePathPattern(changedPath)];
|
|
const seen = new Set(queue);
|
|
for (const current of queue) {
|
|
if (targets.has(current)) {
|
|
return true;
|
|
}
|
|
const importers = [
|
|
...(reverseImports.get(current) ?? []),
|
|
...(reverseReexports.get(current) ?? []),
|
|
];
|
|
for (const importer of importers) {
|
|
if (!seen.has(importer)) {
|
|
seen.add(importer);
|
|
queue.push(importer);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function resolveAffectedTestsFromImportGraph(changedPath, 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/e2e" ||
|
|
relative.startsWith("ui/src/e2e/") ||
|
|
(relative.startsWith("ui/src/") && relative.endsWith(".e2e.test.ts"))
|
|
);
|
|
}
|
|
|
|
function resolveChannelContractTargetKind(relative) {
|
|
if (!relative.startsWith("src/channels/plugins/contracts/")) {
|
|
return null;
|
|
}
|
|
for (const [config, patterns] of CHANNEL_CONTRACT_CONFIG_PATTERNS) {
|
|
if (patterns.some((pattern) => path.matchesGlob(relative, pattern))) {
|
|
return VITEST_CONFIG_TARGET_KIND_BY_PATH.get(config);
|
|
}
|
|
}
|
|
return "contractsChannelSession";
|
|
}
|
|
|
|
function listChangedPathsFromGit(baseRef, 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 =
|
|
SOURCE_TEST_TARGETS.get(changedPath) ?? resolveToolingTestTargets(changedPath, cwd);
|
|
if (!testTargets) {
|
|
return null;
|
|
}
|
|
targets.push(...testTargets);
|
|
}
|
|
return [...new Set(targets)];
|
|
}
|
|
|
|
const TOOLING_SCRIPT_PATH_PATTERN = /^scripts\/(.+)\.(?:mjs|cjs|js|mts|cts|ts|sh|py|ps1)$/u;
|
|
|
|
function resolveConventionalToolingTestTargets(changedPath, 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 resolveUpgradeSurvivorConfigRecipeTargets(changedPath) {
|
|
if (!/^scripts\/e2e\/lib\/upgrade-survivor\/config-recipe\/[^/]+\.json$/u.test(changedPath)) {
|
|
return null;
|
|
}
|
|
return ["test/scripts/upgrade-survivor-config-recipe.test.ts"];
|
|
}
|
|
|
|
function resolveDocsI18nBehaviorTargets(changedPath) {
|
|
if (!/^scripts\/docs-i18n\/testdata\/behavior\/[^/]+\/[^/]+$/u.test(changedPath)) {
|
|
return null;
|
|
}
|
|
return ["test/scripts/docs-i18n.test.ts"];
|
|
}
|
|
|
|
function resolveDocsI18nGoTargets(changedPath) {
|
|
if (!/^scripts\/docs-i18n\/(?:go\.(?:mod|sum)|[^/]+\.go)$/u.test(changedPath)) {
|
|
return null;
|
|
}
|
|
const targets = ["test/scripts/docs-i18n.test.ts"];
|
|
if (changedPath === "scripts/docs-i18n/go.mod") {
|
|
targets.push("test/scripts/ci-workflow-guards.test.ts");
|
|
}
|
|
return targets;
|
|
}
|
|
|
|
function resolveK8sManifestTargets(changedPath) {
|
|
if (!/^scripts\/k8s\/manifests\/[^/]+\.yaml$/u.test(changedPath)) {
|
|
return null;
|
|
}
|
|
return ["test/scripts/k8s-manifests.test.ts"];
|
|
}
|
|
|
|
function resolveParallelsToolingTestTargets(changedPath) {
|
|
if (
|
|
!/^scripts\/e2e\/parallels\/[^/]+\.ts$/u.test(changedPath) &&
|
|
!/^scripts\/e2e\/parallels-(?:linux|macos|npm-update|windows)-smoke\.sh$/u.test(changedPath)
|
|
) {
|
|
return null;
|
|
}
|
|
const targets = ["test/scripts/parallels-smoke-model.test.ts"];
|
|
if (
|
|
[
|
|
"scripts/e2e/parallels/guest-transports.ts",
|
|
"scripts/e2e/parallels/host-command.ts",
|
|
"scripts/e2e/parallels/npm-update-scripts.ts",
|
|
"scripts/e2e/parallels/npm-update-smoke.ts",
|
|
].includes(changedPath)
|
|
) {
|
|
targets.push("test/scripts/parallels-npm-update-smoke.test.ts");
|
|
}
|
|
if (changedPath === "scripts/e2e/parallels/update-job-timeout.ts") {
|
|
targets.push("test/scripts/parallels-update-job-timeout.test.ts");
|
|
}
|
|
return targets;
|
|
}
|
|
|
|
function resolveToolingTestOwnerTargets(...owners) {
|
|
return owners.map((owner) => (owner.includes("/") ? owner : `test/scripts/${owner}.test.ts`));
|
|
}
|
|
|
|
const packageAcceptance = "package-acceptance-workflow";
|
|
const dockerBuild = "docker-build-helper";
|
|
const dockerE2e = "docker-e2e-plan";
|
|
const workflowGuards = "ci-workflow-guards";
|
|
const pluginPrerelease = "plugin-prerelease-test-plan";
|
|
const releaseCheck = "test/release-check.test.ts";
|
|
const installDocker = "test-install-sh-docker";
|
|
const changedScope = "src/scripts/ci-changed-scope.test.ts";
|
|
const dockerCache = "src/docker-build-cache.test.ts";
|
|
const dockerDigests = "src/docker-image-digests.test.ts";
|
|
const openaiChatToolsE2e = "test/e2e/qa-lab/runtime/openai-compatible-chat-tools.e2e.test.ts";
|
|
const npmPostpublish = "test/openclaw-npm-postpublish-verify.test.ts";
|
|
const crossOsReleaseChecks = "openclaw-cross-os-release-checks";
|
|
const runNode = "src/infra/run-node.test.ts";
|
|
const pluginSdkEntryOwners = [
|
|
"src/plugins/contracts/plugin-sdk-index.bundle.test.ts",
|
|
"src/plugins/contracts/plugin-sdk-package-contract-guardrails.test.ts",
|
|
"src/plugins/contracts/plugin-sdk-subpaths.test.ts",
|
|
"src/plugins/contracts/extension-package-project-boundaries.test.ts",
|
|
"plugin-sdk-surface-report",
|
|
"build-all",
|
|
releaseCheck,
|
|
"prepare-extension-package-boundary-artifacts",
|
|
"ts-topology",
|
|
"test/vitest/vitest.tooling.config.ts",
|
|
];
|
|
|
|
// Keep only genuinely ambiguous paths explicit; conventional discovery owns
|
|
// unambiguous scripts and direct imports without a second inventory.
|
|
const EXACT_TOOLING_TARGETS = new Map([
|
|
[
|
|
".github/workflows/mantis-telegram-live.yml",
|
|
["mantis-telegram-desktop-proof-workflow", packageAcceptance, workflowGuards],
|
|
],
|
|
[
|
|
".github/workflows/openclaw-live-and-e2e-checks-reusable.yml",
|
|
[packageAcceptance, workflowGuards, "release-workflow-matrix-plan", installDocker],
|
|
],
|
|
[
|
|
".github/workflows/plugin-npm-release.yml",
|
|
[packageAcceptance, "plugin-npm-extended-stable-workflow", workflowGuards],
|
|
],
|
|
[".github/workflows/qa-live-transports-convex.yml", [packageAcceptance, workflowGuards]],
|
|
[".github/workflows/update-migration.yml", [packageAcceptance, workflowGuards]],
|
|
[
|
|
".github/actions/setup-node-env/action.yml",
|
|
["install-trufflehog", packageAcceptance, workflowGuards],
|
|
],
|
|
[".github/actions/setup-node-env/dependency-fingerprint.mjs", [workflowGuards]],
|
|
[".github/actions/setup-pnpm-store-cache/action.yml", [packageAcceptance, workflowGuards]],
|
|
[".github/actions/setup-pnpm-store-cache/ensure-node.sh", ["setup-pnpm-store-cache-ensure-node"]],
|
|
["test/e2e/qa-lab/runtime/mcp-channels-docker-client.ts", [dockerE2e, pluginPrerelease]],
|
|
[
|
|
"scripts/e2e/lib/mcp-code-mode-probe-server.ts",
|
|
["docker-e2e-seeds", "mcp-code-mode-gateway-client"],
|
|
],
|
|
["scripts/e2e/cron-cli-docker.sh", [dockerBuild, "docker-e2e-observability"]],
|
|
["scripts/ios-release-upload.sh", ["ios-release-wrapper-args", "ios-release-fastlane-gates"]],
|
|
["scripts/release-verify-beta.ts", ["release-wrapper-scripts"]],
|
|
["scripts/lib/bundled-plugin-build-entries.mjs", ["bundled-plugin-build-entries", releaseCheck]],
|
|
["scripts/lib/docker-e2e-package.sh", [dockerBuild]],
|
|
[
|
|
"scripts/lib/release-version.mjs",
|
|
[
|
|
"test/release-version.test.ts",
|
|
"test/npm-publish-plan.test.ts",
|
|
"test/openclaw-npm-release-check.test.ts",
|
|
npmPostpublish,
|
|
"test/plugin-npm-release.test.ts",
|
|
"test/plugin-clawhub-release.test.ts",
|
|
"android-version",
|
|
"android-pin-version",
|
|
"docker-release-policy",
|
|
"ios-version",
|
|
"openclaw-npm-extended-stable-release",
|
|
"openclaw-npm-publish",
|
|
"release-preflight",
|
|
"release-prepare",
|
|
"release-upgrade-baseline",
|
|
"release-version",
|
|
"upgrade-survivor-baselines",
|
|
"upgrade-survivor-config-recipe",
|
|
],
|
|
],
|
|
[
|
|
"scripts/lib/clawhub-bootstrap-artifact.mjs",
|
|
["clawhub-bootstrap-artifact", "verify-clawhub-published-artifact"],
|
|
],
|
|
[
|
|
"scripts/lib/plugin-npm-release.ts",
|
|
["test/plugin-npm-release.test.ts", "test/plugin-clawhub-release.test.ts"],
|
|
],
|
|
[
|
|
"scripts/lib/extension-source-classifier.mjs",
|
|
[
|
|
"extension-source-classifier",
|
|
"src/channels/plugins/contracts/channel-import-guardrails.test.ts",
|
|
],
|
|
],
|
|
["scripts/run-vitest.mjs", ["run-vitest", "test-projects", "vitest-local-scheduling"]],
|
|
["scripts/run-oxlint-shards.mjs", ["run-oxlint"]],
|
|
["scripts/docker-e2e-rerun.mjs", ["docker-e2e-helper-cli"]],
|
|
["scripts/openclaw-postpack.mjs", [TOOLING_VITEST_CONFIG]],
|
|
["scripts/openclaw-npm-prepublish-verify.ts", ["test/openclaw-npm-prepublish-verify.test.ts"]],
|
|
["scripts/lib/docker-e2e-scenarios.mjs", [dockerE2e, pluginPrerelease]],
|
|
["scripts/e2e/kitchen-sink-rpc-walk.mjs", ["kitchen-sink-rpc-walk", pluginPrerelease]],
|
|
[
|
|
"scripts/e2e/agents-delete-shared-workspace-docker.sh",
|
|
[dockerE2e, changedScope, "src/commands/agents.delete.test.ts"],
|
|
],
|
|
[
|
|
"scripts/e2e/browser-cdp-snapshot-docker.sh",
|
|
[dockerBuild, "browser-cdp-snapshot", "e2e-helper-env-limits"],
|
|
],
|
|
[
|
|
"scripts/e2e/config-reload-source-docker.sh",
|
|
[
|
|
dockerE2e,
|
|
packageAcceptance,
|
|
"fixture-config",
|
|
"e2e-mock-config-limits",
|
|
"src/gateway/config-reload.test.ts",
|
|
],
|
|
],
|
|
[
|
|
"scripts/e2e/gateway-network-docker.sh",
|
|
[dockerBuild, dockerE2e, packageAcceptance, "gateway-network-client", changedScope],
|
|
],
|
|
["scripts/e2e/npm-telegram-live-runner.ts", ["npm-telegram-live"]],
|
|
[
|
|
"scripts/e2e/upgrade-survivor-docker.sh",
|
|
[
|
|
dockerBuild,
|
|
dockerE2e,
|
|
packageAcceptance,
|
|
"upgrade-survivor-probe-gateway",
|
|
"upgrade-survivor-assertions",
|
|
"openclaw-test-state",
|
|
],
|
|
],
|
|
[
|
|
"scripts/e2e/bundled-plugin-install-uninstall-docker.sh",
|
|
[dockerBuild, dockerE2e, pluginPrerelease, "bundled-plugin-install-uninstall-probe"],
|
|
],
|
|
["scripts/e2e/lib/plugin-update/corrupt-update-scenario.sh", ["plugin-update-unchanged-docker"]],
|
|
["scripts/e2e/lib/plugin-update/probe.mjs", ["plugin-update-unchanged-docker"]],
|
|
["scripts/e2e/lib/plugin-update/unchanged-scenario.sh", ["plugin-update-unchanged-docker"]],
|
|
[
|
|
"scripts/e2e/update-corrupt-plugin-docker.sh",
|
|
[dockerBuild, dockerE2e, packageAcceptance, "plugin-update-unchanged-docker"],
|
|
],
|
|
["scripts/e2e/plugins-docker.sh", [dockerBuild, dockerE2e, "plugins-assertions"]],
|
|
[
|
|
"scripts/e2e/release-user-journey-docker.sh",
|
|
[dockerBuild, dockerE2e, packageAcceptance, "release-user-journey-assertions"],
|
|
],
|
|
[
|
|
"scripts/e2e/openai-image-auth-docker.sh",
|
|
[
|
|
dockerBuild,
|
|
dockerE2e,
|
|
"openai-image-auth-docker-client",
|
|
"extensions/openai/image-generation-provider.test.ts",
|
|
],
|
|
],
|
|
["scripts/e2e/lib/openai-chat-tools/client.mjs", [openaiChatToolsE2e]],
|
|
[
|
|
"scripts/e2e/lib/openai-web-search-minimal/client.mjs",
|
|
["test/e2e/qa-lab/runtime/openai-web-search-minimal.e2e.test.ts"],
|
|
],
|
|
["scripts/e2e/qr-import-docker.sh", [dockerBuild]],
|
|
["scripts/bundle-a2ui.mjs", ["bundled-plugin-assets"]],
|
|
]);
|
|
|
|
const SEMANTIC_TOOLING_TARGET_PATTERNS = [
|
|
[/^scripts\/pr$/u, ["pr-merge", "pr-operation-lock", "pr-wrappers"]],
|
|
[/^scripts\/lib\/windows-taskkill\.mjs$/u, ["managed-child-process", "run-with-env"]],
|
|
[
|
|
/^scripts\/lib\/config-boundary-guard\.mjs$/u,
|
|
[
|
|
"src/plugins/contracts/config-boundary-guard.test.ts",
|
|
"src/plugins/contracts/deprecated-internal-config-api.test.ts",
|
|
],
|
|
],
|
|
[
|
|
/^\.github\/workflows\/ci\.yml$/u,
|
|
[
|
|
workflowGuards,
|
|
"changed-lanes",
|
|
"check-workflows",
|
|
"plugin-contract-test-plan",
|
|
pluginPrerelease,
|
|
"verify-pr-hosted-gates",
|
|
],
|
|
],
|
|
[
|
|
/^\.github\/workflows\/ci-check-testbox\.yml$/u,
|
|
[workflowGuards, packageAcceptance, "changed-lanes", "install-trufflehog"],
|
|
],
|
|
[
|
|
/^\.github\/workflows\/ci-check-arm-testbox\.yml$/u,
|
|
[workflowGuards, packageAcceptance, "install-trufflehog"],
|
|
],
|
|
[/^\.github\/workflows\/crabbox-hydrate\.yml$/u, [workflowGuards, packageAcceptance]],
|
|
[
|
|
/^\.github\/workflows\/ci-build-artifacts-testbox\.yml$/u,
|
|
["install-trufflehog", packageAcceptance, workflowGuards],
|
|
],
|
|
[
|
|
/^\.github\/workflows\/full-release-validation\.yml$/u,
|
|
["src/dockerfile.test.ts", packageAcceptance, pluginPrerelease],
|
|
],
|
|
[
|
|
/^\.github\/workflows\/openclaw-release-checks\.yml$/u,
|
|
[packageAcceptance, crossOsReleaseChecks, pluginPrerelease, installDocker],
|
|
],
|
|
[/^\.github\/workflows\/docker-release\.yml$/u, ["src/dockerfile.test.ts"]],
|
|
[/^\.github\/workflows\/install-smoke\.yml$/u, ["install-smoke-no-push-workflow", installDocker]],
|
|
[/^\.github\/workflows\/openclaw-performance\.yml$/u, ["openclaw-performance-workflow"]],
|
|
[/^\.github\/workflows\/plugin-prerelease\.yml$/u, [pluginPrerelease]],
|
|
[/^\.github\/workflows\/tui-pty\.yml$/u, [packageAcceptance]],
|
|
[
|
|
/^\.github\/workflows\/openclaw-cross-os-release-checks-reusable\.yml$/u,
|
|
[crossOsReleaseChecks, "openclaw-cross-os-release-workflow", packageAcceptance],
|
|
],
|
|
[
|
|
/^\.github\/workflows\/(?:openclaw-release-publish|package-acceptance)\.yml$/u,
|
|
[packageAcceptance],
|
|
],
|
|
[
|
|
/^\.github\/workflows\/plugin-clawhub-new\.yml$/u,
|
|
[packageAcceptance, "plugin-clawhub-new-workflow"],
|
|
],
|
|
[
|
|
/^\.github\/workflows\/openclaw-npm-release\.yml$/u,
|
|
[npmPostpublish, "openclaw-npm-extended-stable-workflow", packageAcceptance],
|
|
],
|
|
[
|
|
new RegExp(
|
|
[
|
|
"^\\.github\\/workflows\\/(?:auto-response|clawsweeper-dispatch|labeler|",
|
|
"real-behavior-proof|stale)\\.yml$",
|
|
].join(""),
|
|
"u",
|
|
),
|
|
[workflowGuards],
|
|
],
|
|
[
|
|
new RegExp(
|
|
[
|
|
"^\\.github\\/workflows\\/mantis-(?:discord-(?:smoke|status-reactions|",
|
|
"thread-attachment)|slack-desktop-smoke)\\.yml$",
|
|
].join(""),
|
|
"u",
|
|
),
|
|
[packageAcceptance, workflowGuards],
|
|
],
|
|
[
|
|
/^\.github\/workflows\/mantis-telegram-desktop-proof\.yml$/u,
|
|
["mantis-telegram-desktop-proof-workflow", packageAcceptance, workflowGuards],
|
|
],
|
|
[
|
|
/^\.github\/workflows\/mantis-web-ui-chat-proof\.yml$/u,
|
|
["mantis-web-ui-chat-proof-workflow", packageAcceptance, workflowGuards],
|
|
],
|
|
[/^\.github\/workflows\/android-release\.yml$/u, [packageAcceptance, workflowGuards]],
|
|
[/^\.github\/actions\/setup-node-env\/verify-importers\.mjs$/u, [workflowGuards]],
|
|
[/^\.github\/actions\/ensure-base-commit\/action\.yml$/u, [workflowGuards]],
|
|
[/^tsconfig\.scripts\.json$/u, ["changed-lanes", "test-projects"]],
|
|
[/^scripts\/test-projects\.test-support\.mjs$/u, ["test-projects"]],
|
|
[/^scripts\/ci-changed-scope\.mjs$/u, [changedScope, "control-ui-i18n"]],
|
|
[/^scripts\/check-changed\.mjs$/u, ["changed-lanes"]],
|
|
[
|
|
new RegExp(
|
|
[
|
|
"^scripts\\/(?:generate-prompt-snapshots|prompt-snapshot-files|",
|
|
"sync-codex-model-prompt-fixture)\\.ts$",
|
|
].join(""),
|
|
"u",
|
|
),
|
|
["prompt-snapshots"],
|
|
],
|
|
[/^scripts\/e2e\/npm-telegram-live-docker\.sh$/u, ["npm-telegram-live"]],
|
|
[
|
|
/^scripts\/package-openclaw-for-docker\.mjs$/u,
|
|
["test/e2e/qa-lab/runtime/package-openclaw-for-docker.e2e.test.ts"],
|
|
],
|
|
[/^scripts\/run-node\.mjs$/u, [runNode]],
|
|
[/^scripts\/pr-lib\/merge\.sh$/u, ["pr-merge"]],
|
|
[/^scripts\/plugin-clawhub-publish\.sh$/u, ["test/plugin-clawhub-release.test.ts"]],
|
|
[/^scripts\/openclaw-npm-postpublish-verify\.ts$/u, [npmPostpublish]],
|
|
[
|
|
/^scripts\/install\.ps1$/u,
|
|
["install-ps1", "website-installer-sync-workflow", crossOsReleaseChecks, changedScope],
|
|
],
|
|
[/^scripts\/crabbox-wrapper(?:-providers)?\.mjs$/u, ["crabbox-wrapper"]],
|
|
[
|
|
/^scripts\/copy-bundled-plugin-metadata\.mjs$/u,
|
|
["src/plugins/copy-bundled-plugin-metadata.test.ts", runNode],
|
|
],
|
|
[
|
|
/^scripts\/github\/run-openclaw-cross-os-release-checks\.sh$/u,
|
|
["openclaw-cross-os-release-workflow"],
|
|
],
|
|
[
|
|
/^scripts\/write-plugin-sdk-entry-dts\.ts$/u,
|
|
["build-all", "prepare-extension-package-boundary-artifacts"],
|
|
],
|
|
[/^scripts\/pr-lib\/worktree\.sh$/u, ["test/vitest/vitest.tooling.config.ts"]],
|
|
[/^scripts\/dev\/gateway-smoke\.ts$/u, ["test/e2e/qa-lab/runtime/gateway-smoke.e2e.test.ts"]],
|
|
[/^scripts\/e2e\/mock-openai-server\.mjs$/u, ["e2e-mock-config-limits"]],
|
|
[/^apps\/android\/scripts\/build-release-artifacts\.ts$/u, ["android-release-artifacts"]],
|
|
[
|
|
new RegExp(
|
|
[
|
|
"^scripts\\/(?:auth-monitor|mobile-reauth|setup-auth-system|",
|
|
"termux-(?:auth-widget|quick-auth|sync-widget))\\.sh$|",
|
|
"^scripts\\/systemd\\/openclaw-auth-monitor\\.(?:service|timer)$",
|
|
].join(""),
|
|
"u",
|
|
),
|
|
["auth-monitor"],
|
|
],
|
|
[/^scripts\/native-app-i18n\.ts$/u, ["native-app-i18n", workflowGuards]],
|
|
[
|
|
/^scripts\/github\/(?:dependency-guard|guard-shared)\.mjs$/u,
|
|
["dependency-guard-script", "dependency-guard-workflow"],
|
|
],
|
|
[
|
|
/^scripts\/github\/(?:security-sensitive-guard|guard-shared)\.mjs$/u,
|
|
["security-sensitive-guard-script", "security-sensitive-guard-workflow"],
|
|
],
|
|
[/^scripts\/plugin-clawhub-release-check\.ts$/u, ["release-wrapper-scripts"]],
|
|
[
|
|
/^scripts\/generate-runtime-sidecar-paths-baseline\.ts$/u,
|
|
["src/plugins/bundled-plugin-metadata.test.ts"],
|
|
],
|
|
[
|
|
/^scripts\/lib\/guard-inventory-utils\.mjs$/u,
|
|
[
|
|
"test/extension-import-boundaries.test.ts",
|
|
"test/plugin-extension-import-boundary.test.ts",
|
|
"test/architecture-smells.test.ts",
|
|
"test/web-provider-boundary.test.ts",
|
|
"test/test-helper-extension-import-boundary.test.ts",
|
|
"extension-import-boundary-checker",
|
|
"src/plugins/contracts/plugin-sdk-subpaths.test.ts",
|
|
],
|
|
],
|
|
[
|
|
/^scripts\/check-workflows\.mjs$/u,
|
|
["check-composite-action-input-interpolation", "check-no-conflict-markers", workflowGuards],
|
|
],
|
|
[/^apps\/android\/fastlane\/Fastfile$/u, ["android-release-fastlane-gates"]],
|
|
[/^apps\/ios\/fastlane\/Fastfile$/u, ["ios-release-fastlane-gates"]],
|
|
[/^scripts\/ios-release-cut\.(?:sh|ts)$/u, ["ios-release-plan"]],
|
|
[/^scripts\/ios-release-prepare\.sh$/u, ["ios-release-prepare", "ios-release-wrapper-args"]],
|
|
[
|
|
/^scripts\/lib\/bundled-runtime-sidecar-paths\.json$/u,
|
|
[
|
|
"src/plugins/bundled-plugin-metadata.test.ts",
|
|
"src/infra/update-global.test.ts",
|
|
"src/infra/update-runner.test.ts",
|
|
npmPostpublish,
|
|
],
|
|
],
|
|
[/^scripts\/lib\/android-version\.ts$/u, ["android-version", "android-pin-version"]],
|
|
[/^scripts\/lib\/docker-e2e-plan\.mjs$/u, [dockerE2e, "docker-all-scheduler", pluginPrerelease]],
|
|
[
|
|
new RegExp(
|
|
[
|
|
"^scripts\\/lib\\/plugin-sdk-(?:deprecated-barrel-subpaths|entrypoints|",
|
|
"private-local-only-subpaths)\\.json$",
|
|
].join(""),
|
|
"u",
|
|
),
|
|
pluginSdkEntryOwners,
|
|
],
|
|
[
|
|
/^scripts\/lib\/plugin-sdk-deprecated-public-subpaths\.json$/u,
|
|
[
|
|
"check-deprecated-api-usage",
|
|
"src/plugins/contracts/plugin-sdk-package-contract-guardrails.test.ts",
|
|
"plugin-sdk-surface-report",
|
|
"build-all",
|
|
],
|
|
],
|
|
[/^scripts\/lib\/plugin-sdk-entries\.mjs$/u, pluginSdkEntryOwners],
|
|
[
|
|
/^scripts\/lib\/official-external-channel-catalog\.json$/u,
|
|
[
|
|
"src/plugins/official-external-plugin-catalog.test.ts",
|
|
releaseCheck,
|
|
"test/official-channel-catalog.test.ts",
|
|
],
|
|
],
|
|
[
|
|
/^scripts\/lib\/official-external-provider-catalog\.json$/u,
|
|
["src/plugins/official-external-plugin-catalog.test.ts", releaseCheck],
|
|
],
|
|
[
|
|
/^scripts\/lib\/official-external-plugin-catalog\.json$/u,
|
|
["src/plugins/official-external-plugin-catalog.test.ts", releaseCheck],
|
|
],
|
|
[
|
|
/^scripts\/lib\/workspace-bootstrap-smoke\.mjs$/u,
|
|
[releaseCheck, "test/openclaw-npm-release-check.test.ts"],
|
|
],
|
|
[/^scripts\/lib\/extension-test-plan\.mjs$/u, ["test-extension"]],
|
|
[
|
|
/^scripts\/lib\/recommended-tool-installs\.json$/u,
|
|
["src/plugins/recommended-tool-installs.test.ts", releaseCheck],
|
|
],
|
|
[/^scripts\/docker\/install-sh-common\/version-parse\.sh$/u, [installDocker]],
|
|
[
|
|
/^scripts\/lib\/local-build-metadata(?:-paths)?\.mjs$/u,
|
|
[
|
|
"src/infra/build-stamp.test.ts",
|
|
"runtime-postbuild-stamp",
|
|
runNode,
|
|
"src/infra/package-dist-inventory.test.ts",
|
|
releaseCheck,
|
|
"test/openclaw-npm-release-check.test.ts",
|
|
"check-gateway-watch-regression",
|
|
"check-openclaw-package-tarball",
|
|
crossOsReleaseChecks,
|
|
],
|
|
],
|
|
[
|
|
/^scripts\/lib\/package-dist-imports\.mjs$/u,
|
|
[
|
|
"check-package-dist-imports",
|
|
"check-openclaw-package-tarball",
|
|
"postinstall-bundled-plugins",
|
|
releaseCheck,
|
|
],
|
|
],
|
|
[
|
|
/^scripts\/lib\/build-metadata\.sh$/u,
|
|
[
|
|
"src/docker-setup.e2e.test.ts",
|
|
"apple-release-source-check",
|
|
"ios-version",
|
|
"package-mac-app",
|
|
installDocker,
|
|
],
|
|
],
|
|
[/^scripts\/lib\/plistbuddy\.sh$/u, ["create-dmg", "package-mac-app", "package-mac-dist"]],
|
|
[/^scripts\/lib\/swift-toolchain\.sh$/u, ["package-mac-app", "package-mac-dist"]],
|
|
[
|
|
/^scripts\/lib\/npm-publish-plan\.mjs$/u,
|
|
[
|
|
"test/npm-publish-plan.test.ts",
|
|
"test/openclaw-npm-release-check.test.ts",
|
|
npmPostpublish,
|
|
"test/plugin-npm-release.test.ts",
|
|
"test/plugin-clawhub-release.test.ts",
|
|
"release-upgrade-baseline",
|
|
"android-version",
|
|
"ios-version",
|
|
"upgrade-survivor-baselines",
|
|
"upgrade-survivor-config-recipe",
|
|
],
|
|
],
|
|
[/^scripts\/lib\/npm-pack-budget\.mjs$/u, [releaseCheck, installDocker]],
|
|
[/^scripts\/lib\/actions-artifact-archive\.mjs$/u, ["plugin-publication-artifact"]],
|
|
[
|
|
/^scripts\/lib\/static-extension-assets\.mjs$/u,
|
|
["bundled-plugin-assets", "runtime-postbuild", runNode, "plugin-npm-runtime-build-args"],
|
|
],
|
|
[
|
|
/^scripts\/lib\/plugin-npm-runtime-build\.mjs$/u,
|
|
["plugin-npm-runtime-build-args", "test/plugin-npm-runtime-build.test.ts"],
|
|
],
|
|
[/^scripts\/lib\/run-node\.mjs$/u, [runNode]],
|
|
[
|
|
/^\.agents\/skills\/openclaw-changelog-update\/scripts\/verify-release-notes\.mjs$/u,
|
|
["release-notes-ledger", "verify-release-notes"],
|
|
],
|
|
[
|
|
/^scripts\/e2e\/lib\/docker-stats\/assert-resource-ceiling\.mjs$/u,
|
|
["docker-stats-resource-ceiling"],
|
|
],
|
|
[
|
|
/^scripts\/e2e\/mcp-channels-docker\.sh$/u,
|
|
[dockerBuild, "docker-e2e-observability", dockerE2e, pluginPrerelease],
|
|
],
|
|
[
|
|
/^scripts\/e2e\/cron-mcp-cleanup-docker\.sh$/u,
|
|
[dockerBuild, "docker-e2e-observability", dockerE2e, pluginPrerelease],
|
|
],
|
|
[
|
|
/^scripts\/e2e\/mcp-code-mode-gateway-(?:live-)?docker\.sh$/u,
|
|
[
|
|
dockerBuild,
|
|
dockerE2e,
|
|
pluginPrerelease,
|
|
"mcp-code-mode-gateway-client",
|
|
"session-log-mentions",
|
|
],
|
|
],
|
|
[
|
|
/^scripts\/e2e\/agent-bundle-mcp-tools-docker\.sh$/u,
|
|
[
|
|
dockerBuild,
|
|
dockerE2e,
|
|
pluginPrerelease,
|
|
"src/agents/agent-bundle-mcp-runtime.test.ts",
|
|
"src/agents/agent-bundle-mcp-tools.materialize.test.ts",
|
|
],
|
|
],
|
|
[/^scripts\/lib\/tsgo-sparse-guard\.mjs$/u, ["run-tsgo", "changed-lanes"]],
|
|
[
|
|
new RegExp(
|
|
[
|
|
"^(?:scripts\\/materialize-clawhub-cli\\.sh|",
|
|
"\\.github\\/release\\/clawhub-cli\\/package(?:-lock)?\\.json)$",
|
|
].join(""),
|
|
"u",
|
|
),
|
|
[packageAcceptance, "plugin-clawhub-new-workflow"],
|
|
],
|
|
[
|
|
/^scripts\/lib\/generated-text-asset\.mjs$/u,
|
|
[
|
|
"extensions/browser/scripts/build-copilot-runtime.test.ts",
|
|
"build-diffs-viewer-runtime",
|
|
"bundled-plugin-assets",
|
|
],
|
|
],
|
|
[/^scripts\/check-plugin-npm-runtime-builds\.mjs$/u, ["plugin-npm-runtime-build-args"]],
|
|
[
|
|
/^scripts\/install\.sh$/u,
|
|
[
|
|
"install-sh",
|
|
installDocker,
|
|
"website-installer-sync-workflow",
|
|
crossOsReleaseChecks,
|
|
changedScope,
|
|
],
|
|
],
|
|
[
|
|
/^scripts\/sparkle-build\.ts$/u,
|
|
["test/appcast.test.ts", releaseCheck, "package-mac-app", "package-mac-dist"],
|
|
],
|
|
[
|
|
/^test\/vitest\/vitest\.contracts-paths\.mjs$/u,
|
|
[
|
|
"test-projects",
|
|
"test/vitest/vitest.contracts-channel-surface.config.ts",
|
|
"test/vitest/vitest.contracts-channel-config.config.ts",
|
|
"test/vitest/vitest.contracts-channel-registry.config.ts",
|
|
"test/vitest/vitest.contracts-channel-session.config.ts",
|
|
],
|
|
],
|
|
[/^scripts\/e2e\/openai-chat-tools-docker\.sh$/u, [openaiChatToolsE2e, dockerE2e]],
|
|
[
|
|
/^scripts\/e2e\/session-runtime-context-docker\.sh$/u,
|
|
[
|
|
"docker-e2e-clients",
|
|
dockerE2e,
|
|
"src/agents/embedded-agent-runner/run/runtime-context-prompt.test.ts",
|
|
"src/agents/embedded-agent-runner/transcript-rewrite.test.ts",
|
|
],
|
|
],
|
|
[
|
|
new RegExp(
|
|
[
|
|
"^scripts\\/e2e\\/(?!(?:commitments-safety|config-reload-source|",
|
|
"kitchen-sink-(?:plugin|rpc)|npm-telegram-live|onboard|openai-chat-tools|",
|
|
"plugin-lifecycle-matrix|release-media-memory|session-runtime-context|",
|
|
"update-corrupt-plugin)-docker\\.sh$).+-docker\\.sh$",
|
|
].join(""),
|
|
"u",
|
|
),
|
|
[dockerBuild, dockerE2e],
|
|
],
|
|
[/^scripts\/e2e\/kitchen-sink-rpc-docker\.sh$/u, [dockerBuild, pluginPrerelease]],
|
|
[/^scripts\/e2e\/kitchen-sink-plugin-docker\.sh$/u, [dockerBuild, pluginPrerelease]],
|
|
[/^scripts\/e2e\/plugin-lifecycle-matrix-docker\.sh$/u, [dockerBuild]],
|
|
[
|
|
/^scripts\/e2e\/release-media-memory-docker\.sh$/u,
|
|
[dockerE2e, "release-media-memory-scenario"],
|
|
],
|
|
[/^scripts\/e2e\/codex-media-path-docker\.sh$/u, ["codex-media-path-client"]],
|
|
[/^scripts\/e2e\/live-plugin-tool-docker\.sh$/u, ["live-plugin-tool-assertions"]],
|
|
[
|
|
/^scripts\/e2e\/commitments-safety-docker\.sh$/u,
|
|
[
|
|
"docker-e2e-clients",
|
|
dockerE2e,
|
|
"src/commitments/runtime.test.ts",
|
|
"src/commitments/store.test.ts",
|
|
],
|
|
],
|
|
[/^scripts\/e2e\/onboard-docker\.sh$/u, [dockerBuild, "openclaw-test-state"]],
|
|
[
|
|
new RegExp(
|
|
[
|
|
"^scripts\\/e2e\\/(?:codex-npm-plugin-live|config-reload-source|",
|
|
"gateway-network|npm-onboard-channel-agent|doctor-install-switch|",
|
|
"update-channel-switch|skill-install|upgrade-survivor|",
|
|
"update-corrupt-plugin|release-(?:plugin-marketplace|typed-onboarding|",
|
|
"upgrade-user-journey|user-journey)|plugin-binding-command-escape)-docker",
|
|
"\\.sh$",
|
|
].join(""),
|
|
"u",
|
|
),
|
|
[packageAcceptance],
|
|
],
|
|
[
|
|
/^scripts\/e2e\/npm-onboard-channel-agent-docker\.sh$/u,
|
|
["npm-onboard-channel-agent-assertions"],
|
|
],
|
|
[/^scripts\/docker\/cleanup-smoke\/Dockerfile$/u, [dockerCache, dockerDigests, dockerBuild]],
|
|
[
|
|
/^scripts\/docker\/install-sh-(?:e2e|nonroot|smoke)\/Dockerfile$/u,
|
|
[dockerCache, dockerDigests, installDocker],
|
|
],
|
|
[
|
|
/^scripts\/docker\/sandbox\/Dockerfile$/u,
|
|
[dockerCache, dockerDigests, "src/dockerfile.test.ts"],
|
|
],
|
|
[
|
|
/^scripts\/docker\/sandbox\/Dockerfile\.browser$/u,
|
|
[dockerCache, dockerDigests, "src/agents/sandbox/browser.create.test.ts"],
|
|
],
|
|
[/^scripts\/docker\/sandbox\/Dockerfile\.common$/u, [dockerCache]],
|
|
[/^scripts\/e2e\/Dockerfile$/u, [dockerCache, dockerDigests, dockerBuild, dockerE2e]],
|
|
[/^scripts\/e2e\/Dockerfile\.qr-import$/u, [dockerCache, dockerDigests, dockerBuild]],
|
|
[
|
|
/^scripts\/e2e\/plugin-binding-command-escape\.Dockerfile$/u,
|
|
[dockerDigests, dockerBuild, dockerE2e],
|
|
],
|
|
[
|
|
new RegExp(
|
|
[
|
|
"^(?:scripts\\/e2e|test\\/e2e\\/qa-lab\\/runtime)\\/agent-bundle-mcp-tools-doc",
|
|
"ker(?:-client)?\\.(?:sh|ts)$",
|
|
].join(""),
|
|
"u",
|
|
),
|
|
[
|
|
"src/agents/agent-bundle-mcp-runtime.test.ts",
|
|
"src/agents/agent-bundle-mcp-tools.materialize.test.ts",
|
|
],
|
|
],
|
|
[/^scripts\/e2e\/system-agent-(?:first-run|rescue)-docker\.sh$/u, ["docker-e2e-system-agent"]],
|
|
[
|
|
/^test\/e2e\/qa-lab\/runtime\/system-agent-first-run-docker-client\.ts$/u,
|
|
[
|
|
"docker-e2e-system-agent",
|
|
"src/cli/program/register.onboard.test.ts",
|
|
"src/cli/run-main.test.ts",
|
|
"src/cli/run-main.exit.test.ts",
|
|
"src/commands/system-agent-with-inference.test.ts",
|
|
"src/system-agent/assistant.configured.test.ts",
|
|
"src/system-agent/assistant.test.ts",
|
|
"src/system-agent/system-agent.test.ts",
|
|
"src/system-agent/operations.test.ts",
|
|
"src/system-agent/overview.test.ts",
|
|
"src/system-agent/setup-inference.test.ts",
|
|
"src/system-agent/audit.test.ts",
|
|
],
|
|
],
|
|
[
|
|
/^scripts\/e2e\/system-agent-first-run-spec\.json$/u,
|
|
[
|
|
"docker-e2e-system-agent",
|
|
"src/system-agent/operations.test.ts",
|
|
"src/system-agent/audit.test.ts",
|
|
],
|
|
],
|
|
[
|
|
/^scripts\/e2e\/system-agent-rescue-docker-client\.ts$/u,
|
|
[
|
|
"docker-e2e-system-agent",
|
|
"src/system-agent/rescue-policy.test.ts",
|
|
"src/system-agent/rescue-message.test.ts",
|
|
"src/system-agent/operations.test.ts",
|
|
"src/system-agent/audit.test.ts",
|
|
],
|
|
],
|
|
[
|
|
/^scripts\/e2e\/commitments-safety-docker(?:-client)?\.(?:sh|ts)$/u,
|
|
["docker-e2e-clients", "src/commitments/runtime.test.ts", "src/commitments/store.test.ts"],
|
|
],
|
|
[
|
|
/^scripts\/e2e\/session-runtime-context-docker(?:-client)?\.(?:sh|ts)$/u,
|
|
[
|
|
"docker-e2e-clients",
|
|
"src/agents/embedded-agent-runner/run/runtime-context-prompt.test.ts",
|
|
"src/agents/embedded-agent-runner/transcript-rewrite.test.ts",
|
|
],
|
|
],
|
|
[
|
|
new RegExp(
|
|
[
|
|
"^(?:scripts\\/e2e\\/(?!(?:cron-mcp-cleanup-docker-client\\.ts)$)(?:agent-bu",
|
|
"ndle-mcp-tools|mcp-[^/]+-docker|cron-mcp-cleanup-docker|",
|
|
"bundled-plugin-install-uninstall|plugin-update-unchanged).+|",
|
|
"test\\/e2e\\/qa-lab\\/runtime\\/mcp-channels-docker-client\\.ts)$",
|
|
].join(""),
|
|
"u",
|
|
),
|
|
[pluginPrerelease, dockerE2e],
|
|
],
|
|
[
|
|
new RegExp(
|
|
[
|
|
"^(?:scripts\\/e2e\\/mcp-code-mode-gateway-(?:docker|live-docker|",
|
|
"docker-client).+|scripts\\/mcp-code-mode-gateway-e2e\\.ts)$",
|
|
].join(""),
|
|
"u",
|
|
),
|
|
["mcp-code-mode-gateway-client", "session-log-mentions"],
|
|
],
|
|
[
|
|
/^(?:scripts\/e2e\/(?:mcp-channels|mcp-code-mode-gateway|cron-mcp-cleanup)-seed\.ts)$/u,
|
|
["docker-e2e-seeds"],
|
|
],
|
|
[
|
|
/^scripts\/e2e\/(?:mcp-channels|cron-cli|cron-mcp-cleanup)-docker\.sh$/u,
|
|
["docker-e2e-observability"],
|
|
],
|
|
[
|
|
/^scripts\/e2e\/cron-mcp-cleanup-docker-client\.ts$/u,
|
|
[
|
|
"cron-mcp-cleanup-docker-client",
|
|
"src/gateway/server.cron.test.ts",
|
|
"src/gateway/server-methods/agent.test.ts",
|
|
"src/cron/isolated-agent/run.fast-mode.test.ts",
|
|
"src/cron/active-jobs-manual-run.test.ts",
|
|
],
|
|
],
|
|
[
|
|
/^scripts\/e2e\/cron-mcp-cleanup-docker\.sh$/u,
|
|
["cron-mcp-cleanup-docker-client", "docker-e2e-seeds"],
|
|
],
|
|
[
|
|
/^test\/e2e\/qa-lab\/runtime\/mcp-channels\.fixture\.ts$/u,
|
|
["test/e2e/qa-lab/runtime/mcp-gateway-transport.e2e.test.ts", "cron-mcp-cleanup-docker-client"],
|
|
],
|
|
[
|
|
/^scripts\/e2e\/lib\/auth-profile-store-assertions\.mjs$/u,
|
|
["release-scenarios-assertions", "npm-onboard-channel-agent-assertions"],
|
|
],
|
|
[
|
|
/^scripts\/lib\/plugin-npm-package-manifest\.mjs$/u,
|
|
["plugin-npm-package-manifest-args", "test/plugin-npm-package-manifest.test.ts"],
|
|
],
|
|
[
|
|
/^scripts\/e2e\/lib\/release-assertion-files\.mjs$/u,
|
|
["release-scenarios-assertions", "release-user-journey-assertions"],
|
|
],
|
|
[/^scripts\/e2e\/lib\/fixtures\/config\.mjs$/u, ["fixture-config"]],
|
|
[/^scripts\/e2e\/lib\/fixtures\/plugins\.mjs$/u, ["fixture-plugin-commands"]],
|
|
[
|
|
/^scripts\/e2e\/lib\/codex-app-server-fixture\.mjs$/u,
|
|
["codex-media-path-client", "test/e2e/qa-lab/runtime/codex-auth-product-proof.e2e.test.ts"],
|
|
],
|
|
[
|
|
/^scripts\/e2e\/lib\/incremental-line-reader\.mjs$/u,
|
|
[
|
|
"incremental-line-reader",
|
|
"config-reload-log-scanner",
|
|
"codex-media-path-client",
|
|
"test/e2e/qa-lab/runtime/codex-auth-product-proof.e2e.test.ts",
|
|
],
|
|
],
|
|
[
|
|
/^scripts\/e2e\/lib\/fixture\.mjs$/u,
|
|
["fixture-config", "fixtures-workspace", "fixture-plugin-commands"],
|
|
],
|
|
[
|
|
/^test\/e2e\/qa-lab\/runtime\/openai-image-auth-docker-client\.ts$/u,
|
|
[
|
|
"openai-image-auth-docker-client",
|
|
"extensions/openai/image-generation-provider.test.ts",
|
|
"src/image-generation/openai-compatible-image-provider.test.ts",
|
|
],
|
|
],
|
|
[
|
|
/^test\/e2e\/qa-lab\/runtime\/codex-auth-app-server\.fixture\.mjs$/u,
|
|
["test/e2e/qa-lab/runtime/codex-auth-product-proof.e2e.test.ts"],
|
|
],
|
|
[
|
|
/^scripts\/e2e\/lib\/openai-web-search-minimal\/(?:mock-server\.mjs|scenario\.sh)$/u,
|
|
[
|
|
"test/e2e/qa-lab/runtime/openai-web-search-minimal.e2e.test.ts",
|
|
"test/e2e/qa-lab/runtime/openai-web-search-minimal-assertions.e2e.test.ts",
|
|
],
|
|
],
|
|
[
|
|
/^scripts\/e2e\/lib\/openwebui\/http-probe\.mjs$/u,
|
|
["test/e2e/qa-lab/runtime/openwebui-probe.e2e.test.ts"],
|
|
],
|
|
[
|
|
/^scripts\/e2e\/(?:browser-cdp-snapshot-docker\.sh|lib\/env-limits\.mjs)$/u,
|
|
["e2e-helper-env-limits"],
|
|
],
|
|
[/^scripts\/e2e\/skill-install-docker\.sh$/u, ["e2e-shell-tempfiles"]],
|
|
[/^scripts\/e2e\/npm-onboard-channel-agent-docker\.sh$/u, [pluginPrerelease]],
|
|
[
|
|
/^scripts\/e2e\/lib\/clawhub-fixture-server\.cjs$/u,
|
|
["clawhub-fixture-server", pluginPrerelease],
|
|
],
|
|
[
|
|
/^scripts\/e2e\/lib\/codex-media-path\/jsonl-request-tail\.mjs$/u,
|
|
["codex-media-path-client", "test/e2e/qa-lab/runtime/codex-auth-product-proof.e2e.test.ts"],
|
|
],
|
|
[
|
|
/^scripts\/e2e\/lib\/codex-npm-plugin-live\/assertions\.mjs$/u,
|
|
["codex-install-assertions", dockerBuild],
|
|
],
|
|
[
|
|
/^scripts\/e2e\/(?:lib\/openai-chat-tools\/scenario\.sh|openai-chat-tools-docker\.sh)$/u,
|
|
[openaiChatToolsE2e],
|
|
],
|
|
[/^scripts\/e2e\/lib\/openai-chat-tools\/write-config\.mjs$/u, [openaiChatToolsE2e]],
|
|
[
|
|
/^scripts\/e2e\/openai-web-search-minimal-docker\.sh$/u,
|
|
[
|
|
"test/e2e/qa-lab/runtime/openai-web-search-minimal.e2e.test.ts",
|
|
"test/e2e/qa-lab/runtime/openai-web-search-minimal-assertions.e2e.test.ts",
|
|
],
|
|
],
|
|
[
|
|
/^scripts\/e2e\/openwebui-docker\.sh$/u,
|
|
["test/e2e/qa-lab/runtime/openwebui-probe.e2e.test.ts", "fixture-config"],
|
|
],
|
|
];
|
|
function resolveSemanticToolingTargets(changedPath) {
|
|
return SEMANTIC_TOOLING_TARGET_PATTERNS.flatMap(([pattern, owners]) =>
|
|
pattern.test(changedPath) ? resolveToolingTestOwnerTargets(...owners) : [],
|
|
);
|
|
}
|
|
|
|
function resolveGithubYamlGuardTargets(changedPath) {
|
|
if (/^\.github\/workflows\/[^/]+\.ya?ml$/u.test(changedPath)) {
|
|
return GITHUB_YAML_PINNING_GUARD_TEST_TARGETS;
|
|
}
|
|
if (/^\.github\/actions\/.+\.ya?ml$/u.test(changedPath)) {
|
|
return GITHUB_YAML_PINNING_GUARD_TEST_TARGETS;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function resolveDirectToolingReferenceTests(changedPath, cwd) {
|
|
const normalized = normalizePathPattern(changedPath);
|
|
return (listImportGraphGrepMatches(cwd, normalized, { tooling: true }) ?? []).filter(
|
|
(file) =>
|
|
file !== "test/scripts/test-projects.test.ts" &&
|
|
!file.endsWith(".live.test.ts") &&
|
|
isTestFileTarget(file) &&
|
|
fs
|
|
.readFileSync(path.join(cwd, file), "utf8")
|
|
.match(/[A-Za-z0-9_.@+/-]{4,}/gu)
|
|
?.includes(normalized),
|
|
);
|
|
}
|
|
|
|
function resolveToolingTestTargets(changedPath, cwd = process.cwd()) {
|
|
if (changedPath.startsWith("test/scripts/") && isTestFileTarget(changedPath)) {
|
|
return [changedPath];
|
|
}
|
|
if (BROAD_CHANGED_FALLBACK_PATTERNS.some((pattern) => pattern.test(changedPath))) {
|
|
return null;
|
|
}
|
|
// Declarations and implementations share the same owner; deriving the
|
|
// sibling also covers newly added script declarations without an inventory.
|
|
const implementationPath = changedPath.endsWith(".d.mts")
|
|
? changedPath.replace(/\.d\.mts$/u, ".mjs")
|
|
: changedPath;
|
|
const exactOwners = EXACT_TOOLING_TARGETS.get(implementationPath);
|
|
if (exactOwners) {
|
|
return resolveToolingTestOwnerTargets(...exactOwners);
|
|
}
|
|
const semanticTargets = resolveSemanticToolingTargets(implementationPath);
|
|
const facts = getChangedPathFacts(changedPath);
|
|
const hasToolingOwner =
|
|
semanticTargets.length > 0 ||
|
|
facts.surface === "rootTooling" ||
|
|
changedPath === "Dockerfile" ||
|
|
changedPath === ".crabbox.yaml" ||
|
|
changedPath.startsWith(".agents/") ||
|
|
isToolingScriptPath(implementationPath) ||
|
|
(facts.surface === "app" && /\/(?:fastlane|scripts)\//u.test(changedPath)) ||
|
|
(facts.surface === "extension" && /\/(?:scripts\/|package\.json$)/u.test(changedPath)) ||
|
|
(facts.surface === "rootTest" && changedPath.startsWith("test/e2e/qa-lab/"));
|
|
if (!hasToolingOwner) {
|
|
return null;
|
|
}
|
|
const crossOsReleaseTargets =
|
|
implementationPath === "scripts/openclaw-cross-os-release-checks.ts" ||
|
|
implementationPath.startsWith("scripts/lib/cross-os-release-checks/")
|
|
? ["test/scripts/openclaw-cross-os-release-checks.test.ts"]
|
|
: null;
|
|
const explicitTargets =
|
|
(changedPath === "Dockerfile"
|
|
? [
|
|
"src/docker-build-cache.test.ts",
|
|
"src/docker-image-digests.test.ts",
|
|
"src/dockerfile.test.ts",
|
|
"test/scripts/test-install-sh-docker.test.ts",
|
|
]
|
|
: changedPath === ".crabbox.yaml"
|
|
? ["test/scripts/package-acceptance-workflow.test.ts"]
|
|
: null) ??
|
|
crossOsReleaseTargets ??
|
|
resolveUpgradeSurvivorConfigRecipeTargets(implementationPath) ??
|
|
resolveDocsI18nBehaviorTargets(implementationPath) ??
|
|
resolveDocsI18nGoTargets(implementationPath) ??
|
|
resolveK8sManifestTargets(implementationPath) ??
|
|
resolveParallelsToolingTestTargets(implementationPath);
|
|
const githubYamlGuardTargets = resolveGithubYamlGuardTargets(implementationPath);
|
|
const conventionalTargets = resolveConventionalToolingTestTargets(implementationPath, cwd);
|
|
const hasDirectOwner = Boolean(
|
|
explicitTargets?.length ||
|
|
githubYamlGuardTargets?.length ||
|
|
semanticTargets.length ||
|
|
conventionalTargets?.length,
|
|
);
|
|
const importGraphResult =
|
|
!hasDirectOwner &&
|
|
TOOLING_IMPORTABLE_FILE_EXTENSIONS.some((ext) => implementationPath.endsWith(ext))
|
|
? resolveAffectedTestsFromTargetedImportScan(implementationPath, cwd, {
|
|
tooling: true,
|
|
direct: true,
|
|
})
|
|
: [];
|
|
const importGraphTargets = importGraphResult ?? [];
|
|
const referenceTargets =
|
|
semanticTargets.length === 0 && (githubYamlGuardTargets || !hasDirectOwner)
|
|
? resolveDirectToolingReferenceTests(implementationPath, cwd)
|
|
: [];
|
|
const targets = [
|
|
...(explicitTargets ?? []),
|
|
...semanticTargets,
|
|
...(conventionalTargets ?? []),
|
|
...importGraphTargets,
|
|
...referenceTargets,
|
|
...(githubYamlGuardTargets ?? []),
|
|
];
|
|
if (targets.length > 0) {
|
|
return uniqueOrdered(targets);
|
|
}
|
|
return isToolingScriptPath(implementationPath) || facts.surface === "rootTooling"
|
|
? [TOOLING_VITEST_CONFIG]
|
|
: null;
|
|
}
|
|
|
|
function shouldUseBroadChangedTargets(env = process.env) {
|
|
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;
|
|
}
|
|
const surface = getChangedPathFacts(changedPath).surface;
|
|
return (
|
|
["source", "package", "extension", "rootTest"].includes(surface) ||
|
|
changedPath === "ui" ||
|
|
changedPath.startsWith("ui/") ||
|
|
["src", "test", "extensions", "packages"].includes(changedPath)
|
|
);
|
|
}
|
|
|
|
function resolveSiblingTestTarget(changedPath, 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/");
|
|
}
|
|
|
|
function resolvePromptSnapshotFixtureTargets(changedPath) {
|
|
if (!/^test\/fixtures\/agents\/prompt-snapshots\/.+\.(?:json|md)$/u.test(changedPath)) {
|
|
return null;
|
|
}
|
|
return ["test/scripts/prompt-snapshots.test.ts"];
|
|
}
|
|
|
|
function resolveAppcastTargets(changedPath) {
|
|
return changedPath === "appcast.xml" ? APPCAST_TEST_TARGETS : null;
|
|
}
|
|
|
|
function resolvePreciseChangedTestTargets(changedPath, options) {
|
|
const cwd = options.cwd ?? process.cwd();
|
|
const mappedTargets =
|
|
SOURCE_TEST_TARGETS.get(changedPath) ??
|
|
resolveToolingTestTargets(changedPath, cwd) ??
|
|
resolveAppcastTargets(changedPath) ??
|
|
resolvePromptSnapshotFixtureTargets(changedPath);
|
|
if (mappedTargets) {
|
|
return mappedTargets;
|
|
}
|
|
if (isRoutableChangedTarget(changedPath) && isTestFileTarget(changedPath)) {
|
|
return [changedPath];
|
|
}
|
|
const siblingTest = resolveSiblingTestTarget(changedPath, cwd);
|
|
if (
|
|
siblingTest &&
|
|
!shouldCombineSiblingTestWithImportGraph(changedPath) &&
|
|
options.combineSiblingWithImportGraph !== true
|
|
) {
|
|
return [siblingTest];
|
|
}
|
|
if (shouldRouteChangedTargetWithoutImportGraph(changedPath)) {
|
|
return changedPath.startsWith("ui/src/") ? [changedPath] : null;
|
|
}
|
|
if (options.skipImportGraph === true) {
|
|
return null;
|
|
}
|
|
const facts = getChangedPathFacts(changedPath);
|
|
if (
|
|
facts.surface === "source" ||
|
|
facts.surface === "package" ||
|
|
facts.surface === "extension" ||
|
|
changedPath.startsWith("test/helpers/") ||
|
|
changedPath.startsWith("ui/src/") ||
|
|
changedPath.startsWith("ui/config/")
|
|
) {
|
|
const affectedTests = resolveAffectedTestsFromImportGraph(changedPath, cwd, {
|
|
forceFull: options.forceFullImportGraph === true,
|
|
});
|
|
if (affectedTests.length > 0) {
|
|
return siblingTest ? uniqueOrdered([siblingTest, ...affectedTests]) : affectedTests;
|
|
}
|
|
}
|
|
return siblingTest ? [siblingTest] : null;
|
|
}
|
|
|
|
function isDeletedChangedTestTarget(changedPath, 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 &&
|
|
options.includeExtensionImpact !== false &&
|
|
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 (isAgentsCoreIsolatedTestFile(relative)) {
|
|
return agentVitestProjectOwners.coreIsolated.kind;
|
|
}
|
|
if (isControlUiE2eTarget(relative)) {
|
|
return "uiE2e";
|
|
}
|
|
if (isUiIsolatedTestFile(relative)) {
|
|
return "uiIsolated";
|
|
}
|
|
if (isPathAtOrUnder(relative, "ui/src")) {
|
|
return "ui";
|
|
}
|
|
if (relative.startsWith("src/tui/tui-pty-")) {
|
|
return "tuiPty";
|
|
}
|
|
if (relative.endsWith(".e2e.test.ts")) {
|
|
return "e2e";
|
|
}
|
|
if (
|
|
relative === "src/gateway/gateway.test.ts" ||
|
|
relative === "src/gateway/server.startup-matrix-migration.integration.test.ts" ||
|
|
relative === "src/gateway/sessions-history-http.test.ts"
|
|
) {
|
|
return "e2e";
|
|
}
|
|
const channelContractKind = resolveChannelContractTargetKind(relative);
|
|
if (channelContractKind) {
|
|
return channelContractKind;
|
|
}
|
|
if (relative.startsWith("src/plugins/contracts/")) {
|
|
return "contractsPlugin";
|
|
}
|
|
if (resolveUnitFastTimerTestIncludePattern(relative)) {
|
|
return "unitFastFakeTimers";
|
|
}
|
|
if (resolveUnitFastIsolatedTestIncludePattern(relative)) {
|
|
return "unitFastIsolated";
|
|
}
|
|
if (resolveUnitFastTestIncludePattern(relative)) {
|
|
return "unitFast";
|
|
}
|
|
if (relative === "extensions") {
|
|
return "extensionFull";
|
|
}
|
|
if (getChangedPathFacts(relative).surface === "extension") {
|
|
const extensionRoot = relative.split("/").slice(0, 2).join("/");
|
|
const splitChannelShard = resolveSplitChannelExtensionShard(extensionRoot);
|
|
if (splitChannelShard) {
|
|
return splitChannelShard.kind;
|
|
}
|
|
if (isProviderOpenAiExtensionRoot(extensionRoot)) {
|
|
return "extensionProviderOpenAi";
|
|
}
|
|
if (isQaExtensionRoot(extensionRoot)) {
|
|
return "extensionQa";
|
|
}
|
|
if (isChannelSurfaceTestFile(relative)) {
|
|
return "extensionChannel";
|
|
}
|
|
if (isAcpxExtensionRoot(extensionRoot)) {
|
|
return "extensionAcpx";
|
|
}
|
|
if (isActiveMemoryExtensionRoot(extensionRoot)) {
|
|
return "extensionActiveMemory";
|
|
}
|
|
if (isCodexExtensionRoot(extensionRoot)) {
|
|
return "extensionCodex";
|
|
}
|
|
if (isDiffsExtensionRoot(extensionRoot)) {
|
|
return "extensionDiffs";
|
|
}
|
|
if (isBrowserExtensionRoot(extensionRoot)) {
|
|
return "extensionBrowser";
|
|
}
|
|
if (isFeishuExtensionRoot(extensionRoot)) {
|
|
return "extensionFeishu";
|
|
}
|
|
if (isIrcExtensionRoot(extensionRoot)) {
|
|
return "extensionIrc";
|
|
}
|
|
if (isMattermostExtensionRoot(extensionRoot)) {
|
|
return "extensionMattermost";
|
|
}
|
|
if (isTelegramExtensionRoot(extensionRoot)) {
|
|
return "extensionTelegram";
|
|
}
|
|
if (isVoiceCallExtensionRoot(extensionRoot)) {
|
|
return "extensionVoiceCall";
|
|
}
|
|
if (isWhatsAppExtensionRoot(extensionRoot)) {
|
|
return "extensionWhatsApp";
|
|
}
|
|
if (isZaloExtensionRoot(extensionRoot)) {
|
|
return "extensionZalo";
|
|
}
|
|
if (isMatrixExtensionRoot(extensionRoot)) {
|
|
return "extensionMatrix";
|
|
}
|
|
if (isMediaExtensionRoot(extensionRoot)) {
|
|
return "extensionMedia";
|
|
}
|
|
if (isMemoryExtensionRoot(extensionRoot)) {
|
|
return "extensionMemory";
|
|
}
|
|
if (isMsTeamsExtensionRoot(extensionRoot)) {
|
|
return "extensionMsTeams";
|
|
}
|
|
if (isMessagingExtensionRoot(extensionRoot)) {
|
|
return "extensionMessaging";
|
|
}
|
|
if (isMiscExtensionRoot(extensionRoot)) {
|
|
return "extensionMisc";
|
|
}
|
|
return isProviderExtensionRoot(extensionRoot) ? "extensionProvider" : "extension";
|
|
}
|
|
if (isChannelSurfaceTestFile(relative)) {
|
|
return "channel";
|
|
}
|
|
if (isBoundaryTestFile(relative)) {
|
|
return "boundary";
|
|
}
|
|
if (isToolingIsolatedTestFile(relative)) {
|
|
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, "packages/gateway-client") ||
|
|
isPathAtOrUnder(relative, "packages/gateway-protocol")
|
|
) {
|
|
return "gatewayClient";
|
|
}
|
|
if (isPathAtOrUnder(relative, "src/hooks")) {
|
|
return "hooks";
|
|
}
|
|
if (isPathAtOrUnder(relative, "src/infra")) {
|
|
return "infra";
|
|
}
|
|
if (isPathAtOrUnder(relative, "src/config")) {
|
|
return "runtimeConfig";
|
|
}
|
|
if (isPathAtOrUnder(relative, "src/cron")) {
|
|
return "cron";
|
|
}
|
|
if (isPathAtOrUnder(relative, "src/daemon")) {
|
|
return "daemon";
|
|
}
|
|
if (isPathAtOrUnder(relative, "src/media-understanding")) {
|
|
return "mediaUnderstanding";
|
|
}
|
|
if (isPathAtOrUnder(relative, "src/media")) {
|
|
return "media";
|
|
}
|
|
if (isPathAtOrUnder(relative, "src/logging")) {
|
|
return "logging";
|
|
}
|
|
if (isPathAtOrUnder(relative, "src/plugin-sdk")) {
|
|
return isPluginSdkLightTarget(relative) ? "pluginSdkLight" : "pluginSdk";
|
|
}
|
|
if (isPathAtOrUnder(relative, "src/process")) {
|
|
return "process";
|
|
}
|
|
if (isPathAtOrUnder(relative, "src/secrets")) {
|
|
return "secrets";
|
|
}
|
|
if (isPathAtOrUnder(relative, "src/shared")) {
|
|
return "sharedCore";
|
|
}
|
|
if (isPathAtOrUnder(relative, "src/tasks")) {
|
|
return "tasks";
|
|
}
|
|
if (isPathAtOrUnder(relative, "src/tui")) {
|
|
return "tui";
|
|
}
|
|
if (isPathAtOrUnder(relative, "src/acp")) {
|
|
return "acp";
|
|
}
|
|
if (isCliProcessTestFile(relative)) {
|
|
return "cliProcess";
|
|
}
|
|
if (isPathAtOrUnder(relative, "src/cli")) {
|
|
return "cli";
|
|
}
|
|
if (isPathAtOrUnder(relative, "src/commands")) {
|
|
return isCommandsLightTarget(relative) ? "commandLight" : "command";
|
|
}
|
|
if (isPathAtOrUnder(relative, "src/auto-reply")) {
|
|
return "autoReply";
|
|
}
|
|
if (isPathAtOrUnder(relative, agentVitestProjectOwners.all.root)) {
|
|
// Focused runs must preserve the full suite's isolated harness and hook-timeout contracts.
|
|
if (
|
|
relative === agentVitestProjectOwners.all.root ||
|
|
relative === AGENTS_EMBEDDED_AGENT_TEST_ROOT
|
|
) {
|
|
return agentVitestProjectOwners.all.kind;
|
|
}
|
|
if (agentVitestProjectOwners.embeddedIncompleteTurn.include.includes(relative)) {
|
|
return agentVitestProjectOwners.embeddedIncompleteTurn.kind;
|
|
}
|
|
if (agentVitestProjectOwners.embeddedOverflowCompaction.include.includes(relative)) {
|
|
return agentVitestProjectOwners.embeddedOverflowCompaction.kind;
|
|
}
|
|
if (isPathAtOrUnder(relative, agentVitestProjectOwners.embeddedRun.root)) {
|
|
return agentVitestProjectOwners.embeddedRun.kind;
|
|
}
|
|
if (isPathAtOrUnder(relative, AGENTS_EMBEDDED_AGENT_TEST_ROOT)) {
|
|
return isGlobTarget(relative)
|
|
? agentVitestProjectOwners.all.kind
|
|
: agentVitestProjectOwners.embedded.kind;
|
|
}
|
|
if (isPathAtOrUnder(relative, agentVitestProjectOwners.tools.root)) {
|
|
return agentVitestProjectOwners.tools.kind;
|
|
}
|
|
if (isGlobTarget(relative)) {
|
|
const owner = relative.slice(agentVitestProjectOwners.all.root.length + 1).split("/", 1)[0];
|
|
return isGlobTarget(owner)
|
|
? agentVitestProjectOwners.all.kind
|
|
: agentVitestProjectOwners.support.kind;
|
|
}
|
|
return isFileLikeTarget(relative) &&
|
|
path.posix.dirname(relative) === agentVitestProjectOwners.core.root
|
|
? agentVitestProjectOwners.core.kind
|
|
: agentVitestProjectOwners.support.kind;
|
|
}
|
|
if (isPathAtOrUnder(relative, "src/plugins")) {
|
|
return "plugin";
|
|
}
|
|
if (isPathAtOrUnder(relative, "src/utils")) {
|
|
return "utils";
|
|
}
|
|
if (isPathAtOrUnder(relative, "src/wizard")) {
|
|
return "wizard";
|
|
}
|
|
return "default";
|
|
}
|
|
|
|
function resolveLightLaneIncludePatterns(kind, 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 === "unitFastIsolated") {
|
|
const includePattern = resolveUnitFastIsolatedTestIncludePattern(relative);
|
|
return includePattern ? [includePattern] : null;
|
|
}
|
|
if (kind === "pluginSdkLight") {
|
|
const includePattern = resolvePluginSdkLightIncludePattern(relative);
|
|
return includePattern ? [includePattern] : null;
|
|
}
|
|
if (kind === "commandLight") {
|
|
const includePattern = resolveCommandsLightIncludePattern(relative);
|
|
return includePattern ? [includePattern] : null;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function shouldUseWholeConfigTarget(kind, 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/");
|
|
}
|
|
|
|
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 createVitestPreflightPnpmArgs(config) {
|
|
if (config !== UI_E2E_VITEST_CONFIG) {
|
|
return null;
|
|
}
|
|
return ["exec", "node", "scripts/ensure-playwright-chromium.mjs"];
|
|
}
|
|
|
|
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 = expandBroadToolingScriptTargets(
|
|
expandExplicitSourceTestTargets(requestedTargetArgs, cwd),
|
|
cwd,
|
|
watchMode,
|
|
);
|
|
const activeForwardedArgs =
|
|
changedTargetArgs !== null ? stripChangedArgs(forwardedArgs) : forwardedArgs;
|
|
if (changedTargetArgs !== null && activeTargetArgs.length === 0) {
|
|
return [];
|
|
}
|
|
if (activeTargetArgs.length === 0) {
|
|
return [
|
|
{
|
|
config: DEFAULT_VITEST_CONFIG,
|
|
forwardedArgs: activeForwardedArgs,
|
|
includePatterns: null,
|
|
watchMode,
|
|
},
|
|
];
|
|
}
|
|
|
|
const nonTargetArgs = activeForwardedArgs.filter((arg) => !requestedTargetArgs.includes(arg));
|
|
const explicitConfigTargets = activeTargetArgs.map((targetArg) =>
|
|
toRepoRelativeTarget(targetArg, cwd),
|
|
);
|
|
if (explicitConfigTargets.every(isVitestConfigFileTarget)) {
|
|
if (watchMode && explicitConfigTargets.length > 1) {
|
|
throw new Error(
|
|
"watch mode with mixed test suites is not supported; target one suite at a time or use a dedicated suite command",
|
|
);
|
|
}
|
|
return explicitConfigTargets.map((config) => ({
|
|
config,
|
|
forwardedArgs: nonTargetArgs,
|
|
includePatterns: null,
|
|
watchMode,
|
|
}));
|
|
}
|
|
|
|
const groupedTargets = new Map();
|
|
for (const targetArg of activeTargetArgs) {
|
|
if (!watchMode && toRepoRelativeTarget(targetArg, cwd) === AGENTS_EMBEDDED_AGENT_TEST_ROOT) {
|
|
// The recursive parent spans four harness owners; keep every isolated project intact.
|
|
for (const { kind, include: targets } of embeddedAgentVitestProjectOwners) {
|
|
const current = groupedTargets.get(kind) ?? [];
|
|
for (const target of targets) {
|
|
if (!current.includes(target)) {
|
|
current.push(target);
|
|
}
|
|
}
|
|
groupedTargets.set(kind, current);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
const kind = classifyTarget(targetArg, cwd);
|
|
const current = groupedTargets.get(kind) ?? [];
|
|
current.push(targetArg);
|
|
groupedTargets.set(kind, current);
|
|
}
|
|
const toolingTargets = groupedTargets.get("tooling") ?? [];
|
|
if (
|
|
!watchMode &&
|
|
toolingTargets.some((targetArg) =>
|
|
includePatternMatchesAnyFile(toScopedIncludePattern(targetArg, cwd), [
|
|
TOOLING_DOCKER_TEST_TARGET,
|
|
]),
|
|
)
|
|
) {
|
|
const current = groupedTargets.get("toolingDocker") ?? [];
|
|
if (!current.includes(TOOLING_DOCKER_TEST_TARGET)) {
|
|
current.push(TOOLING_DOCKER_TEST_TARGET);
|
|
groupedTargets.set("toolingDocker", current);
|
|
}
|
|
}
|
|
const impliedToolingIsolatedTargets = !watchMode
|
|
? toolingIsolatedTestFiles.filter((file) =>
|
|
toolingTargets.some((targetArg) =>
|
|
includePatternMatchesAnyFile(toScopedIncludePattern(targetArg, cwd), [file]),
|
|
),
|
|
)
|
|
: [];
|
|
if (impliedToolingIsolatedTargets.length > 0) {
|
|
const current = groupedTargets.get("toolingIsolated") ?? [];
|
|
for (const target of impliedToolingIsolatedTargets) {
|
|
if (!current.includes(target)) {
|
|
current.push(target);
|
|
}
|
|
}
|
|
groupedTargets.set("toolingIsolated", current);
|
|
}
|
|
const uiTargets = groupedTargets.get("ui") ?? [];
|
|
const impliedUiIsolatedTargets = uiIsolatedTestFiles.filter((file) =>
|
|
uiTargets.some((targetArg) =>
|
|
includePatternMatchesAnyFile(toScopedIncludePattern(targetArg, cwd), [file]),
|
|
),
|
|
);
|
|
if (impliedUiIsolatedTargets.length > 0) {
|
|
const current = groupedTargets.get("uiIsolated") ?? [];
|
|
for (const target of impliedUiIsolatedTargets) {
|
|
if (!current.includes(target)) {
|
|
current.push(target);
|
|
}
|
|
}
|
|
groupedTargets.set("uiIsolated", current);
|
|
}
|
|
const cliTargets = groupedTargets.get("cli") ?? [];
|
|
const impliedCliProcessTargets = cliProcessTestFiles.filter((file) =>
|
|
cliTargets.some((targetArg) =>
|
|
includePatternMatchesAnyFile(toScopedIncludePattern(targetArg, cwd), [file]),
|
|
),
|
|
);
|
|
if (impliedCliProcessTargets.length > 0) {
|
|
const current = groupedTargets.get("cliProcess") ?? [];
|
|
for (const target of impliedCliProcessTargets) {
|
|
if (!current.includes(target)) {
|
|
current.push(target);
|
|
}
|
|
}
|
|
groupedTargets.set("cliProcess", current);
|
|
}
|
|
|
|
if (watchMode && groupedTargets.size > 1) {
|
|
throw new Error(
|
|
"watch mode with mixed test suites is not supported; target one suite at a time or use a dedicated suite command",
|
|
);
|
|
}
|
|
|
|
const orderedKinds = Object.keys(VITEST_CONFIG_BY_KIND);
|
|
orderedKinds.splice(orderedKinds.indexOf("boundary"), 0, "default");
|
|
const plans = [];
|
|
for (const kind of orderedKinds) {
|
|
const grouped = groupedTargets.get(kind);
|
|
if (!grouped || grouped.length === 0) {
|
|
continue;
|
|
}
|
|
if (kind === "extensionFull") {
|
|
const configs = watchMode
|
|
? [FULL_EXTENSIONS_VITEST_CONFIG]
|
|
: listFullExtensionVitestProjectConfigs();
|
|
for (const config of configs) {
|
|
const plan = {
|
|
config,
|
|
forwardedArgs: nonTargetArgs,
|
|
includePatterns: null,
|
|
watchMode,
|
|
};
|
|
const boundedPlans = createBoundedExtensionPlans({
|
|
config,
|
|
forwardedArgs: nonTargetArgs,
|
|
roots: matrixExtensionTestRoots,
|
|
watchMode,
|
|
});
|
|
plans.push(...(boundedPlans ?? [plan]));
|
|
}
|
|
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) : [];
|
|
const forwardedPlanArgs = [...nonTargetArgs, ...scopedTargetArgs];
|
|
const broadToolingScriptPlans = createBroadToolingScriptPlans({
|
|
config,
|
|
cwd,
|
|
forwardedArgs: forwardedPlanArgs,
|
|
includePatterns,
|
|
watchMode,
|
|
});
|
|
if (broadToolingScriptPlans) {
|
|
plans.push(...broadToolingScriptPlans);
|
|
continue;
|
|
}
|
|
const boundedExtensionRoots = grouped.flatMap((targetArg) => {
|
|
const root = toRepoRelativeTarget(targetArg, cwd);
|
|
return isMatrixExtensionRoot(root) && isExistingDirectoryTarget(targetArg, cwd) ? [root] : [];
|
|
});
|
|
const boundedRootsCoverGroupedTargets = grouped.every((targetArg) => {
|
|
const relativeTarget = toRepoRelativeTarget(targetArg, cwd);
|
|
return boundedExtensionRoots.some(
|
|
(root) => relativeTarget === root || relativeTarget.startsWith(`${root}/`),
|
|
);
|
|
});
|
|
const boundedExtensionPlans =
|
|
boundedExtensionRoots.length > 0 && boundedRootsCoverGroupedTargets
|
|
? createBoundedExtensionPlans({
|
|
config,
|
|
forwardedArgs: forwardedPlanArgs,
|
|
roots: boundedExtensionRoots,
|
|
watchMode,
|
|
})
|
|
: null;
|
|
if (boundedExtensionPlans) {
|
|
plans.push(...boundedExtensionPlans);
|
|
continue;
|
|
}
|
|
plans.push({
|
|
config,
|
|
forwardedArgs: forwardedPlanArgs,
|
|
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 [];
|
|
}
|
|
// The remote Testbox full gate runs every agentic and extension project in one process tree.
|
|
// Bound project and worker lifetimes before either aggregate reaches V8's heap limit.
|
|
const expandShard =
|
|
expandToProjectConfigs ||
|
|
(process.env.OPENCLAW_TESTBOX_REMOTE_RUN === "1" &&
|
|
(shard.config === FULL_AGENTIC_VITEST_CONFIG ||
|
|
shard.config === FULL_EXTENSIONS_VITEST_CONFIG));
|
|
const configs = expandShard ? shard.projects : [shard.config];
|
|
return configs.flatMap((config) => {
|
|
if (expandShard && targetArgs.length === 0) {
|
|
let chunks = [];
|
|
if (config === AGENTS_CORE_VITEST_CONFIG) {
|
|
// A single non-isolated agents-core process grows until its worker can
|
|
// exit under the full-suite memory load. Bound each process lifetime.
|
|
chunks = splitTargetChunks(
|
|
listAgentsCoreFullSuiteTestTargets(cwd),
|
|
FULL_SUITE_AGENTS_CORE_TEST_TARGET_CHUNK_COUNT,
|
|
);
|
|
} else if (config === UNIT_FAST_VITEST_CONFIG) {
|
|
const targets = listUnitFastFullSuiteTestTargets();
|
|
const chunkCount = Math.ceil(
|
|
targets.length / FULL_SUITE_UNIT_FAST_TEST_TARGET_CHUNK_SIZE,
|
|
);
|
|
chunks = splitTargetChunks(targets, chunkCount);
|
|
} else if (config === TOOLING_VITEST_CONFIG) {
|
|
// Tooling tests spawn package managers and native helpers. Keep native
|
|
// process lifetime short enough that unrelated files cannot crash together.
|
|
const targets = listToolingFullSuiteTestTargets(cwd);
|
|
const chunkCount = Math.ceil(targets.length / FULL_SUITE_TOOLING_TEST_TARGET_CHUNK_SIZE);
|
|
chunks = splitTargetChunks(targets, chunkCount);
|
|
} else if (config === GATEWAY_SERVER_VITEST_CONFIG) {
|
|
chunks = splitTargetChunks(
|
|
resolveGatewayServerFullSuiteTargets(cwd),
|
|
GATEWAY_SERVER_FULL_SUITE_TARGET_CHUNK_COUNT,
|
|
);
|
|
} else if (config === EXTENSION_MATRIX_VITEST_CONFIG) {
|
|
chunks = createExtensionTestProcessTargetChunks(
|
|
config,
|
|
matrixExtensionTestRoots,
|
|
forwardedArgs,
|
|
);
|
|
}
|
|
if (chunks.length > 0) {
|
|
return chunks.map((targets) => ({
|
|
config,
|
|
forwardedArgs: [...forwardedArgs, ...targets],
|
|
includePatterns: null,
|
|
watchMode: false,
|
|
}));
|
|
}
|
|
}
|
|
return [
|
|
{
|
|
config,
|
|
forwardedArgs,
|
|
includePatterns: null,
|
|
watchMode: false,
|
|
},
|
|
];
|
|
});
|
|
});
|
|
}
|
|
|
|
function shouldUseLocalFullSuiteParallelByDefault(env = process.env) {
|
|
if (hasConservativeVitestWorkerBudget(env)) {
|
|
return false;
|
|
}
|
|
return env.OPENCLAW_TEST_PROJECTS_SERIAL !== "1" && !isCiLikeEnv(env);
|
|
}
|
|
|
|
function shouldExpandLocalFullSuiteShardsByDefault(env = process.env) {
|
|
return !isCiLikeEnv(env);
|
|
}
|
|
|
|
function parsePositiveInt(value, 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;
|
|
}
|
|
|
|
const FULL_EXTENSIONS_CONFIG = "test/vitest/vitest.full-extensions.config.ts";
|
|
const FULL_EXTENSIONS_MIN_HEAP_MB = 8192;
|
|
|
|
function ensureMaxOldSpaceSize(nodeOptions, minimumMb) {
|
|
const normalized = nodeOptions?.trim() ?? "";
|
|
const matches = Array.from(
|
|
normalized.matchAll(/(^|\s)--max[-_]old[-_]space[-_]size(?:=|\s+)(\d+)(?=\s|$)/gu),
|
|
);
|
|
const match = matches.at(-1);
|
|
if (!match) {
|
|
return [normalized, `--max-old-space-size=${minimumMb}`].filter(Boolean).join(" ");
|
|
}
|
|
const currentMb = Number(match[2]);
|
|
if (Number.isSafeInteger(currentMb) && currentMb >= minimumMb) {
|
|
return normalized;
|
|
}
|
|
const start = match.index;
|
|
const replacement = match[0].replace(/\d+$/u, String(minimumMb));
|
|
return `${normalized.slice(0, start)}${replacement}${normalized.slice(start + match[0].length)}`;
|
|
}
|
|
|
|
export function applyFullExtensionsHeapBudget(specs, params = {}) {
|
|
const baseEnv = params.env ?? {};
|
|
return specs.map((spec) =>
|
|
spec.config === FULL_EXTENSIONS_CONFIG
|
|
? {
|
|
...spec,
|
|
env: {
|
|
...spec.env,
|
|
NODE_OPTIONS: ensureMaxOldSpaceSize(
|
|
spec.env?.NODE_OPTIONS ?? baseEnv.NODE_OPTIONS,
|
|
FULL_EXTENSIONS_MIN_HEAP_MB,
|
|
),
|
|
},
|
|
}
|
|
: spec,
|
|
);
|
|
}
|
|
|
|
export function resolveParallelFullSuiteConcurrency(specCount, 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;
|
|
const cwd = params.cwd ?? process.cwd();
|
|
const configuredCacheRoot = baseEnv[FS_MODULE_CACHE_PATH_ENV_KEY]?.trim() || undefined;
|
|
// CI publishes a persistent cache root, not a writer-safe leaf. Every
|
|
// concurrent Vitest process still needs its own live directory below it.
|
|
const cacheRoot =
|
|
configuredCacheRoot ?? path.join(cwd, "node_modules", ".experimental-vitest-cache");
|
|
return specs.map((spec, index) => {
|
|
const specCachePath = spec.env?.[FS_MODULE_CACHE_PATH_ENV_KEY]?.trim();
|
|
if (specCachePath && specCachePath !== configuredCacheRoot) {
|
|
return spec;
|
|
}
|
|
const cacheSegment = sanitizeVitestCachePathSegment(`${index}-${spec.config}`);
|
|
return {
|
|
...spec,
|
|
env: {
|
|
...spec.env,
|
|
[FS_MODULE_CACHE_PATH_ENV_KEY]: path.join(cacheRoot, cacheSegment),
|
|
},
|
|
};
|
|
});
|
|
}
|
|
|
|
export function applyDefaultMultiSpecVitestCachePaths(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 ?? "");
|
|
}
|
|
|
|
// Shards may pin a short no-output window so the known warm-cache stall dies
|
|
// fast (see AGENTS_CORE_RUNTIME_ENV in ci-node-test-plan.mjs). A cold Vitest
|
|
// module cache makes those same imports legitimately silent for minutes, so
|
|
// the retry attempt must get the full watchdog window or it re-dies at the
|
|
// short limit and the job fails without ever running a test.
|
|
const RETRY_NO_OUTPUT_TIMEOUT_FLOOR_MS = 300_000;
|
|
|
|
export function withRetryNoOutputTimeout(spec) {
|
|
const current = Number(spec.env?.[VITEST_NO_OUTPUT_TIMEOUT_ENV_KEY]);
|
|
if (!Number.isFinite(current) || current <= 0 || current >= RETRY_NO_OUTPUT_TIMEOUT_FLOOR_MS) {
|
|
return spec;
|
|
}
|
|
return {
|
|
...spec,
|
|
env: {
|
|
...spec.env,
|
|
[VITEST_NO_OUTPUT_TIMEOUT_ENV_KEY]: String(RETRY_NO_OUTPUT_TIMEOUT_FLOOR_MS),
|
|
},
|
|
};
|
|
}
|
|
|
|
export function createVitestRunSpecs(args, 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),
|
|
preflightPnpmArgs: createVitestPreflightPnpmArgs(plan.config),
|
|
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
|
|
);
|
|
}
|
|
|
|
function expandVitestIncludePatterns(includePatterns, cwd) {
|
|
const candidateFiles = includePatterns.some(isGlobTarget)
|
|
? listExplicitTestTargetFilesForCwd(cwd)
|
|
: [];
|
|
return uniqueOrdered(
|
|
includePatterns.flatMap((pattern) => {
|
|
if (!isGlobTarget(pattern)) {
|
|
return [pattern];
|
|
}
|
|
return candidateFiles.filter((file) => path.matchesGlob(file, pattern));
|
|
}),
|
|
);
|
|
}
|
|
|
|
export function writeVitestIncludeFile(filePath, includePatterns, options = {}) {
|
|
// Shared Vitest projects intersect this file with their ownership globs.
|
|
// One-shot runs emit concrete paths; watch runs retain globs for new files.
|
|
const expandedPatterns =
|
|
options.expandGlobs === false
|
|
? includePatterns
|
|
: expandVitestIncludePatterns(includePatterns, options.cwd ?? process.cwd());
|
|
fs.writeFileSync(filePath, `${JSON.stringify(expandedPatterns, null, 2)}\n`);
|
|
}
|
|
|
|
function shellQuote(value) {
|
|
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);
|
|
}
|