From 339cc33cf87971beae093d5ff58a9299bd02f4c3 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 26 Mar 2026 15:39:16 +0000 Subject: [PATCH] perf: speed up channel test runs --- docs/help/testing.md | 13 ++++++------ ...t-message-context.audio-transcript.test.ts | 11 +++------- .../bot-message-context.dm-threads.test.ts | 17 +++++----------- ...te-telegram-bot.channel-post-media.test.ts | 10 ++++------ .../src/bot.create-telegram-bot.test.ts | 19 ++++++------------ package.json | 2 ++ test/fixtures/test-parallel.behavior.json | 20 +++++++++++++++++++ 7 files changed, 47 insertions(+), 45 deletions(-) diff --git a/docs/help/testing.md b/docs/help/testing.md index 7ebd66f94ca..bd4e070dc6a 100644 --- a/docs/help/testing.md +++ b/docs/help/testing.md @@ -23,6 +23,7 @@ This doc is a “how we test” guide: Most days: - Full gate (expected before push): `pnpm build && pnpm check && pnpm test` +- Faster local full-suite run on a roomy machine: `pnpm test:max` When you touch tests or want extra confidence: @@ -54,9 +55,8 @@ Think of the suites as “increasing realism” (and increasing flakiness/cost): - Should be fast and stable - Scheduler note: - `pnpm test` now keeps a small checked-in behavioral manifest for true pool/isolation overrides and a separate timing snapshot for the slowest unit files. - - Shared unit coverage now defaults to `threads`, while the manifest keeps the measured fork-only exceptions and heavy singleton lanes explicit. - - The shared extension lane still defaults to `threads`; the wrapper keeps explicit fork-only exceptions in `test/fixtures/test-parallel.behavior.json` when a file cannot safely share a non-isolated worker. - - The channel suite (`vitest.channels.config.ts`) now also defaults to `threads`; the March 22, 2026 direct full-suite control run passed clean without channel-specific fork exceptions. + - Shared unit, extension, channel, and gateway runs all stay on Vitest `forks`. + - The wrapper keeps measured fork-isolated exceptions and heavy singleton lanes explicit in `test/fixtures/test-parallel.behavior.json`. - The wrapper peels the heaviest measured files into dedicated lanes instead of relying on a growing hand-maintained exclusion list. - Refresh the timing snapshot with `pnpm test:perf:update-timings` after major suite shape changes. - Embedded runner note: @@ -72,15 +72,16 @@ Think of the suites as “increasing realism” (and increasing flakiness/cost): sufficient substitute for those integration paths. - Pool note: - Base Vitest config still defaults to `forks`. - - Unit wrapper lanes default to `threads`, with explicit manifest fork-only exceptions. - - Extension scoped config defaults to `threads`. - - Channel scoped config defaults to `threads`. + - Unit, channel, extension, and gateway wrapper lanes all default to `forks`. - Unit, channel, and extension configs default to `isolate: false` for faster file startup. - `pnpm test` also passes `--isolate=false` at the wrapper level. - Opt back into Vitest file isolation with `OPENCLAW_TEST_ISOLATE=1 pnpm test`. - `OPENCLAW_TEST_NO_ISOLATE=0` or `OPENCLAW_TEST_NO_ISOLATE=false` also force isolated runs. - Fast-local iteration note: - `pnpm test:changed` runs the wrapper with `--changed origin/main`. + - `pnpm test:changed:max` keeps the same changed-file filter but uses the wrapper's aggressive local planner profile. + - `pnpm test:max` exposes that same planner profile for a full local run. + - On Node 25, the normal local profile keeps top-level lane parallelism off; `pnpm test:max` re-enables it. On Node 22/24 LTS, normal local runs can also use top-level lane parallelism. - The base Vitest config marks the wrapper manifests/config files as `forceRerunTriggers` so changed-mode reruns stay correct when scheduler inputs change. - Vitest's filesystem module cache is now enabled by default for Node-side test reruns. - Opt out with `OPENCLAW_VITEST_FS_MODULE_CACHE=0` or `OPENCLAW_VITEST_FS_MODULE_CACHE=false` if you suspect stale transform cache behavior. diff --git a/extensions/telegram/src/bot-message-context.audio-transcript.test.ts b/extensions/telegram/src/bot-message-context.audio-transcript.test.ts index 9f50d700484..ef0d3503823 100644 --- a/extensions/telegram/src/bot-message-context.audio-transcript.test.ts +++ b/extensions/telegram/src/bot-message-context.audio-transcript.test.ts @@ -1,4 +1,4 @@ -import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; const transcribeFirstAudioMock = vi.fn(); const DEFAULT_MODEL = "anthropic/claude-opus-4-5"; @@ -9,7 +9,8 @@ vi.mock("./media-understanding.runtime.js", () => ({ transcribeFirstAudio: (...args: unknown[]) => transcribeFirstAudioMock(...args), })); -let buildTelegramMessageContextForTest: typeof import("./bot-message-context.test-harness.js").buildTelegramMessageContextForTest; +const { buildTelegramMessageContextForTest } = + await import("./bot-message-context.test-harness.js"); async function buildGroupVoiceContext(params: { messageId: number; @@ -75,12 +76,6 @@ function expectAudioPlaceholderRendered(ctx: Awaited { - beforeAll(async () => { - vi.resetModules(); - ({ buildTelegramMessageContextForTest } = - await import("./bot-message-context.test-harness.js")); - }); - beforeEach(() => { transcribeFirstAudioMock.mockReset(); }); diff --git a/extensions/telegram/src/bot-message-context.dm-threads.test.ts b/extensions/telegram/src/bot-message-context.dm-threads.test.ts index 3288dee1397..8826a96b77a 100644 --- a/extensions/telegram/src/bot-message-context.dm-threads.test.ts +++ b/extensions/telegram/src/bot-message-context.dm-threads.test.ts @@ -1,15 +1,8 @@ -import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; - -let buildTelegramMessageContextForTest: typeof import("./bot-message-context.test-harness.js").buildTelegramMessageContextForTest; -let clearRuntimeConfigSnapshot: typeof import("../../../src/config/config.js").clearRuntimeConfigSnapshot; -let setRuntimeConfigSnapshot: typeof import("../../../src/config/config.js").setRuntimeConfigSnapshot; - -beforeAll(async () => { - vi.resetModules(); - ({ buildTelegramMessageContextForTest } = await import("./bot-message-context.test-harness.js")); - ({ clearRuntimeConfigSnapshot, setRuntimeConfigSnapshot } = - await import("../../../src/config/config.js")); -}); +import { afterEach, beforeEach, describe, expect, it } from "vitest"; +const { buildTelegramMessageContextForTest } = + await import("./bot-message-context.test-harness.js"); +const { clearRuntimeConfigSnapshot, setRuntimeConfigSnapshot } = + await import("../../../src/config/config.js"); beforeEach(() => { clearRuntimeConfigSnapshot(); diff --git a/extensions/telegram/src/bot.create-telegram-bot.channel-post-media.test.ts b/extensions/telegram/src/bot.create-telegram-bot.channel-post-media.test.ts index fcda6768ab8..8520254da70 100644 --- a/extensions/telegram/src/bot.create-telegram-bot.channel-post-media.test.ts +++ b/extensions/telegram/src/bot.create-telegram-bot.channel-post-media.test.ts @@ -10,6 +10,8 @@ const { telegramBotDepsForTest, telegramBotRuntimeForTest, } = harness; +const { createTelegramBot: createTelegramBotBase, setTelegramBotRuntimeForTest } = + await import("./bot.js"); let createTelegramBot: ( opts: Parameters[0], @@ -136,10 +138,7 @@ async function queueChannelPostAlbum( } describe("createTelegramBot channel_post media", () => { - beforeAll(async () => { - vi.resetModules(); - const { createTelegramBot: createTelegramBotBase, setTelegramBotRuntimeForTest } = - await import("./bot.js"); + beforeAll(() => { createTelegramBot = (opts) => createTelegramBotBase({ ...opts, @@ -150,8 +149,7 @@ describe("createTelegramBot channel_post media", () => { ); }); - beforeEach(async () => { - const { setTelegramBotRuntimeForTest } = await import("./bot.js"); + beforeEach(() => { setTelegramBotRuntimeForTest( telegramBotRuntimeForTest as unknown as Parameters[0], ); diff --git a/extensions/telegram/src/bot.create-telegram-bot.test.ts b/extensions/telegram/src/bot.create-telegram-bot.test.ts index cbc4cc139d0..8709453bb60 100644 --- a/extensions/telegram/src/bot.create-telegram-bot.test.ts +++ b/extensions/telegram/src/bot.create-telegram-bot.test.ts @@ -34,13 +34,15 @@ const { throttlerSpy, useSpy, } = harness; -let resolveTelegramFetch: typeof import("./fetch.js").resolveTelegramFetch; -let setTelegramBotRuntimeForTest: typeof import("./bot.js").setTelegramBotRuntimeForTest; -let createTelegramBotBase: typeof import("./bot.js").createTelegramBot; +const { resolveTelegramFetch } = await import("./fetch.js"); +const { + createTelegramBot: createTelegramBotBase, + getTelegramSequentialKey, + setTelegramBotRuntimeForTest, +} = await import("./bot.js"); let createTelegramBot: ( opts: Parameters[0], ) => ReturnType; -let getTelegramSequentialKey: typeof import("./bot.js").getTelegramSequentialKey; const loadConfig = getLoadConfigMock(); const loadWebMedia = getLoadWebMediaMock(); @@ -81,15 +83,6 @@ describe("createTelegramBot", () => { beforeAll(() => { process.env.TZ = "UTC"; }); - beforeAll(async () => { - vi.resetModules(); - ({ resolveTelegramFetch } = await import("./fetch.js")); - ({ - createTelegramBot: createTelegramBotBase, - getTelegramSequentialKey, - setTelegramBotRuntimeForTest, - } = await import("./bot.js")); - }); afterAll(() => { process.env.TZ = ORIGINAL_TZ; }); diff --git a/package.json b/package.json index 2fffbc15dd0..4105d64d07f 100644 --- a/package.json +++ b/package.json @@ -705,6 +705,7 @@ "test:auth:compat": "vitest run --config vitest.gateway.config.ts src/gateway/server.auth.compat-baseline.test.ts src/gateway/client.test.ts src/gateway/reconnect-gating.test.ts src/gateway/protocol/connect-error-details.test.ts", "test:build:singleton": "node scripts/test-built-plugin-singleton.mjs", "test:changed": "pnpm test -- --changed origin/main", + "test:changed:max": "node scripts/test-parallel.mjs --profile max --changed origin/main", "test:channels": "node scripts/test-parallel.mjs --surface channels", "test:contracts": "pnpm test:contracts:channels && pnpm test:contracts:plugins", "test:contracts:channels": "OPENCLAW_TEST_PROFILE=serial pnpm test -- src/channels/plugins/contracts", @@ -735,6 +736,7 @@ "test:install:e2e:openai": "OPENCLAW_E2E_MODELS=openai bash scripts/test-install-sh-e2e-docker.sh", "test:install:smoke": "bash scripts/test-install-sh-docker.sh", "test:live": "OPENCLAW_LIVE_TEST=1 vitest run --config vitest.live.config.ts", + "test:max": "node scripts/test-parallel.mjs --profile max", "test:parallels:linux": "bash scripts/e2e/parallels-linux-smoke.sh", "test:parallels:macos": "bash scripts/e2e/parallels-macos-smoke.sh", "test:parallels:npm-update": "bash scripts/e2e/parallels-npm-update-smoke.sh", diff --git a/test/fixtures/test-parallel.behavior.json b/test/fixtures/test-parallel.behavior.json index 262c9fa11b9..8982c7595e9 100644 --- a/test/fixtures/test-parallel.behavior.json +++ b/test/fixtures/test-parallel.behavior.json @@ -74,6 +74,22 @@ "file": "extensions/whatsapp/src/inbound.media.test.ts", "reason": "This WhatsApp inbound media suite is green alone but can inherit polluted media and inbound parsing state from the shared channel lane; keep it in its own forked lane for deterministic CI." }, + { + "file": "extensions/whatsapp/src/monitor-inbox.streams-inbound-messages.test.ts", + "reason": "This WhatsApp monitor inbox stream suite is green alone but can fail after the shared channel lane reuses inbound parsing state; keep it in its own forked lane for deterministic reruns and to trim the serial shared channels batch." + }, + { + "file": "extensions/whatsapp/src/monitor-inbox.captures-media-path-image-messages.test.ts", + "reason": "This WhatsApp inbox media-path suite spends about two seconds in assertions and grows worker RSS near 0.9 GiB alone; keep it in its own forked lane so the shared channels batch shrinks and top-level concurrency can overlap the hotspot." + }, + { + "file": "extensions/whatsapp/src/monitor-inbox.blocks-messages-from-unauthorized-senders-not-allowfrom.test.ts", + "reason": "This WhatsApp inbox allow-from rejection suite is a heavy shared hotspot with about two seconds of test work and high worker RSS; keep it in its own forked lane so it can overlap instead of extending the serial shared channels batch." + }, + { + "file": "extensions/whatsapp/src/monitor-inbox.allows-messages-from-senders-allowfrom-list.test.ts", + "reason": "This WhatsApp inbox allow-from acceptance suite is another heavy shared hotspot with about two seconds of test work and high worker RSS; keep it in its own forked lane so it can overlap instead of extending the serial shared channels batch." + }, { "file": "src/browser/chrome.test.ts", "reason": "This Chrome helper suite is green alone but can inherit stale fetch, websocket, or timer state from the shared channel lane; keep it isolated so its CDP timeout assertions stay deterministic." @@ -94,6 +110,10 @@ "file": "extensions/telegram/src/fetch.test.ts", "reason": "This Telegram transport suite measured ~759.3 MiB RSS growth locally; keep it in its own forked channel lane so the shared channels worker can recycle immediately after the hotspot file." }, + { + "file": "extensions/telegram/src/sendchataction-401-backoff.test.ts", + "reason": "This Telegram send-chat-action backoff suite hoists infra-runtime sleep mocks and remains a relatively heavy shared hotspot; keep it isolated so top-level concurrency can overlap it instead of extending the shared channels batch." + }, { "file": "extensions/telegram/src/monitor.test.ts", "reason": "This Telegram monitor suite measured ~748.4 MiB RSS growth locally; keep it in its own forked channel lane so the shared channels worker can recycle immediately after the hotspot file."