perf: speed up channel test runs

This commit is contained in:
Peter Steinberger
2026-03-26 15:39:16 +00:00
parent 06b4a0a1f2
commit 339cc33cf8
7 changed files with 47 additions and 45 deletions

View File

@@ -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.

View File

@@ -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<ReturnType<typeof buildGrou
}
describe("buildTelegramMessageContext audio transcript body", () => {
beforeAll(async () => {
vi.resetModules();
({ buildTelegramMessageContextForTest } =
await import("./bot-message-context.test-harness.js"));
});
beforeEach(() => {
transcribeFirstAudioMock.mockReset();
});

View File

@@ -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();

View File

@@ -10,6 +10,8 @@ const {
telegramBotDepsForTest,
telegramBotRuntimeForTest,
} = harness;
const { createTelegramBot: createTelegramBotBase, setTelegramBotRuntimeForTest } =
await import("./bot.js");
let createTelegramBot: (
opts: Parameters<typeof import("./bot.js").createTelegramBot>[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<typeof setTelegramBotRuntimeForTest>[0],
);

View File

@@ -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<typeof import("./bot.js").createTelegramBot>[0],
) => ReturnType<typeof import("./bot.js").createTelegramBot>;
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;
});

View File

@@ -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",

View File

@@ -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."