From cf3ae2612b1cb034c4f670c2abdb40f277e20183 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Tue, 31 Mar 2026 19:48:17 +0900 Subject: [PATCH] fix(ci): reduce slow channel test skew --- extensions/slack/src/runtime-api.ts | 5 +- extensions/slack/src/targets.test.ts | 7 +- extensions/slack/src/targets.ts | 24 ++++++ extensions/telegram/src/bot-deps.ts | 5 ++ .../src/bot-message-context.test-harness.ts | 16 +++- .../bot-native-commands.plugin-auth.test.ts | 7 +- .../bot-native-commands.session-meta.test.ts | 1 + .../src/bot-native-commands.test-helpers.ts | 1 + .../telegram/src/bot-native-commands.ts | 4 +- scripts/test-planner/catalog.mjs | 4 + scripts/test-planner/planner.mjs | 73 ++++++++++++++++++- src/channels/plugins/normalize/slack.ts | 36 ++++++++- test/fixtures/test-timings.channels.json | 22 +++--- test/scripts/test-planner.test.ts | 56 +++++++++++++- 14 files changed, 230 insertions(+), 31 deletions(-) diff --git a/extensions/slack/src/runtime-api.ts b/extensions/slack/src/runtime-api.ts index 31da0160338..23f3db26a83 100644 --- a/extensions/slack/src/runtime-api.ts +++ b/extensions/slack/src/runtime-api.ts @@ -5,10 +5,7 @@ export { resolveConfiguredFromRequiredCredentialStatuses, } from "openclaw/plugin-sdk/channel-status"; export { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/account-id"; -export { - looksLikeSlackTargetId, - normalizeSlackMessagingTarget, -} from "openclaw/plugin-sdk/slack-targets"; +export { looksLikeSlackTargetId, normalizeSlackMessagingTarget } from "./targets.js"; export type { ChannelPlugin, OpenClawConfig, SlackAccountConfig } from "openclaw/plugin-sdk/slack"; export { buildChannelConfigSchema, diff --git a/extensions/slack/src/targets.test.ts b/extensions/slack/src/targets.test.ts index 56b796e5ac3..8fc1ff39ef4 100644 --- a/extensions/slack/src/targets.test.ts +++ b/extensions/slack/src/targets.test.ts @@ -1,6 +1,9 @@ -import { normalizeSlackMessagingTarget } from "openclaw/plugin-sdk/slack"; import { describe, expect, it } from "vitest"; -import { parseSlackTarget, resolveSlackChannelId } from "./targets.js"; +import { + normalizeSlackMessagingTarget, + parseSlackTarget, + resolveSlackChannelId, +} from "./targets.js"; describe("parseSlackTarget", () => { it("parses user mentions and prefixes", () => { diff --git a/extensions/slack/src/targets.ts b/extensions/slack/src/targets.ts index 356f990d600..bdafb120c05 100644 --- a/extensions/slack/src/targets.ts +++ b/extensions/slack/src/targets.ts @@ -55,3 +55,27 @@ export function resolveSlackChannelId(raw: string): string { const target = parseSlackTarget(raw, { defaultKind: "channel" }); return requireTargetKind({ platform: "Slack", target, kind: "channel" }); } + +export function normalizeSlackMessagingTarget(raw: string): string | undefined { + return parseSlackTarget(raw, { defaultKind: "channel" })?.normalized; +} + +export function looksLikeSlackTargetId(raw: string): boolean { + const trimmed = raw.trim(); + if (!trimmed) { + return false; + } + if (/^<@([A-Z0-9]+)>$/i.test(trimmed)) { + return true; + } + if (/^(user|channel):/i.test(trimmed)) { + return true; + } + if (/^slack:/i.test(trimmed)) { + return true; + } + if (/^[@#]/.test(trimmed)) { + return true; + } + return /^[CUWGD][A-Z0-9]{8,}$/i.test(trimmed); +} diff --git a/extensions/telegram/src/bot-deps.ts b/extensions/telegram/src/bot-deps.ts index a194afcbb97..c918fd0da39 100644 --- a/extensions/telegram/src/bot-deps.ts +++ b/extensions/telegram/src/bot-deps.ts @@ -9,6 +9,7 @@ import { readChannelAllowFromStore } from "openclaw/plugin-sdk/conversation-runt import { upsertChannelPairingRequest } from "openclaw/plugin-sdk/conversation-runtime"; import { dispatchReplyWithBufferedBlockDispatcher } from "openclaw/plugin-sdk/reply-runtime"; import { loadWebMedia } from "openclaw/plugin-sdk/web-media"; +import { syncTelegramMenuCommands } from "./bot-native-command-menu.js"; import { deliverReplies, emitInternalMessageSentHook } from "./bot/delivery.js"; import { createTelegramDraftStream } from "./draft-stream.js"; import { resolveTelegramExecApproval } from "./exec-approval-resolver.js"; @@ -26,6 +27,7 @@ export type TelegramBotDeps = { loadWebMedia?: typeof loadWebMedia; buildModelsProviderData: typeof buildModelsProviderData; listSkillCommandsForAgents: typeof listSkillCommandsForAgents; + syncTelegramMenuCommands?: typeof syncTelegramMenuCommands; wasSentByBot: typeof wasSentByBot; resolveExecApproval?: typeof resolveTelegramExecApproval; createTelegramDraftStream?: typeof createTelegramDraftStream; @@ -65,6 +67,9 @@ export const defaultTelegramBotDeps: TelegramBotDeps = { get listSkillCommandsForAgents() { return listSkillCommandsForAgents; }, + get syncTelegramMenuCommands() { + return syncTelegramMenuCommands; + }, get wasSentByBot() { return wasSentByBot; }, diff --git a/extensions/telegram/src/bot-message-context.test-harness.ts b/extensions/telegram/src/bot-message-context.test-harness.ts index 150683aa07f..fb0c0b1c42f 100644 --- a/extensions/telegram/src/bot-message-context.test-harness.ts +++ b/extensions/telegram/src/bot-message-context.test-harness.ts @@ -1,3 +1,4 @@ +import { vi } from "vitest"; import type { BuildTelegramMessageContextParams, TelegramMediaRef } from "./bot-message-context.js"; export const baseTelegramMessageContextConfig = { @@ -22,8 +23,7 @@ export async function buildTelegramMessageContextForTest( ): Promise< Awaited> > { - const { vi } = await import("vitest"); - const { buildTelegramMessageContext } = await import("./bot-message-context.js"); + const buildTelegramMessageContext = await loadBuildTelegramMessageContext(); return await buildTelegramMessageContext({ primaryCtx: { message: { @@ -64,3 +64,15 @@ export async function buildTelegramMessageContextForTest( sendChatActionHandler: { sendChatAction: vi.fn() } as never, }); } + +let buildTelegramMessageContextLoader: + | typeof import("./bot-message-context.js").buildTelegramMessageContext + | undefined; + +async function loadBuildTelegramMessageContext() { + if (!buildTelegramMessageContextLoader) { + ({ buildTelegramMessageContext: buildTelegramMessageContextLoader } = + await import("./bot-message-context.js")); + } + return buildTelegramMessageContextLoader; +} diff --git a/extensions/telegram/src/bot-native-commands.plugin-auth.test.ts b/extensions/telegram/src/bot-native-commands.plugin-auth.test.ts index 3bbec3d3e44..30cc09cf2bd 100644 --- a/extensions/telegram/src/bot-native-commands.plugin-auth.test.ts +++ b/extensions/telegram/src/bot-native-commands.plugin-auth.test.ts @@ -1,6 +1,6 @@ import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime"; import type { TelegramAccountConfig } from "openclaw/plugin-sdk/config-runtime"; -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; let createNativeCommandsHarness: typeof import("./bot-native-commands.test-helpers.js").createNativeCommandsHarness; let deliverReplies: typeof import("./bot-native-commands.test-helpers.js").deliverReplies; @@ -27,7 +27,7 @@ let executePluginCommandMock: { }; describe("registerTelegramNativeCommands (plugin auth)", () => { - beforeEach(async () => { + beforeAll(async () => { vi.resetModules(); ({ createNativeCommandsHarness, @@ -40,6 +40,9 @@ describe("registerTelegramNativeCommands (plugin auth)", () => { getPluginCommandSpecs as unknown as typeof getPluginCommandSpecsMock; matchPluginCommandMock = matchPluginCommand as unknown as typeof matchPluginCommandMock; executePluginCommandMock = executePluginCommand as unknown as typeof executePluginCommandMock; + }); + + beforeEach(() => { vi.clearAllMocks(); }); diff --git a/extensions/telegram/src/bot-native-commands.session-meta.test.ts b/extensions/telegram/src/bot-native-commands.session-meta.test.ts index d850ba64181..9813b937bae 100644 --- a/extensions/telegram/src/bot-native-commands.session-meta.test.ts +++ b/extensions/telegram/src/bot-native-commands.session-meta.test.ts @@ -206,6 +206,7 @@ function registerAndResolveCommandHandlerBase(params: { modelNames: new Map(), })), listSkillCommandsForAgents: vi.fn(() => []), + syncTelegramMenuCommands: vi.fn(), wasSentByBot: vi.fn(() => false), }; registerTelegramNativeCommands({ diff --git a/extensions/telegram/src/bot-native-commands.test-helpers.ts b/extensions/telegram/src/bot-native-commands.test-helpers.ts index 33bd1f7b8f7..0dcf38992b8 100644 --- a/extensions/telegram/src/bot-native-commands.test-helpers.ts +++ b/extensions/telegram/src/bot-native-commands.test-helpers.ts @@ -129,6 +129,7 @@ export function createNativeCommandsHarness(params?: { modelNames: new Map(), })), listSkillCommandsForAgents: vi.fn(() => []), + syncTelegramMenuCommands: vi.fn(), wasSentByBot: vi.fn(() => false), }; const bot = { diff --git a/extensions/telegram/src/bot-native-commands.ts b/extensions/telegram/src/bot-native-commands.ts index b69754ee0be..4fb00377092 100644 --- a/extensions/telegram/src/bot-native-commands.ts +++ b/extensions/telegram/src/bot-native-commands.ts @@ -56,7 +56,7 @@ import type { TelegramMessageContextOptions } from "./bot-message-context.types. import { buildCappedTelegramMenuCommands, buildPluginTelegramMenuCommands, - syncTelegramMenuCommands, + syncTelegramMenuCommands as syncTelegramMenuCommandsRuntime, } from "./bot-native-command-menu.js"; import { TelegramUpdateKeyContext } from "./bot-updates.js"; import { TelegramBotOptions } from "./bot.js"; @@ -501,6 +501,8 @@ export const registerTelegramNativeCommands = ({ `Use channels.telegram.commands.native: false to disable, or reduce plugin/skill/custom commands.`, ); } + const syncTelegramMenuCommands = + telegramDeps.syncTelegramMenuCommands ?? syncTelegramMenuCommandsRuntime; // Telegram only limits the setMyCommands payload (menu entries). // Keep hidden commands callable by registering handlers for the full catalog. syncTelegramMenuCommands({ diff --git a/scripts/test-planner/catalog.mjs b/scripts/test-planner/catalog.mjs index de937012c71..361d78415f9 100644 --- a/scripts/test-planner/catalog.mjs +++ b/scripts/test-planner/catalog.mjs @@ -83,6 +83,7 @@ export function loadTestCatalog() { const isolated = options.unitMemoryIsolatedFiles?.includes(normalizedFile) || options.extensionTimedIsolatedFiles?.includes(normalizedFile) || + options.channelTimedIsolatedFiles?.includes(normalizedFile) || unitForkIsolatedFileSet.has(normalizedFile) || extensionForkIsolatedFileSet.has(normalizedFile) || channelIsolatedFileSet.has(normalizedFile); @@ -92,6 +93,9 @@ export function loadTestCatalog() { if (options.extensionTimedIsolatedFiles?.includes(normalizedFile)) { reasons.push("extensions-timed-heavy"); } + if (options.channelTimedIsolatedFiles?.includes(normalizedFile)) { + reasons.push("channels-timed-heavy"); + } if (unitForkIsolatedFileSet.has(normalizedFile)) { reasons.push("unit-isolated-manifest"); } diff --git a/scripts/test-planner/planner.mjs b/scripts/test-planner/planner.mjs index 4fd7a67c3b7..9ab7c61eea4 100644 --- a/scripts/test-planner/planner.mjs +++ b/scripts/test-planner/planner.mjs @@ -455,6 +455,30 @@ const resolveExtensionTimedHeavyFiles = (context) => { }); }; +const resolveChannelTimedHeavyFiles = (context) => { + const { env, runtime, catalog, channelTimingManifest } = context; + const timedHeavyChannelFileLimit = parseEnvNumber( + env, + "OPENCLAW_TEST_HEAVY_CHANNEL_FILE_LIMIT", + runtime.isCI ? 10 : 6, + ); + const timedHeavyChannelMinDurationMs = parseEnvNumber( + env, + "OPENCLAW_TEST_HEAVY_CHANNEL_MIN_MS", + runtime.isCI ? 12_000 : 18_000, + ); + return selectTimedHeavyFiles({ + candidates: catalog.allKnownTestFiles.filter( + (file) => + catalog.channelTestPrefixes.some((prefix) => file.startsWith(prefix)) && + !catalog.channelIsolatedFileSet.has(file), + ), + limit: timedHeavyChannelFileLimit, + minDurationMs: timedHeavyChannelMinDurationMs, + timings: channelTimingManifest, + }); +}; + const buildDefaultUnits = (context, request) => { const { env, @@ -481,6 +505,7 @@ const buildDefaultUnits = (context, request) => { (file) => !catalog.unitBehaviorOverrideSet.has(file), ); const extensionTimedHeavyFiles = resolveExtensionTimedHeavyFiles(context); + const channelTimedHeavyFiles = resolveChannelTimedHeavyFiles(context); const unitSchedulingOverrideSet = new Set([ ...catalog.unitBehaviorOverrideSet, ...memoryHeavyUnitFiles, @@ -489,6 +514,10 @@ const buildDefaultUnits = (context, request) => { ...catalog.extensionForkIsolatedFiles, ...extensionTimedHeavyFiles, ]); + const channelSchedulingOverrideSet = new Set([ + ...catalog.channelIsolatedFiles, + ...channelTimedHeavyFiles, + ]); const unitFastExcludedFiles = [ ...new Set([ ...unitSchedulingOverrideSet, @@ -518,7 +547,7 @@ const buildDefaultUnits = (context, request) => { const channelSharedCandidateFiles = catalog.allKnownTestFiles.filter( (file) => catalog.channelTestPrefixes.some((prefix) => file.startsWith(prefix)) && - !catalog.channelIsolatedFileSet.has(file), + !channelSchedulingOverrideSet.has(file), ); const defaultExtensionsBatchTargetMs = executionBudget.extensionsBatchTargetMs; const extensionsBatchTargetMs = parseEnvNumber( @@ -709,6 +738,26 @@ const buildDefaultUnits = (context, request) => { }), ); } + for (const file of channelTimedHeavyFiles) { + units.push( + createExecutionUnit(context, { + id: `channels-${path.basename(file, ".test.ts")}-isolated`, + surface: "channels", + isolate: true, + estimatedDurationMs: estimateChannelDurationMs(file), + args: [ + "vitest", + "run", + "--config", + "vitest.channels.config.ts", + "--pool=forks", + ...noIsolateArgs, + file, + ], + reasons: ["channels-timed-heavy"], + }), + ); + } } if (selectedSurfaceSet.has("contracts")) { @@ -839,7 +888,7 @@ const buildDefaultUnits = (context, request) => { ); } - return { units, unitMemoryIsolatedFiles }; + return { units, unitMemoryIsolatedFiles, channelTimedHeavyFiles }; }; const createTargetedUnit = (context, classification, filters) => { @@ -961,6 +1010,7 @@ const buildTargetedUnits = (context, request) => { } const unitMemoryIsolatedFiles = request.unitMemoryIsolatedFiles ?? []; const extensionTimedIsolatedFiles = request.extensionTimedIsolatedFiles ?? []; + const channelTimedIsolatedFiles = request.channelTimedIsolatedFiles ?? []; const estimateUnitDurationMs = (file) => context.unitTimingManifest.files[file]?.durationMs ?? context.unitTimingManifest.defaultDurationMs; @@ -985,6 +1035,7 @@ const buildTargetedUnits = (context, request) => { const classification = context.catalog.classifyTestFile(normalizeRepoPath(fileFilter), { unitMemoryIsolatedFiles, extensionTimedIsolatedFiles, + channelTimedIsolatedFiles, }); const key = `${classification.legacyBasePinned ? "base-pinned" : classification.surface}:${ classification.isolated ? "isolated" : "default" @@ -998,6 +1049,7 @@ const buildTargetedUnits = (context, request) => { const classification = context.catalog.classifyTestFile(matchedFile, { unitMemoryIsolatedFiles, extensionTimedIsolatedFiles, + channelTimedIsolatedFiles, }); const key = `${classification.legacyBasePinned ? "base-pinned" : classification.surface}:${ classification.isolated ? "isolated" : "default" @@ -1017,6 +1069,7 @@ const buildTargetedUnits = (context, request) => { context.catalog.classifyTestFile(file, { unitMemoryIsolatedFiles, extensionTimedIsolatedFiles, + channelTimedIsolatedFiles, }), [file], ), @@ -1154,10 +1207,18 @@ const estimateTopLevelEntryDurationMs = (unit, context) => { ); } if (context.catalog.channelTestPrefixes.some((prefix) => file.startsWith(prefix))) { - return totalMs + 3_000; + return ( + totalMs + + (context.channelTimingManifest.files[file]?.durationMs ?? + context.channelTimingManifest.defaultDurationMs) + ); } if (file.startsWith(BUNDLED_PLUGIN_PATH_PREFIX)) { - return totalMs + 2_000; + return ( + totalMs + + (context.extensionTimingManifest.files[file]?.durationMs ?? + context.extensionTimingManifest.defaultDurationMs) + ); } return totalMs + 1_000; }, 0); @@ -1497,9 +1558,11 @@ export function explainExecutionTarget(request, options = {}) { (file) => !context.catalog.unitBehaviorOverrideSet.has(file), ); const extensionTimedIsolatedFiles = resolveExtensionTimedHeavyFiles(context); + const channelTimedIsolatedFiles = resolveChannelTimedHeavyFiles(context); const classification = context.catalog.classifyTestFile(normalizedTarget, { unitMemoryIsolatedFiles, extensionTimedIsolatedFiles, + channelTimedIsolatedFiles, }); const targetedUnit = createTargetedUnit(context, classification, [normalizedTarget]); return { @@ -1584,12 +1647,14 @@ export function buildExecutionPlan(request, options = {}) { const defaultPlanning = buildDefaultUnits(context, { ...request, fileFilters }); const extensionTimedIsolatedFiles = resolveExtensionTimedHeavyFiles(context); + const channelTimedIsolatedFiles = resolveChannelTimedHeavyFiles(context); let units = defaultPlanning.units; const targetedUnits = buildTargetedUnits(context, { ...request, fileFilters, unitMemoryIsolatedFiles: defaultPlanning.unitMemoryIsolatedFiles, extensionTimedIsolatedFiles, + channelTimedIsolatedFiles, }); if (context.configuredShardCount !== null && context.shardCount > 1) { units = expandUnitsAcrossTopLevelShards(context, units); diff --git a/src/channels/plugins/normalize/slack.ts b/src/channels/plugins/normalize/slack.ts index 50e31a0feee..e3259c2bb69 100644 --- a/src/channels/plugins/normalize/slack.ts +++ b/src/channels/plugins/normalize/slack.ts @@ -1,8 +1,38 @@ -import { parseSlackTarget } from "../../../plugin-sdk/slack-targets.js"; +import { + buildMessagingTarget, + ensureTargetId, + parseMentionPrefixOrAtUserTarget, +} from "../../targets.js"; export function normalizeSlackMessagingTarget(raw: string): string | undefined { - const target = parseSlackTarget(raw, { defaultKind: "channel" }); - return target?.normalized; + const trimmed = raw.trim(); + if (!trimmed) { + return undefined; + } + const userTarget = parseMentionPrefixOrAtUserTarget({ + raw: trimmed, + mentionPattern: /^<@([A-Z0-9]+)>$/i, + prefixes: [ + { prefix: "user:", kind: "user" }, + { prefix: "channel:", kind: "channel" }, + { prefix: "slack:", kind: "user" }, + ], + atUserPattern: /^[A-Z0-9]+$/i, + atUserErrorMessage: "Slack DMs require a user id (use user: or <@id>)", + }); + if (userTarget) { + return userTarget.normalized; + } + if (trimmed.startsWith("#")) { + const candidate = trimmed.slice(1).trim(); + const id = ensureTargetId({ + candidate, + pattern: /^[A-Z0-9]+$/i, + errorMessage: "Slack channels require a channel id (use channel:)", + }); + return buildMessagingTarget("channel", id, trimmed).normalized; + } + return buildMessagingTarget("channel", trimmed, trimmed).normalized; } export function looksLikeSlackTargetId(raw: string): boolean { diff --git a/test/fixtures/test-timings.channels.json b/test/fixtures/test-timings.channels.json index f4efd016ff2..45e27dc1c4b 100644 --- a/test/fixtures/test-timings.channels.json +++ b/test/fixtures/test-timings.channels.json @@ -1,6 +1,6 @@ { "config": "vitest.channels.config.ts", - "generatedAt": "2026-03-24T04:15:05Z", + "generatedAt": "2026-03-31T10:45:00Z", "defaultDurationMs": 3000, "files": { "extensions/telegram/src/bot.create-telegram-bot.test.ts": { @@ -64,7 +64,7 @@ "durationMs": 3200 }, "extensions/telegram/src/bot/delivery.resolve-media-retry.test.ts": { - "durationMs": 3000 + "durationMs": 18920 }, "extensions/telegram/src/webhook.test.ts": { "durationMs": 2900 @@ -82,7 +82,7 @@ "durationMs": 2300 }, "extensions/whatsapp/src/auto-reply/deliver-reply.test.ts": { - "durationMs": 2200 + "durationMs": 12400 }, "extensions/slack/src/monitor/events/reactions.test.ts": { "durationMs": 2200 @@ -91,7 +91,7 @@ "durationMs": 2100 }, "extensions/telegram/src/bot-native-commands.session-meta.test.ts": { - "durationMs": 2100 + "durationMs": 18000 }, "extensions/discord/src/monitor/native-command.plugin-dispatch.test.ts": { "durationMs": 2000 @@ -100,7 +100,7 @@ "durationMs": 1900 }, "extensions/telegram/src/bot-native-commands.test.ts": { - "durationMs": 1700 + "durationMs": 17590 }, "extensions/whatsapp/src/auto-reply/heartbeat-runner.test.ts": { "durationMs": 1700 @@ -130,7 +130,7 @@ "durationMs": 1500 }, "extensions/telegram/src/bot.create-telegram-bot.channel-post-media.test.ts": { - "durationMs": 1500 + "durationMs": 13000 }, "extensions/whatsapp/src/setup-surface.test.ts": { "durationMs": 1400 @@ -163,7 +163,7 @@ "durationMs": 1000 }, "extensions/telegram/src/bot-message-context.audio-transcript.test.ts": { - "durationMs": 1000 + "durationMs": 17310 }, "extensions/whatsapp/src/monitor-inbox.append-upsert.test.ts": { "durationMs": 999 @@ -220,7 +220,7 @@ "durationMs": 701 }, "extensions/telegram/src/bot-message-context.thread-binding.test.ts": { - "durationMs": 695 + "durationMs": 17700 }, "src/browser/pw-tools-core.waits-next-download-saves-it.test.ts": { "durationMs": 677 @@ -241,7 +241,7 @@ "durationMs": 570 }, "extensions/telegram/src/bot-native-commands.plugin-auth.test.ts": { - "durationMs": 563 + "durationMs": 19500 }, "extensions/discord/src/monitor/agent-components.wildcard.test.ts": { "durationMs": 547 @@ -517,7 +517,7 @@ "durationMs": 11 }, "extensions/telegram/src/bot-message-context.sender-prefix.test.ts": { - "durationMs": 11 + "durationMs": 19410 }, "extensions/whatsapp/src/active-listener.test.ts": { "durationMs": 11 @@ -736,7 +736,7 @@ "durationMs": 5 }, "extensions/slack/src/targets.test.ts": { - "durationMs": 5 + "durationMs": 100 }, "extensions/imessage/src/monitor/loop-rate-limiter.test.ts": { "durationMs": 5 diff --git a/test/scripts/test-planner.test.ts b/test/scripts/test-planner.test.ts index 21abd1edab1..ddb3f311b02 100644 --- a/test/scripts/test-planner.test.ts +++ b/test/scripts/test-planner.test.ts @@ -152,6 +152,39 @@ describe("test planner", () => { artifacts.cleanupTempArtifacts(); }); + it("auto-isolates timed-heavy channel suites in CI", () => { + const env = { + CI: "true", + GITHUB_ACTIONS: "true", + RUNNER_OS: "Linux", + OPENCLAW_TEST_HOST_CPU_COUNT: "4", + OPENCLAW_TEST_HOST_MEMORY_GIB: "16", + }; + const artifacts = createExecutionArtifacts(env); + const plan = buildExecutionPlan( + { + profile: null, + mode: "ci", + surfaces: ["channels"], + passthroughArgs: [], + }, + { + env, + platform: "linux", + writeTempJsonArtifact: artifacts.writeTempJsonArtifact, + }, + ); + + const hotspotUnit = plan.selectedUnits.find( + (unit) => unit.id === "channels-bot-native-commands.plugin-auth-isolated", + ); + + expect(hotspotUnit).toBeTruthy(); + expect(hotspotUnit?.isolate).toBe(true); + expect(hotspotUnit?.reasons).toContain("channels-timed-heavy"); + artifacts.cleanupTempArtifacts(); + }); + it("scales down mid-tier local concurrency under saturated load", () => { const artifacts = createExecutionArtifacts({ RUNNER_OS: "Linux", @@ -437,6 +470,25 @@ describe("test planner", () => { expect(explanation.reasons).toContain("extensions-timed-heavy"); }); + it("explains timed-heavy channel suites as isolated", () => { + const explanation = explainExecutionTarget( + { + mode: "ci", + fileFilters: ["extensions/telegram/src/bot-native-commands.plugin-auth.test.ts"], + }, + { + env: { + CI: "true", + GITHUB_ACTIONS: "true", + }, + }, + ); + + expect(explanation.surface).toBe("channels"); + expect(explanation.isolate).toBe(true); + expect(explanation.reasons).toContain("channels-timed-heavy"); + }); + it("does not leak default-plan shard assignments into targeted units with the same id", () => { const artifacts = createExecutionArtifacts({}); const plan = buildExecutionPlan( @@ -573,13 +625,13 @@ describe("test planner", () => { expect(manifest.jobs.buildArtifacts.enabled).toBe(true); expect(manifest.shardCounts.unit).toBe(4); - expect(manifest.shardCounts.channels).toBe(3); + expect(manifest.shardCounts.channels).toBe(4); expect(manifest.shardCounts.extensionFast).toBeGreaterThanOrEqual(4); expect(manifest.shardCounts.extensionFast).toBeLessThanOrEqual(5); expect(manifest.shardCounts.windows).toBe(6); expect(manifest.shardCounts.macosNode).toBe(9); expect(manifest.shardCounts.bun).toBe(6); - expect(manifest.jobs.checks.matrix.include).toHaveLength(7); + expect(manifest.jobs.checks.matrix.include).toHaveLength(8); expect(manifest.jobs.checksWindows.matrix.include).toHaveLength(6); expect(manifest.jobs.bunChecks.matrix.include).toHaveLength(6); expect(manifest.jobs.macosNode.matrix.include).toHaveLength(9);