test: split heavy extension test shards

This commit is contained in:
Peter Steinberger
2026-04-20 16:49:24 +01:00
parent 68b7666d7c
commit 0603ceba23
26 changed files with 503 additions and 104 deletions

View File

@@ -3,6 +3,7 @@ import path from "node:path";
import { channelTestRoots } from "../../test/vitest/vitest.channel-paths.mjs";
import { isAcpxExtensionRoot } from "../../test/vitest/vitest.extension-acpx-paths.mjs";
import { isBlueBubblesExtensionRoot } from "../../test/vitest/vitest.extension-bluebubbles-paths.mjs";
import { resolveSplitChannelExtensionShard } from "../../test/vitest/vitest.extension-channel-split-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";
@@ -11,7 +12,10 @@ import { isMattermostExtensionRoot } from "../../test/vitest/vitest.extension-ma
import { isMemoryExtensionRoot } from "../../test/vitest/vitest.extension-memory-paths.mjs";
import { isMessagingExtensionRoot } from "../../test/vitest/vitest.extension-messaging-paths.mjs";
import { isMsTeamsExtensionRoot } from "../../test/vitest/vitest.extension-msteams-paths.mjs";
import { isProviderExtensionRoot } from "../../test/vitest/vitest.extension-provider-paths.mjs";
import {
isProviderExtensionRoot,
isProviderOpenAiExtensionRoot,
} from "../../test/vitest/vitest.extension-provider-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";
@@ -111,6 +115,7 @@ export function resolveExtensionTestPlan(params = {}) {
const roots = [relativeExtensionDir];
const splitChannelShard = resolveSplitChannelExtensionShard(relativeExtensionDir);
const usesChannelConfig = roots.some((root) => channelTestRoots.includes(root));
const usesAcpxConfig = roots.some((root) => isAcpxExtensionRoot(root));
const usesDiffsConfig = roots.some((root) => isDiffsExtensionRoot(root));
@@ -126,40 +131,45 @@ export function resolveExtensionTestPlan(params = {}) {
const usesMemoryConfig = roots.some((root) => isMemoryExtensionRoot(root));
const usesMsTeamsConfig = roots.some((root) => isMsTeamsExtensionRoot(root));
const usesMessagingConfig = roots.some((root) => isMessagingExtensionRoot(root));
const usesProviderOpenAiConfig = roots.some((root) => isProviderOpenAiExtensionRoot(root));
const usesProviderConfig = roots.some((root) => isProviderExtensionRoot(root));
const config = usesChannelConfig
? "test/vitest/vitest.extension-channels.config.ts"
: usesAcpxConfig
? "test/vitest/vitest.extension-acpx.config.ts"
: usesDiffsConfig
? "test/vitest/vitest.extension-diffs.config.ts"
: usesBlueBubblesConfig
? "test/vitest/vitest.extension-bluebubbles.config.ts"
: usesFeishuConfig
? "test/vitest/vitest.extension-feishu.config.ts"
: usesIrcConfig
? "test/vitest/vitest.extension-irc.config.ts"
: usesMattermostConfig
? "test/vitest/vitest.extension-mattermost.config.ts"
: usesMatrixConfig
? "test/vitest/vitest.extension-matrix.config.ts"
: usesTelegramConfig
? "test/vitest/vitest.extension-telegram.config.ts"
: usesVoiceCallConfig
? "test/vitest/vitest.extension-voice-call.config.ts"
: usesWhatsAppConfig
? "test/vitest/vitest.extension-whatsapp.config.ts"
: usesZaloConfig
? "test/vitest/vitest.extension-zalo.config.ts"
: usesMemoryConfig
? "test/vitest/vitest.extension-memory.config.ts"
: usesMsTeamsConfig
? "test/vitest/vitest.extension-msteams.config.ts"
: usesMessagingConfig
? "test/vitest/vitest.extension-messaging.config.ts"
: usesProviderConfig
? "test/vitest/vitest.extension-providers.config.ts"
: "test/vitest/vitest.extensions.config.ts";
const config = splitChannelShard
? splitChannelShard.config
: usesChannelConfig
? "test/vitest/vitest.extension-channels.config.ts"
: usesAcpxConfig
? "test/vitest/vitest.extension-acpx.config.ts"
: usesDiffsConfig
? "test/vitest/vitest.extension-diffs.config.ts"
: usesBlueBubblesConfig
? "test/vitest/vitest.extension-bluebubbles.config.ts"
: usesFeishuConfig
? "test/vitest/vitest.extension-feishu.config.ts"
: usesIrcConfig
? "test/vitest/vitest.extension-irc.config.ts"
: usesMattermostConfig
? "test/vitest/vitest.extension-mattermost.config.ts"
: usesMatrixConfig
? "test/vitest/vitest.extension-matrix.config.ts"
: usesTelegramConfig
? "test/vitest/vitest.extension-telegram.config.ts"
: usesVoiceCallConfig
? "test/vitest/vitest.extension-voice-call.config.ts"
: usesWhatsAppConfig
? "test/vitest/vitest.extension-whatsapp.config.ts"
: usesZaloConfig
? "test/vitest/vitest.extension-zalo.config.ts"
: usesMemoryConfig
? "test/vitest/vitest.extension-memory.config.ts"
: usesMsTeamsConfig
? "test/vitest/vitest.extension-msteams.config.ts"
: usesMessagingConfig
? "test/vitest/vitest.extension-messaging.config.ts"
: usesProviderOpenAiConfig
? "test/vitest/vitest.extension-provider-openai.config.ts"
: usesProviderConfig
? "test/vitest/vitest.extension-providers.config.ts"
: "test/vitest/vitest.extensions.config.ts";
const testFileCount = roots.reduce(
(sum, root) => sum + countTestFiles(path.join(repoRoot, root)),
0,

View File

@@ -11,6 +11,7 @@ import {
applyParallelVitestCachePaths,
buildFullSuiteVitestRunPlans,
createVitestRunSpecs,
listFullExtensionVitestProjectConfigs,
parseTestProjectsArgs,
resolveParallelFullSuiteConcurrency,
resolveChangedTargetArgs,
@@ -33,7 +34,7 @@ const FULL_SUITE_CONFIG_WEIGHT = new Map([
["test/vitest/vitest.agents.config.ts", 170],
["test/vitest/vitest.extension-voice-call.config.ts", 169],
["test/vitest/vitest.extensions.config.ts", 168],
["test/vitest/vitest.extension-channels.config.ts", 167],
["test/vitest/vitest.extension-provider-openai.config.ts", 167],
["test/vitest/vitest.runtime-config.config.ts", 166],
["test/vitest/vitest.contracts.config.ts", 165],
["test/vitest/vitest.tasks.config.ts", 165],
@@ -46,6 +47,7 @@ const FULL_SUITE_CONFIG_WEIGHT = new Map([
["test/vitest/vitest.wizard.config.ts", 130],
["test/vitest/vitest.unit-src.config.ts", 125],
["test/vitest/vitest.extension-matrix.config.ts", 100],
["test/vitest/vitest.extension-discord.config.ts", 98],
["test/vitest/vitest.extension-providers.config.ts", 96],
["test/vitest/vitest.extension-telegram.config.ts", 94],
["test/vitest/vitest.extension-whatsapp.config.ts", 92],
@@ -54,6 +56,7 @@ const FULL_SUITE_CONFIG_WEIGHT = new Map([
["test/vitest/vitest.media.config.ts", 84],
["test/vitest/vitest.plugins.config.ts", 82],
["test/vitest/vitest.bundled.config.ts", 80],
["test/vitest/vitest.extension-slack.config.ts", 78],
["test/vitest/vitest.commands-light.config.ts", 48],
["test/vitest/vitest.plugin-sdk.config.ts", 46],
["test/vitest/vitest.auto-reply-top-level.config.ts", 45],
@@ -70,6 +73,9 @@ const FULL_SUITE_CONFIG_WEIGHT = new Map([
["test/vitest/vitest.extension-feishu.config.ts", 18],
["test/vitest/vitest.extension-mattermost.config.ts", 16],
["test/vitest/vitest.extension-messaging.config.ts", 14],
["test/vitest/vitest.extension-imessage.config.ts", 13],
["test/vitest/vitest.extension-line.config.ts", 12],
["test/vitest/vitest.extension-signal.config.ts", 11],
["test/vitest/vitest.extension-acpx.config.ts", 10],
["test/vitest/vitest.extension-diffs.config.ts", 8],
["test/vitest/vitest.extension-memory.config.ts", 6],
@@ -160,6 +166,19 @@ function orderFullSuiteSpecsForParallelRun(specs) {
});
}
function isFullExtensionsProjectRun(specs) {
const fullExtensionProjectConfigs = new Set(listFullExtensionVitestProjectConfigs());
return (
specs.length > 1 &&
specs.every(
(spec) =>
spec.watchMode === false &&
spec.includePatterns === null &&
fullExtensionProjectConfigs.has(spec.config),
)
);
}
async function runVitestSpecsParallel(specs, concurrency) {
let nextIndex = 0;
let exitCode = 0;
@@ -216,6 +235,11 @@ async function main() {
cwd: process.cwd(),
});
if (runSpecs.length === 0) {
console.error("[test] no changed test targets; skipping Vitest.");
return;
}
releaseLock = shouldAcquireLocalHeavyCheckLock(runSpecs, process.env)
? acquireLocalHeavyCheckLockSync({
cwd: process.cwd(),
@@ -228,7 +252,8 @@ async function main() {
targetArgs.length === 0 &&
changedTargetArgs === null &&
!runSpecs.some((spec) => spec.watchMode);
if (isFullSuiteRun) {
const isParallelShardRun = isFullSuiteRun || isFullExtensionsProjectRun(runSpecs);
if (isParallelShardRun) {
const concurrency = resolveParallelFullSuiteConcurrency(runSpecs.length, process.env);
if (concurrency > 1) {
const localFullSuiteProfile = resolveLocalFullSuiteProfile(process.env);

View File

@@ -35,6 +35,8 @@ export function resolveChangedTargetArgs(
listChangedPaths?: (baseRef: string, cwd: string) => string[],
): string[] | null;
export function listFullExtensionVitestProjectConfigs(): string[];
export function createVitestRunSpecs(
args: string[],
params?: {

View File

@@ -9,6 +9,7 @@ import {
} from "../test/vitest/vitest.commands-light-paths.mjs";
import { isAcpxExtensionRoot } from "../test/vitest/vitest.extension-acpx-paths.mjs";
import { isBlueBubblesExtensionRoot } from "../test/vitest/vitest.extension-bluebubbles-paths.mjs";
import { resolveSplitChannelExtensionShard } from "../test/vitest/vitest.extension-channel-split-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";
@@ -17,7 +18,10 @@ import { isMattermostExtensionRoot } from "../test/vitest/vitest.extension-matte
import { isMemoryExtensionRoot } from "../test/vitest/vitest.extension-memory-paths.mjs";
import { isMessagingExtensionRoot } from "../test/vitest/vitest.extension-messaging-paths.mjs";
import { isMsTeamsExtensionRoot } from "../test/vitest/vitest.extension-msteams-paths.mjs";
import { isProviderExtensionRoot } from "../test/vitest/vitest.extension-provider-paths.mjs";
import {
isProviderExtensionRoot,
isProviderOpenAiExtensionRoot,
} from "../test/vitest/vitest.extension-provider-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";
@@ -32,6 +36,7 @@ import {
isBoundaryTestFile,
isBundledPluginDependentUnitTestFile,
} from "../test/vitest/vitest.unit-paths.mjs";
import { detectChangedLanes } from "./changed-lanes.mjs";
import { isCiLikeEnv, resolveLocalFullSuiteProfile } from "./lib/vitest-local-scheduling.mjs";
import { resolveVitestCliEntry, resolveVitestNodeArgs } from "./run-vitest.mjs";
@@ -53,14 +58,21 @@ const EXTENSION_ACPX_VITEST_CONFIG = "test/vitest/vitest.extension-acpx.config.t
const EXTENSION_BLUEBUBBLES_VITEST_CONFIG = "test/vitest/vitest.extension-bluebubbles.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_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_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_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";
@@ -105,18 +117,25 @@ const VITEST_CONFIG_BY_KIND = {
daemon: DAEMON_VITEST_CONFIG,
e2e: E2E_VITEST_CONFIG,
extension: EXTENSIONS_VITEST_CONFIG,
extensionFull: FULL_EXTENSIONS_VITEST_CONFIG,
extensionAcpx: EXTENSION_ACPX_VITEST_CONFIG,
extensionBlueBubbles: EXTENSION_BLUEBUBBLES_VITEST_CONFIG,
extensionChannel: EXTENSION_CHANNELS_VITEST_CONFIG,
extensionDiffs: EXTENSION_DIFFS_VITEST_CONFIG,
extensionDiscord: EXTENSION_DISCORD_VITEST_CONFIG,
extensionFeishu: EXTENSION_FEISHU_VITEST_CONFIG,
extensionImessage: EXTENSION_IMESSAGE_VITEST_CONFIG,
extensionIrc: EXTENSION_IRC_VITEST_CONFIG,
extensionLine: EXTENSION_LINE_VITEST_CONFIG,
extensionMatrix: EXTENSION_MATRIX_VITEST_CONFIG,
extensionMattermost: EXTENSION_MATTERMOST_VITEST_CONFIG,
extensionMemory: EXTENSION_MEMORY_VITEST_CONFIG,
extensionMessaging: EXTENSION_MESSAGING_VITEST_CONFIG,
extensionMsTeams: EXTENSION_MSTEAMS_VITEST_CONFIG,
extensionProviderOpenAi: EXTENSION_PROVIDER_OPENAI_VITEST_CONFIG,
extensionProvider: EXTENSION_PROVIDERS_VITEST_CONFIG,
extensionSignal: EXTENSION_SIGNAL_VITEST_CONFIG,
extensionSlack: EXTENSION_SLACK_VITEST_CONFIG,
extensionTelegram: EXTENSION_TELEGRAM_VITEST_CONFIG,
extensionVoiceCall: EXTENSION_VOICE_CALL_VITEST_CONFIG,
extensionWhatsApp: EXTENSION_WHATSAPP_VITEST_CONFIG,
@@ -148,6 +167,7 @@ const BROAD_CHANGED_RERUN_PATTERNS = [
/^test\/setup(?:\.shared|\.extensions|-openclaw-runtime)?\.ts$/u,
/^vitest(?:\..+)?\.(?:config\.ts|paths\.mjs)$/u,
/^test\/vitest\/vitest(?:\..+)?\.(?:config\.ts|paths\.mjs)$/u,
/^test\/helpers\//u,
/^scripts\/run-vitest\.mjs$/u,
/^scripts\/test-projects(?:\.test-support)?\.mjs$/u,
];
@@ -260,7 +280,14 @@ function shouldKeepBroadChangedRun(changedPaths) {
}
function isRoutableChangedTarget(changedPath) {
return /^(?:src|test|extensions|ui|packages|apps)(?:\/|$)/u.test(changedPath);
return /^(?:src|test|extensions|ui|packages)(?:\/|$)/u.test(changedPath);
}
export function listFullExtensionVitestProjectConfigs() {
return (
fullSuiteVitestShards.find((shard) => shard.config === FULL_EXTENSIONS_VITEST_CONFIG)
?.projects ?? []
);
}
export function resolveChangedTargetArgs(
@@ -276,8 +303,16 @@ export function resolveChangedTargetArgs(
if (changedPaths.length === 0 || shouldKeepBroadChangedRun(changedPaths)) {
return null;
}
const changedLanes = detectChangedLanes(changedPaths);
if (changedLanes.lanes.all) {
return null;
}
const routablePaths = changedPaths.filter(isRoutableChangedTarget);
return routablePaths.length > 0 ? [...new Set(routablePaths)] : null;
const targets = [...routablePaths];
if (changedLanes.extensionImpactFromCore) {
targets.push("extensions");
}
return [...new Set(targets)];
}
function classifyTarget(arg, cwd) {
@@ -295,8 +330,18 @@ function classifyTarget(arg, cwd) {
) {
return "e2e";
}
if (relative === "extensions") {
return "extensionFull";
}
if (relative.startsWith("extensions/")) {
const extensionRoot = relative.split("/").slice(0, 2).join("/");
const splitChannelShard = resolveSplitChannelExtensionShard(extensionRoot);
if (splitChannelShard) {
return splitChannelShard.kind;
}
if (isProviderOpenAiExtensionRoot(extensionRoot)) {
return "extensionProviderOpenAi";
}
if (isChannelSurfaceTestFile(relative)) {
return "extensionChannel";
}
@@ -506,7 +551,11 @@ export function buildVitestRunPlans(
const changedTargetArgs =
targetArgs.length === 0 ? resolveChangedTargetArgs(args, cwd, listChangedPaths) : null;
const activeTargetArgs = changedTargetArgs ?? targetArgs;
const activeForwardedArgs = changedTargetArgs ? stripChangedArgs(forwardedArgs) : forwardedArgs;
const activeForwardedArgs =
changedTargetArgs !== null ? stripChangedArgs(forwardedArgs) : forwardedArgs;
if (changedTargetArgs !== null && activeTargetArgs.length === 0) {
return [];
}
if (activeTargetArgs.length === 0) {
return [
{
@@ -570,8 +619,11 @@ export function buildVitestRunPlans(
"extensionAcpx",
"extensionDiffs",
"extensionBlueBubbles",
"extensionDiscord",
"extensionFeishu",
"extensionImessage",
"extensionIrc",
"extensionLine",
"extensionMattermost",
"extensionChannel",
"extensionTelegram",
@@ -582,7 +634,11 @@ export function buildVitestRunPlans(
"extensionMemory",
"extensionMsTeams",
"extensionMessaging",
"extensionProviderOpenAi",
"extensionProvider",
"extensionSignal",
"extensionSlack",
"extensionFull",
"channel",
"extension",
];
@@ -592,6 +648,20 @@ export function buildVitestRunPlans(
if (!grouped || grouped.length === 0) {
continue;
}
if (kind === "extensionFull") {
const configs = watchMode
? [FULL_EXTENSIONS_VITEST_CONFIG]
: listFullExtensionVitestProjectConfigs();
for (const config of configs) {
plans.push({
config,
forwardedArgs: nonTargetArgs,
includePatterns: null,
watchMode,
});
}
continue;
}
const config = VITEST_CONFIG_BY_KIND[kind] ?? DEFAULT_VITEST_CONFIG;
const useCliTargetArgs =
kind === "e2e" ||