From 7320dfc1ff47fb90c7ce82536356acdca8d7e742 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Wed, 15 Apr 2026 10:22:33 +0100 Subject: [PATCH] test(perf): speed up slow cron infra and secrets specs --- ...d-agent.isolated-auth-session-flag.test.ts | 142 +++++++++--------- ...tbeat-runner.returns-default-unset.test.ts | 93 ++++++++++-- src/infra/outbound/channel-resolution.test.ts | 9 +- src/infra/outbound/channel-resolution.ts | 7 +- src/secrets/runtime.coverage.test.ts | 4 +- 5 files changed, 168 insertions(+), 87 deletions(-) diff --git a/src/cron/isolated-agent.isolated-auth-session-flag.test.ts b/src/cron/isolated-agent.isolated-auth-session-flag.test.ts index 00050dc61b2..140b4933878 100644 --- a/src/cron/isolated-agent.isolated-auth-session-flag.test.ts +++ b/src/cron/isolated-agent.isolated-auth-session-flag.test.ts @@ -1,84 +1,86 @@ -import "./isolated-agent.mocks.js"; -import fs from "node:fs/promises"; -import path from "node:path"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import * as sessionOverride from "../agents/auth-profiles/session-override.js"; -import { runEmbeddedPiAgent } from "../agents/pi-embedded.js"; -import { createCliDeps } from "./isolated-agent.delivery.test-helpers.js"; -import { runCronIsolatedAgentTurn } from "./isolated-agent.js"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { - makeCfg, - makeJob, - withTempCronHome, - writeSessionStore, -} from "./isolated-agent.test-harness.js"; -import { setupIsolatedAgentTurnMocks } from "./isolated-agent.test-setup.js"; + clearFastTestEnv, + loadRunCronIsolatedAgentTurn, + makeCronSession, + resolveConfiguredModelRefMock, + resolveCronSessionMock, + resolveSessionAuthProfileOverrideMock, + resetRunCronIsolatedAgentTurnHarness, + restoreFastTestEnv, +} from "./isolated-agent/run.test-harness.js"; + +const runCronIsolatedAgentTurn = await loadRunCronIsolatedAgentTurn(); + +function makeParams(overrides?: Record) { + return { + cfg: { + auth: { + profiles: { + "openrouter:default": { + type: "api_key", + provider: "openrouter", + key: "sk-or-test-key", + }, + }, + order: { openrouter: ["openrouter:default"] }, + }, + }, + deps: {} as never, + job: { + id: "cron-auth-flag", + name: "Auth Flag", + enabled: true, + createdAtMs: 0, + updatedAtMs: 0, + schedule: { kind: "cron" as const, expr: "0 * * * *", tz: "UTC" }, + sessionTarget: "isolated" as const, + state: {}, + wakeMode: "next-heartbeat" as const, + payload: { kind: "agentTurn" as const, message: "hi" }, + delivery: { mode: "none" as const }, + }, + message: "hi", + sessionKey: "cron:auth-flag-1", + lane: "cron" as const, + ...overrides, + }; +} describe("isolated cron resolveSessionAuthProfileOverride isNewSession (#62783)", () => { + let previousFastTestEnv: string | undefined; + beforeEach(() => { - setupIsolatedAgentTurnMocks({ fast: true }); + previousFastTestEnv = clearFastTestEnv(); + resetRunCronIsolatedAgentTurnHarness(); + resolveConfiguredModelRefMock.mockReturnValue({ + provider: "openrouter", + model: "moonshotai/kimi-k2.5", + }); + resolveCronSessionMock.mockReturnValue( + makeCronSession({ + isNewSession: true, + sessionEntry: { + sessionId: "main-session", + updatedAt: 0, + systemSent: false, + skillsSnapshot: undefined, + }, + }), + ); + resolveSessionAuthProfileOverrideMock.mockResolvedValue("openrouter:default"); }); afterEach(() => { - vi.restoreAllMocks(); + restoreFastTestEnv(previousFastTestEnv); }); it("passes isNewSession=false when sessionTarget is isolated", async () => { - const spy = vi.spyOn(sessionOverride, "resolveSessionAuthProfileOverride"); - spy.mockResolvedValue("openrouter:default"); + await runCronIsolatedAgentTurn(makeParams()); - await withTempCronHome(async (home) => { - const storePath = await writeSessionStore(home, { lastProvider: "webchat", lastTo: "" }); - const agentDir = path.join(home, ".openclaw", "agents", "main", "agent"); - await fs.mkdir(agentDir, { recursive: true }); - await fs.writeFile( - path.join(agentDir, "auth-profiles.json"), - JSON.stringify({ - version: 1, - profiles: { - "openrouter:default": { - type: "api_key", - provider: "openrouter", - key: "sk-or-test-key", - }, - }, - order: { openrouter: ["openrouter:default"] }, - }), - "utf-8", - ); - - vi.mocked(runEmbeddedPiAgent).mockResolvedValue({ - payloads: [{ text: "ok" }], - meta: { - durationMs: 1, - agentMeta: { sessionId: "s", provider: "openrouter", model: "kimi-k2.5" }, - }, - }); - - const cfg = makeCfg(home, storePath, { - agents: { - defaults: { - model: { primary: "openrouter/moonshotai/kimi-k2.5" }, - workspace: path.join(home, "openclaw"), - }, - }, - }); - - await runCronIsolatedAgentTurn({ - cfg, - deps: createCliDeps(), - job: { - ...makeJob({ kind: "agentTurn", message: "hi" }), - sessionTarget: "isolated", - delivery: { mode: "none" }, - }, - message: "hi", - sessionKey: "cron:auth-flag-1", - lane: "cron", - }); - }); - - const openRouterCall = spy.mock.calls.find((c) => c[0]?.provider === "openrouter"); + const openRouterCall = resolveSessionAuthProfileOverrideMock.mock.calls.find( + (call) => call[0]?.provider === "openrouter", + ); expect( openRouterCall, "resolveSessionAuthProfileOverride was not called with provider openrouter", diff --git a/src/infra/heartbeat-runner.returns-default-unset.test.ts b/src/infra/heartbeat-runner.returns-default-unset.test.ts index d520124b916..ca753773930 100644 --- a/src/infra/heartbeat-runner.returns-default-unset.test.ts +++ b/src/infra/heartbeat-runner.returns-default-unset.test.ts @@ -2,12 +2,8 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -import { - isWhatsAppGroupJid, - normalizeWhatsAppTarget, -} from "../../test/helpers/channels/command-contract.js"; -import { whatsappOutbound } from "../../test/helpers/infra/deliver-test-outbounds.js"; import { HEARTBEAT_PROMPT } from "../auto-reply/heartbeat.js"; +import type { ChannelOutboundAdapter } from "../channels/plugins/types.public.js"; import type { OpenClawConfig } from "../config/config.js"; import { resolveAgentIdFromSessionKey, @@ -39,6 +35,85 @@ let testRegistry: ReturnType | null = null; let fixtureRoot = ""; let fixtureCount = 0; +function normalizeWhatsAppTargetForTest(raw: string): string | null { + const trimmed = raw + .trim() + .replace(/^whatsapp:/i, "") + .trim(); + if (!trimmed) { + return null; + } + const lowered = trimmed.toLowerCase().replace(/\s+/gu, ""); + if (/^\d+@g\.us$/u.test(lowered)) { + return lowered; + } + const digits = trimmed.replace(/\D/gu, ""); + const normalized = digits ? `+${digits}` : ""; + return /^\+\d{7,15}$/u.test(normalized) ? normalized : null; +} + +function isWhatsAppGroupJidForTest(raw: string): boolean { + return /^\d+@g\.us$/u.test(raw.trim().toLowerCase()); +} + +const whatsappOutboundForTest: ChannelOutboundAdapter = { + deliveryMode: "gateway", + sendText: async ({ cfg, to, text, accountId, deps }) => { + const sender = deps?.whatsapp as + | (( + to: string, + text: string, + options?: Record, + ) => Promise<{ messageId: string } & Record>) + | undefined; + if (!sender) { + throw new Error("missing whatsapp sender"); + } + const result = await sender(to, text, { + verbose: false, + cfg, + accountId: accountId ?? undefined, + }); + return { + channel: "whatsapp", + ...result, + }; + }, + sendMedia: async ({ + cfg, + to, + text, + mediaUrl, + mediaLocalRoots, + mediaReadFile, + accountId, + deps, + }) => { + const sender = deps?.whatsapp as + | (( + to: string, + text: string, + options?: Record, + ) => Promise<{ messageId: string } & Record>) + | undefined; + if (!sender) { + throw new Error("missing whatsapp sender"); + } + const result = await sender(to, text, { + verbose: false, + cfg, + mediaUrl, + mediaLocalRoots, + mediaReadFile, + accountId: accountId ?? undefined, + }); + return { + channel: "whatsapp", + ...result, + }; + }, +}; + function resolveWhatsAppTargetForTest(params: { to: string | null | undefined; allowFrom: Array | null | undefined; @@ -50,9 +125,9 @@ function resolveWhatsAppTargetForTest(params: { const hasWildcard = allowListRaw.includes("*"); const allowList = allowListRaw .filter((entry) => entry !== "*") - .map((entry) => normalizeWhatsAppTarget(entry)) + .map((entry) => normalizeWhatsAppTargetForTest(entry)) .filter((entry): entry is string => Boolean(entry)); - const normalizedTarget = normalizeWhatsAppTarget(trimmed); + const normalizedTarget = normalizeWhatsAppTargetForTest(trimmed); if (!normalizedTarget) { return { @@ -60,7 +135,7 @@ function resolveWhatsAppTargetForTest(params: { error: new Error('Missing target for WhatsApp; expected "".'), }; } - if (isWhatsAppGroupJid(normalizedTarget)) { + if (isWhatsAppGroupJidForTest(normalizedTarget)) { return { ok: true as const, to: normalizedTarget }; } if (hasWildcard || allowList.length === 0 || allowList.includes(normalizedTarget)) { @@ -86,7 +161,7 @@ beforeAll(async () => { const whatsappPlugin = createOutboundTestPlugin({ id: "whatsapp", outbound: { - ...whatsappOutbound, + ...whatsappOutboundForTest, resolveTarget: ({ to, allowFrom }) => resolveWhatsAppTargetForTest({ to, diff --git a/src/infra/outbound/channel-resolution.test.ts b/src/infra/outbound/channel-resolution.test.ts index 46b14c9ddc4..6dbfa19c3a3 100644 --- a/src/infra/outbound/channel-resolution.test.ts +++ b/src/infra/outbound/channel-resolution.test.ts @@ -2,6 +2,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; const resolveDefaultAgentIdMock = vi.hoisted(() => vi.fn()); const resolveAgentWorkspaceDirMock = vi.hoisted(() => vi.fn()); +const getLoadedChannelPluginMock = vi.hoisted(() => vi.fn()); const getChannelPluginMock = vi.hoisted(() => vi.fn()); const applyPluginAutoEnableMock = vi.hoisted(() => vi.fn()); const resolveRuntimePluginRegistryMock = vi.hoisted(() => vi.fn()); @@ -17,6 +18,7 @@ vi.mock("../../agents/agent-scope.js", () => ({ })); vi.mock("../../channels/plugins/index.js", () => ({ + getLoadedChannelPlugin: (...args: unknown[]) => getLoadedChannelPluginMock(...args), getChannelPlugin: (...args: unknown[]) => getChannelPluginMock(...args), })); @@ -67,6 +69,7 @@ describe("outbound channel resolution", () => { beforeEach(async () => { resolveDefaultAgentIdMock.mockReset(); resolveAgentWorkspaceDirMock.mockReset(); + getLoadedChannelPluginMock.mockReset(); getChannelPluginMock.mockReset(); applyPluginAutoEnableMock.mockReset(); resolveRuntimePluginRegistryMock.mockReset(); @@ -107,7 +110,7 @@ describe("outbound channel resolution", () => { it("returns the already-registered plugin without bootstrapping", async () => { const plugin = { id: "telegram" }; - getChannelPluginMock.mockReturnValueOnce(plugin); + getLoadedChannelPluginMock.mockReturnValueOnce(plugin); const channelResolution = await importChannelResolution("existing-plugin"); expect( @@ -140,7 +143,7 @@ describe("outbound channel resolution", () => { it("bootstraps plugins once per registry key and returns the newly loaded plugin", async () => { const plugin = { id: "telegram" }; - getChannelPluginMock.mockReturnValueOnce(undefined).mockReturnValueOnce(plugin); + getLoadedChannelPluginMock.mockReturnValueOnce(undefined).mockReturnValueOnce(plugin); const channelResolution = await importChannelResolution("bootstrap-success"); expect( @@ -162,7 +165,7 @@ describe("outbound channel resolution", () => { it("bootstraps when the active registry has other channels but not the requested one", async () => { const plugin = { id: "telegram" }; - getChannelPluginMock.mockReturnValueOnce(undefined).mockReturnValueOnce(plugin); + getLoadedChannelPluginMock.mockReturnValueOnce(undefined).mockReturnValueOnce(plugin); getActivePluginRegistryMock.mockReturnValue({ channels: [{ plugin: { id: "discord" } }], }); diff --git a/src/infra/outbound/channel-resolution.ts b/src/infra/outbound/channel-resolution.ts index a5cf49892e1..70149a404c7 100644 --- a/src/infra/outbound/channel-resolution.ts +++ b/src/infra/outbound/channel-resolution.ts @@ -1,4 +1,4 @@ -import { getChannelPlugin } from "../../channels/plugins/index.js"; +import { getChannelPlugin, getLoadedChannelPlugin } from "../../channels/plugins/index.js"; import type { ChannelPlugin } from "../../channels/plugins/types.plugin.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; import { getActivePluginRegistry } from "../../plugins/runtime.js"; @@ -58,8 +58,9 @@ export function resolveOutboundChannelPlugin(params: { return undefined; } + const resolveLoaded = () => getLoadedChannelPlugin(normalized); const resolve = () => getChannelPlugin(normalized); - const current = resolve(); + const current = resolveLoaded(); if (current) { return current; } @@ -69,5 +70,5 @@ export function resolveOutboundChannelPlugin(params: { } maybeBootstrapChannelPlugin({ channel: normalized, cfg: params.cfg }); - return resolve() ?? resolveDirectFromActiveRegistry(normalized); + return resolveLoaded() ?? resolveDirectFromActiveRegistry(normalized) ?? resolve(); } diff --git a/src/secrets/runtime.coverage.test.ts b/src/secrets/runtime.coverage.test.ts index 502f52a0835..0f3ff190870 100644 --- a/src/secrets/runtime.coverage.test.ts +++ b/src/secrets/runtime.coverage.test.ts @@ -416,7 +416,7 @@ async function prepareConfigCoverageSnapshot(params: { skipConfigCollectors?: boolean; }) { await ensureConfigCoverageRuntimeLoaded(); - const sourceConfig = structuredClone(params.config); + const sourceConfig = params.config; const resolvedConfig = structuredClone(params.config); const context = createResolverContext({ sourceConfig, @@ -468,7 +468,7 @@ async function prepareAuthCoverageSnapshot(params: { loadAuthStore: (agentDir?: string) => AuthProfileStore; }) { await ensureAuthCoverageRuntimeLoaded(); - const sourceConfig = structuredClone(params.config); + const sourceConfig = params.config; const context = createResolverContext({ sourceConfig, env: params.env,