From a0d3dc94d0a1e7a1928852d36f999ab70bbaf5fb Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 18 Mar 2026 18:19:12 +0000 Subject: [PATCH] perf: reduce unit test hot path overhead --- extensions/whatsapp/src/shared.ts | 30 +++--- scripts/lib/optional-bundled-clusters.d.mts | 2 +- scripts/lib/optional-bundled-clusters.d.ts | 6 ++ scripts/test-parallel.mjs | 17 +++- src/acp/translator.session-rate-limit.test.ts | 7 +- src/auto-reply/thinking.shared.ts | 40 ++++++++ src/auto-reply/thinking.ts | 7 ++ src/commands/channel-test-helpers.ts | 12 ++- ...rovider-usage.auth.normalizes-keys.test.ts | 19 +++- src/infra/provider-usage.auth.ts | 6 +- src/infra/provider-usage.load.ts | 2 + src/infra/provider-usage.test-support.ts | 4 + src/infra/provider-usage.test.ts | 1 + src/plugin-sdk/outbound-media.test.ts | 2 +- test/fixtures/test-timings.unit.json | 92 +++++++++++++++++++ 15 files changed, 213 insertions(+), 34 deletions(-) create mode 100644 scripts/lib/optional-bundled-clusters.d.ts diff --git a/extensions/whatsapp/src/shared.ts b/extensions/whatsapp/src/shared.ts index 3888cdc36d3..3e241c9f94c 100644 --- a/extensions/whatsapp/src/shared.ts +++ b/extensions/whatsapp/src/shared.ts @@ -92,20 +92,7 @@ export function createWhatsAppPluginBase(params: { setupWizard: NonNullable["setupWizard"]>; setup: NonNullable["setup"]>; isConfigured: NonNullable["config"]>["isConfigured"]; -}): Pick< - ChannelPlugin, - | "id" - | "meta" - | "setupWizard" - | "capabilities" - | "reload" - | "gatewayMethods" - | "configSchema" - | "config" - | "security" - | "setup" - | "groups" -> { +}) { const collectWhatsAppSecurityWarnings = createAllowlistProviderRouteAllowlistWarningCollector({ providerConfigPresent: (cfg) => cfg.channels?.whatsapp !== undefined, @@ -126,7 +113,7 @@ export function createWhatsAppPluginBase(params: { groupAllowFromPath: "channels.whatsapp.groupAllowFrom", }, }); - return createChannelPluginBase({ + const base = createChannelPluginBase({ id: WHATSAPP_CHANNEL, meta: { ...getChatChannelMeta(WHATSAPP_CHANNEL), @@ -167,7 +154,18 @@ export function createWhatsAppPluginBase(params: { }, setup: params.setup, groups: params.groups, - }) as Pick< + }); + return { + ...base, + setupWizard: base.setupWizard!, + capabilities: base.capabilities!, + reload: base.reload!, + gatewayMethods: base.gatewayMethods!, + configSchema: base.configSchema!, + config: base.config!, + security: base.security!, + groups: base.groups!, + } satisfies Pick< ChannelPlugin, | "id" | "meta" diff --git a/scripts/lib/optional-bundled-clusters.d.mts b/scripts/lib/optional-bundled-clusters.d.mts index 42640bd1772..425e241ced7 100644 --- a/scripts/lib/optional-bundled-clusters.d.mts +++ b/scripts/lib/optional-bundled-clusters.d.mts @@ -1,6 +1,6 @@ export const optionalBundledClusters: string[]; export const optionalBundledClusterSet: Set; -export const OPTIONAL_BUNDLED_BUILD_ENV: string; +export const OPTIONAL_BUNDLED_BUILD_ENV: "OPENCLAW_INCLUDE_OPTIONAL_BUNDLED"; export function isOptionalBundledCluster(cluster: string): boolean; export function shouldIncludeOptionalBundledClusters(env?: NodeJS.ProcessEnv): boolean; export function shouldBuildBundledCluster(cluster: string, env?: NodeJS.ProcessEnv): boolean; diff --git a/scripts/lib/optional-bundled-clusters.d.ts b/scripts/lib/optional-bundled-clusters.d.ts new file mode 100644 index 00000000000..425e241ced7 --- /dev/null +++ b/scripts/lib/optional-bundled-clusters.d.ts @@ -0,0 +1,6 @@ +export const optionalBundledClusters: string[]; +export const optionalBundledClusterSet: Set; +export const OPTIONAL_BUNDLED_BUILD_ENV: "OPENCLAW_INCLUDE_OPTIONAL_BUNDLED"; +export function isOptionalBundledCluster(cluster: string): boolean; +export function shouldIncludeOptionalBundledClusters(env?: NodeJS.ProcessEnv): boolean; +export function shouldBuildBundledCluster(cluster: string, env?: NodeJS.ProcessEnv): boolean; diff --git a/scripts/test-parallel.mjs b/scripts/test-parallel.mjs index 68361a6b094..94d2a173a0e 100644 --- a/scripts/test-parallel.mjs +++ b/scripts/test-parallel.mjs @@ -236,11 +236,16 @@ const parseEnvNumber = (name, fallback) => { const parsed = Number.parseInt(process.env[name] ?? "", 10); return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback; }; -const allKnownUnitFiles = allKnownTestFiles.filter((file) => inferTarget(file).owner === "unit"); +const allKnownUnitFiles = allKnownTestFiles.filter((file) => { + if (file.endsWith(".live.test.ts") || file.endsWith(".e2e.test.ts")) { + return false; + } + return inferTarget(file).owner !== "gateway"; +}); const defaultHeavyUnitFileLimit = - testProfile === "serial" ? 0 : testProfile === "low" ? 8 : highMemLocalHost ? 24 : 16; + testProfile === "serial" ? 0 : testProfile === "low" ? 20 : highMemLocalHost ? 80 : 60; const defaultHeavyUnitLaneCount = - testProfile === "serial" ? 0 : testProfile === "low" ? 1 : highMemLocalHost ? 3 : 2; + testProfile === "serial" ? 0 : testProfile === "low" ? 2 : highMemLocalHost ? 5 : 4; const heavyUnitFileLimit = parseEnvNumber( "OPENCLAW_TEST_HEAVY_UNIT_FILE_LIMIT", defaultHeavyUnitFileLimit, @@ -582,8 +587,10 @@ const defaultWorkerBudget = } : highMemLocalHost ? { - // High-memory local hosts can prioritize wall-clock speed. - unit: Math.max(4, Math.min(14, Math.floor((localWorkers * 7) / 8))), + // After peeling measured hotspots into dedicated lanes, the shared + // unit-fast lane shuts down more reliably with a slightly smaller + // worker fan-out than the old "max it out" local default. + unit: Math.max(4, Math.min(10, Math.floor((localWorkers * 5) / 8))), unitIsolated: Math.max(1, Math.min(2, Math.floor(localWorkers / 6) || 1)), extensions: Math.max(1, Math.min(4, Math.floor(localWorkers / 4))), gateway: Math.max(2, Math.min(6, Math.floor(localWorkers / 2))), diff --git a/src/acp/translator.session-rate-limit.test.ts b/src/acp/translator.session-rate-limit.test.ts index d5897fa8172..566b61a5027 100644 --- a/src/acp/translator.session-rate-limit.test.ts +++ b/src/acp/translator.session-rate-limit.test.ts @@ -5,11 +5,10 @@ import type { SetSessionConfigOptionRequest, SetSessionModeRequest, } from "@agentclientprotocol/sdk"; -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { describe, expect, it, vi } from "vitest"; import { listThinkingLevels } from "../auto-reply/thinking.js"; import type { GatewayClient } from "../gateway/client.js"; import type { EventFrame } from "../gateway/protocol/index.js"; -import { resetProviderRuntimeHookCacheForTest } from "../plugins/provider-runtime.js"; import { createInMemorySessionStore } from "./session.js"; import { AcpGatewayAgent } from "./translator.js"; import { createAcpConnection, createAcpGateway } from "./translator.test-helpers.js"; @@ -121,10 +120,6 @@ async function expectOversizedPromptRejected(params: { sessionId: string; text: sessionStore.clearAllSessionsForTest(); } -beforeEach(() => { - resetProviderRuntimeHookCacheForTest(); -}); - describe("acp session creation rate limit", () => { it("rate limits excessive newSession bursts", async () => { const sessionStore = createInMemorySessionStore(); diff --git a/src/auto-reply/thinking.shared.ts b/src/auto-reply/thinking.shared.ts index 7487928eac3..e5a80c8bdb3 100644 --- a/src/auto-reply/thinking.shared.ts +++ b/src/auto-reply/thinking.shared.ts @@ -14,6 +14,25 @@ export type ThinkingCatalogEntry = { const BASE_THINKING_LEVELS: ThinkLevel[] = ["off", "minimal", "low", "medium", "high", "adaptive"]; const ANTHROPIC_CLAUDE_46_MODEL_RE = /^claude-(?:opus|sonnet)-4(?:\.|-)6(?:$|[-.])/i; const AMAZON_BEDROCK_CLAUDE_46_MODEL_RE = /claude-(?:opus|sonnet)-4(?:\.|-)6(?:$|[-.])/i; +const OPENAI_XHIGH_MODEL_IDS = [ + "gpt-5.4", + "gpt-5.4-pro", + "gpt-5.4-mini", + "gpt-5.4-nano", + "gpt-5.2", +] as const; +const OPENAI_CODEX_XHIGH_MODEL_IDS = [ + "gpt-5.4", + "gpt-5.3-codex", + "gpt-5.3-codex-spark", + "gpt-5.2-codex", + "gpt-5.1-codex", +] as const; +const GITHUB_COPILOT_XHIGH_MODEL_IDS = ["gpt-5.2", "gpt-5.2-codex"] as const; + +function matchesExactOrPrefix(modelId: string, ids: readonly string[]): boolean { + return ids.some((candidate) => modelId === candidate || modelId.startsWith(`${candidate}-`)); +} export function normalizeProviderId(provider?: string | null): string { if (!provider) { @@ -33,6 +52,27 @@ export function isBinaryThinkingProvider(provider?: string | null): boolean { return normalizeProviderId(provider) === "zai"; } +export function supportsBuiltInXHighThinking( + provider?: string | null, + model?: string | null, +): boolean { + const providerId = normalizeProviderId(provider); + const modelId = model?.trim().toLowerCase(); + if (!providerId || !modelId) { + return false; + } + if (providerId === "openai") { + return matchesExactOrPrefix(modelId, OPENAI_XHIGH_MODEL_IDS); + } + if (providerId === "openai-codex") { + return matchesExactOrPrefix(modelId, OPENAI_CODEX_XHIGH_MODEL_IDS); + } + if (providerId === "github-copilot") { + return GITHUB_COPILOT_XHIGH_MODEL_IDS.includes(modelId as never); + } + return false; +} + // Normalize user-provided thinking level strings to the canonical enum. export function normalizeThinkLevel(raw?: string | null): ThinkLevel | undefined { if (!raw) { diff --git a/src/auto-reply/thinking.ts b/src/auto-reply/thinking.ts index 1f2f1738b1f..7c0f2df02c7 100644 --- a/src/auto-reply/thinking.ts +++ b/src/auto-reply/thinking.ts @@ -5,6 +5,7 @@ import { listThinkingLevels as listThinkingLevelsFallback, normalizeProviderId, resolveThinkingDefaultForModel as resolveThinkingDefaultForModelFallback, + supportsBuiltInXHighThinking, } from "./thinking.shared.js"; import type { ThinkLevel, ThinkingCatalogEntry } from "./thinking.shared.js"; export { @@ -36,6 +37,9 @@ import { } from "../plugins/provider-runtime.js"; export function isBinaryThinkingProvider(provider?: string | null, model?: string | null): boolean { + if (isBinaryThinkingProviderFallback(provider)) { + return true; + } const normalizedProvider = normalizeProviderId(provider); if (!normalizedProvider) { return false; @@ -59,6 +63,9 @@ export function supportsXHighThinking(provider?: string | null, model?: string | if (!modelKey) { return false; } + if (supportsBuiltInXHighThinking(provider, modelKey)) { + return true; + } const providerKey = normalizeProviderId(provider); if (providerKey) { const pluginDecision = resolveProviderXHighThinking({ diff --git a/src/commands/channel-test-helpers.ts b/src/commands/channel-test-helpers.ts index eff2b5ecc33..455ff235be6 100644 --- a/src/commands/channel-test-helpers.ts +++ b/src/commands/channel-test-helpers.ts @@ -1,3 +1,7 @@ +import { matrixPlugin } from "../../extensions/matrix/index.js"; +import { msteamsPlugin } from "../../extensions/msteams/index.js"; +import { nostrPlugin } from "../../extensions/nostr/index.js"; +import { tlonPlugin } from "../../extensions/tlon/index.js"; import { bundledChannelPlugins } from "../channels/plugins/bundled.js"; import { setActivePluginRegistry } from "../plugins/runtime.js"; import { createTestRegistry } from "../test-utils/channel-plugins.js"; @@ -20,7 +24,13 @@ type PatchedSetupAdapterFields = { }; export function setDefaultChannelPluginRegistryForTests(): void { - const channels = bundledChannelPlugins.map((plugin) => ({ + const channels = [ + ...bundledChannelPlugins, + matrixPlugin, + msteamsPlugin, + nostrPlugin, + tlonPlugin, + ].map((plugin) => ({ pluginId: plugin.id, plugin, source: "test" as const, diff --git a/src/infra/provider-usage.auth.normalizes-keys.test.ts b/src/infra/provider-usage.auth.normalizes-keys.test.ts index 261ff0203bc..0309a63c7f6 100644 --- a/src/infra/provider-usage.auth.normalizes-keys.test.ts +++ b/src/infra/provider-usage.auth.normalizes-keys.test.ts @@ -1,9 +1,18 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import { afterAll, beforeAll, describe, expect, it } from "vitest"; +import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { NON_ENV_SECRETREF_MARKER } from "../agents/model-auth-markers.js"; -import { resolveProviderAuths, type ProviderAuth } from "./provider-usage.auth.js"; + +const resolveProviderUsageAuthWithPluginMock = vi.fn(async () => null); + +vi.mock("../plugins/provider-runtime.js", () => ({ + resolveProviderUsageAuthWithPlugin: (...args: unknown[]) => + resolveProviderUsageAuthWithPluginMock(...args), +})); + +let resolveProviderAuths: typeof import("./provider-usage.auth.js").resolveProviderAuths; +type ProviderAuth = import("./provider-usage.auth.js").ProviderAuth; describe("resolveProviderAuths key normalization", () => { let suiteRoot = ""; @@ -18,6 +27,7 @@ describe("resolveProviderAuths key normalization", () => { beforeAll(async () => { suiteRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-provider-auth-suite-")); + ({ resolveProviderAuths } = await import("./provider-usage.auth.js")); }); afterAll(async () => { @@ -26,6 +36,11 @@ describe("resolveProviderAuths key normalization", () => { suiteCase = 0; }); + beforeEach(() => { + resolveProviderUsageAuthWithPluginMock.mockReset(); + resolveProviderUsageAuthWithPluginMock.mockResolvedValue(null); + }); + async function withSuiteHome( fn: (home: string) => Promise, env: Record, diff --git a/src/infra/provider-usage.auth.ts b/src/infra/provider-usage.auth.ts index 982ffbc8be5..c503779b6f5 100644 --- a/src/infra/provider-usage.auth.ts +++ b/src/infra/provider-usage.auth.ts @@ -229,17 +229,19 @@ export async function resolveProviderAuths(params: { providers: UsageProviderId[]; auth?: ProviderAuth[]; agentDir?: string; + config?: OpenClawConfig; + env?: NodeJS.ProcessEnv; }): Promise { if (params.auth) { return params.auth; } const state: UsageAuthState = { - cfg: loadConfig(), + cfg: params.config ?? loadConfig(), store: ensureAuthProfileStore(params.agentDir, { allowKeychainPrompt: false, }), - env: process.env, + env: params.env ?? process.env, agentDir: params.agentDir, }; const auths: ProviderAuth[] = []; diff --git a/src/infra/provider-usage.load.ts b/src/infra/provider-usage.load.ts index a8658889c68..ec870aa27ee 100644 --- a/src/infra/provider-usage.load.ts +++ b/src/infra/provider-usage.load.ts @@ -179,6 +179,8 @@ export async function loadProviderUsageSummary( providers: opts.providers ?? usageProviders, auth: opts.auth, agentDir: opts.agentDir, + config, + env, }); if (auths.length === 0) { return { updatedAt: now, providers: [] }; diff --git a/src/infra/provider-usage.test-support.ts b/src/infra/provider-usage.test-support.ts index 2d2609a29d6..13006bb7213 100644 --- a/src/infra/provider-usage.test-support.ts +++ b/src/infra/provider-usage.test-support.ts @@ -1,3 +1,4 @@ +import type { OpenClawConfig } from "../config/config.js"; import { createProviderUsageFetch } from "../test-utils/provider-usage-fetch.js"; import type { ProviderAuth } from "./provider-usage.auth.js"; import type { UsageSummary } from "./provider-usage.types.js"; @@ -8,6 +9,7 @@ type ProviderUsageLoader = (params: { now: number; auth?: ProviderAuth[]; fetch?: typeof fetch; + config?: OpenClawConfig; }) => Promise; export type ProviderUsageAuth = NonNullable< @@ -23,5 +25,7 @@ export async function loadUsageWithAuth( now: usageNow, auth, fetch: mockFetch as unknown as typeof fetch, + // These tests exercise the built-in usage fetchers, not provider plugin hooks. + config: { plugins: { enabled: false } } as OpenClawConfig, }); } diff --git a/src/infra/provider-usage.test.ts b/src/infra/provider-usage.test.ts index fdd2326a9a0..fb267613184 100644 --- a/src/infra/provider-usage.test.ts +++ b/src/infra/provider-usage.test.ts @@ -294,6 +294,7 @@ describe("provider usage loading", () => { providers: ["anthropic"], agentDir, fetch: mockFetch as unknown as typeof fetch, + config: { plugins: { enabled: false } }, }); const claude = expectSingleAnthropicProvider(summary); diff --git a/src/plugin-sdk/outbound-media.test.ts b/src/plugin-sdk/outbound-media.test.ts index 6efb42df7fe..b68f382cd3a 100644 --- a/src/plugin-sdk/outbound-media.test.ts +++ b/src/plugin-sdk/outbound-media.test.ts @@ -2,7 +2,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; const loadWebMediaMock = vi.hoisted(() => vi.fn()); -vi.mock("../media/web-media.js", () => ({ +vi.mock("./web-media.js", () => ({ loadWebMedia: loadWebMediaMock, })); diff --git a/test/fixtures/test-timings.unit.json b/test/fixtures/test-timings.unit.json index 2199276bc5b..cdb2505d881 100644 --- a/test/fixtures/test-timings.unit.json +++ b/test/fixtures/test-timings.unit.json @@ -130,6 +130,98 @@ "src/agents/session-tool-result-guard.tool-result-persist-hook.test.ts": { "durationMs": 1600, "testCount": 22 + }, + "src/plugins/tools.optional.test.ts": { + "durationMs": 1590, + "testCount": 18 + }, + "src/security/fix.test.ts": { + "durationMs": 1580, + "testCount": 24 + }, + "src/utils.test.ts": { + "durationMs": 1570, + "testCount": 34 + }, + "src/auto-reply/tool-meta.test.ts": { + "durationMs": 1560, + "testCount": 26 + }, + "src/auto-reply/envelope.test.ts": { + "durationMs": 1550, + "testCount": 20 + }, + "src/commands/auth-choice.test.ts": { + "durationMs": 1540, + "testCount": 18 + }, + "src/agents/skills.build-workspace-skills-prompt.syncs-merged-skills-into-target-workspace.test.ts": { + "durationMs": 1530, + "testCount": 14 + }, + "src/media/store.header-ext.test.ts": { + "durationMs": 1520, + "testCount": 16 + }, + "extensions/whatsapp/src/media.test.ts": { + "durationMs": 1510, + "testCount": 16 + }, + "extensions/whatsapp/src/auto-reply.web-auto-reply.falls-back-text-media-send-fails.test.ts": { + "durationMs": 1500, + "testCount": 10 + }, + "src/browser/server.covers-additional-endpoint-branches.test.ts": { + "durationMs": 1490, + "testCount": 18 + }, + "src/browser/server.post-tabs-open-profile-unknown-returns-404.test.ts": { + "durationMs": 1480, + "testCount": 12 + }, + "src/browser/server.skips-default-maxchars-explicitly-set-zero.test.ts": { + "durationMs": 1470, + "testCount": 10 + }, + "src/browser/server.auth-token-gates-http.test.ts": { + "durationMs": 1460, + "testCount": 15 + }, + "extensions/acpx/src/runtime.test.ts": { + "durationMs": 1450, + "testCount": 12 + }, + "test/scripts/ios-team-id.test.ts": { + "durationMs": 1440, + "testCount": 12 + }, + "src/agents/bash-tools.exec.background-abort.test.ts": { + "durationMs": 1430, + "testCount": 10 + }, + "src/agents/subagent-announce.format.test.ts": { + "durationMs": 1420, + "testCount": 12 + }, + "src/auto-reply/reply.triggers.trigger-handling.filters-usage-summary-current-model-provider.test.ts": { + "durationMs": 1410, + "testCount": 14 + }, + "src/auto-reply/reply.triggers.trigger-handling.targets-active-session-native-stop.test.ts": { + "durationMs": 1400, + "testCount": 10 + }, + "src/auto-reply/reply.triggers.group-intro-prompts.test.ts": { + "durationMs": 1390, + "testCount": 12 + }, + "src/auto-reply/reply.triggers.trigger-handling.handles-inline-commands-strips-it-before-agent.test.ts": { + "durationMs": 1380, + "testCount": 10 + }, + "extensions/whatsapp/src/auto-reply.web-auto-reply.compresses-common-formats-jpeg-cap.test.ts": { + "durationMs": 1370, + "testCount": 10 } } }