From b21bcf6eb69d75206cae3960229ca69f0034e5a2 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sun, 22 Mar 2026 17:36:32 -0700 Subject: [PATCH] fix(test): reduce channel suite startup hotspots --- .../discord/src/monitor/monitor.test.ts | 66 ++++-- .../src/monitor.tool-result.autostart.test.ts | 209 ++++++++++++++++++ ...ends-tool-summaries-responseprefix.test.ts | 162 -------------- .../src/bot.create-telegram-bot.test.ts | 5 +- 4 files changed, 255 insertions(+), 187 deletions(-) create mode 100644 extensions/signal/src/monitor.tool-result.autostart.test.ts diff --git a/extensions/discord/src/monitor/monitor.test.ts b/extensions/discord/src/monitor/monitor.test.ts index 55b4fc096a2..6a9bbc0b1a6 100644 --- a/extensions/discord/src/monitor/monitor.test.ts +++ b/extensions/discord/src/monitor/monitor.test.ts @@ -12,7 +12,7 @@ import type { DiscordAccountConfig } from "openclaw/plugin-sdk/config-runtime"; import { buildPluginBindingApprovalCustomId } from "openclaw/plugin-sdk/conversation-runtime"; import { buildAgentSessionKey } from "openclaw/plugin-sdk/routing"; import { beforeEach, describe, expect, it, vi } from "vitest"; -import { peekSystemEvents, resetSystemEventsForTest } from "../../../../src/infra/system-events.js"; +import { peekSystemEvents, resetSystemEventsForTest } from "../../../../src/infra/system-events.ts"; import { clearDiscordComponentEntries, registerDiscordComponentEntries, @@ -51,6 +51,7 @@ import { const readAllowFromStoreMock = vi.hoisted(() => vi.fn()); const upsertPairingRequestMock = vi.hoisted(() => vi.fn()); +const enqueueSystemEventMock = vi.hoisted(() => vi.fn()); const dispatchReplyMock = vi.hoisted(() => vi.fn()); const recordInboundSessionMock = vi.hoisted(() => vi.fn()); const readSessionUpdatedAtMock = vi.hoisted(() => vi.fn()); @@ -87,6 +88,34 @@ vi.mock("openclaw/plugin-sdk/conversation-runtime", async (importOriginal) => { resolvePluginConversationBindingApprovalMock(...args), buildPluginBindingResolvedText: (...args: unknown[]) => buildPluginBindingResolvedTextMock(...args), + recordInboundSession: (...args: unknown[]) => recordInboundSessionMock(...args), + }; +}); +vi.mock("openclaw/plugin-sdk/conversation-runtime.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + upsertChannelPairingRequest: (...args: unknown[]) => upsertPairingRequestMock(...args), + resolvePluginConversationBindingApproval: (...args: unknown[]) => + resolvePluginConversationBindingApprovalMock(...args), + buildPluginBindingResolvedText: (...args: unknown[]) => + buildPluginBindingResolvedTextMock(...args), + recordInboundSession: (...args: unknown[]) => recordInboundSessionMock(...args), + }; +}); + +vi.mock("openclaw/plugin-sdk/infra-runtime", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + enqueueSystemEvent: (...args: unknown[]) => enqueueSystemEventMock(...args), + }; +}); +vi.mock("openclaw/plugin-sdk/infra-runtime.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + enqueueSystemEvent: (...args: unknown[]) => enqueueSystemEventMock(...args), }; }); @@ -111,14 +140,6 @@ vi.mock("../../../../src/auto-reply/reply/provider-dispatcher.js", async (import }; }); -vi.mock("openclaw/plugin-sdk/conversation-runtime", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - recordInboundSession: (...args: unknown[]) => recordInboundSessionMock(...args), - }; -}); - vi.mock("openclaw/plugin-sdk/config-runtime", async (importOriginal) => { const actual = await importOriginal(); return { @@ -138,14 +159,15 @@ vi.mock("openclaw/plugin-sdk/plugin-runtime", async (importOriginal) => { }); describe("agent components", () => { - const createCfg = (): OpenClawConfig => ({}) as OpenClawConfig; - const dmSessionKey = buildAgentSessionKey({ + const defaultDmSessionKey = buildAgentSessionKey({ agentId: "main", channel: "discord", accountId: "default", peer: { kind: "direct", id: "123456789" }, }); + const createCfg = (): OpenClawConfig => ({}) as OpenClawConfig; + const createBaseDmInteraction = (overrides: Record = {}) => { const reply = vi.fn().mockResolvedValue(undefined); const defer = vi.fn().mockResolvedValue(undefined); @@ -203,11 +225,9 @@ describe("agent components", () => { const pairingText = String(reply.mock.calls[0]?.[0]?.content ?? ""); expect(pairingText).toContain("Pairing code:"); const code = pairingText.match(/Pairing code:\s*([A-Z2-9]{8})/)?.[1]; - if (!code) { - throw new Error(`pairing reply did not include an 8-character pairing code: ${pairingText}`); - } + expect(code).toBeDefined(); expect(pairingText).toContain(`openclaw pairing approve discord ${code}`); - expect(peekSystemEvents(dmSessionKey)).toEqual([]); + expect(peekSystemEvents(defaultDmSessionKey)).toEqual([]); expect(readAllowFromStoreMock).toHaveBeenCalledWith("discord", "default"); }); @@ -226,7 +246,7 @@ describe("agent components", () => { content: "You are not authorized to use this button.", ephemeral: true, }); - expect(peekSystemEvents(dmSessionKey)).toEqual([]); + expect(peekSystemEvents(defaultDmSessionKey)).toEqual([]); expect(readAllowFromStoreMock).not.toHaveBeenCalled(); }); @@ -243,7 +263,7 @@ describe("agent components", () => { expect(defer).not.toHaveBeenCalled(); expect(reply).toHaveBeenCalledWith({ content: "✓", ephemeral: true }); - expect(peekSystemEvents(dmSessionKey)).toEqual([ + expect(peekSystemEvents(defaultDmSessionKey)).toEqual([ "[Discord component: hello clicked by Alice#1234 (123456789)]", ]); expect(upsertPairingRequestMock).not.toHaveBeenCalled(); @@ -263,7 +283,7 @@ describe("agent components", () => { expect(defer).not.toHaveBeenCalled(); expect(reply).toHaveBeenCalledWith({ content: "✓", ephemeral: true }); - expect(peekSystemEvents(dmSessionKey)).toEqual([ + expect(peekSystemEvents(defaultDmSessionKey)).toEqual([ "[Discord component: hello clicked by Alice#1234 (123456789)]", ]); expect(readAllowFromStoreMock).not.toHaveBeenCalled(); @@ -285,7 +305,7 @@ describe("agent components", () => { content: "DM interactions are disabled.", ephemeral: true, }); - expect(peekSystemEvents(dmSessionKey)).toEqual([]); + expect(peekSystemEvents(defaultDmSessionKey)).toEqual([]); expect(readAllowFromStoreMock).not.toHaveBeenCalled(); }); @@ -303,7 +323,7 @@ describe("agent components", () => { expect(defer).not.toHaveBeenCalled(); expect(reply).toHaveBeenCalledWith({ content: "✓", ephemeral: true }); - expect(peekSystemEvents(dmSessionKey)).toEqual([ + expect(peekSystemEvents(defaultDmSessionKey)).toEqual([ "[Discord select menu: hello interacted by Alice#1234 (123456789) (selected: alpha)]", ]); expect(readAllowFromStoreMock).not.toHaveBeenCalled(); @@ -322,7 +342,7 @@ describe("agent components", () => { expect(defer).not.toHaveBeenCalled(); expect(reply).toHaveBeenCalledWith({ content: "✓", ephemeral: true }); - expect(peekSystemEvents(dmSessionKey)).toEqual([ + expect(peekSystemEvents(defaultDmSessionKey)).toEqual([ "[Discord component: hello_cid clicked by Alice#1234 (123456789)]", ]); expect(readAllowFromStoreMock).not.toHaveBeenCalled(); @@ -341,7 +361,7 @@ describe("agent components", () => { expect(defer).not.toHaveBeenCalled(); expect(reply).toHaveBeenCalledWith({ content: "✓", ephemeral: true }); - expect(peekSystemEvents(dmSessionKey)).toEqual([ + expect(peekSystemEvents(defaultDmSessionKey)).toEqual([ "[Discord component: hello%2G clicked by Alice#1234 (123456789)]", ]); expect(readAllowFromStoreMock).not.toHaveBeenCalled(); @@ -502,7 +522,7 @@ describe("discord component interactions", () => { lastDispatchCtx = undefined; readAllowFromStoreMock.mockClear().mockResolvedValue([]); upsertPairingRequestMock.mockClear().mockResolvedValue({ code: "PAIRCODE", created: true }); - resetSystemEventsForTest(); + enqueueSystemEventMock.mockClear(); dispatchReplyMock.mockClear().mockImplementation(async (params: DispatchParams) => { lastDispatchCtx = params.ctx; await params.dispatcherOptions.deliver({ text: "ok" }); diff --git a/extensions/signal/src/monitor.tool-result.autostart.test.ts b/extensions/signal/src/monitor.tool-result.autostart.test.ts new file mode 100644 index 00000000000..06eaf2fd597 --- /dev/null +++ b/extensions/signal/src/monitor.tool-result.autostart.test.ts @@ -0,0 +1,209 @@ +import { describe, expect, it, vi } from "vitest"; +import type { OpenClawConfig } from "../../../src/config/config.js"; +import type { SignalDaemonExitEvent } from "./daemon.js"; +import { + createMockSignalDaemonHandle, + config, + getSignalToolResultTestMocks, + installSignalToolResultTestHooks, + setSignalToolResultTestConfig, +} from "./monitor.tool-result.test-harness.js"; + +installSignalToolResultTestHooks(); + +vi.resetModules(); +const { monitorSignalProvider } = await import("./monitor.js"); + +const { waitForTransportReadyMock, spawnSignalDaemonMock, streamMock } = + getSignalToolResultTestMocks(); + +const SIGNAL_BASE_URL = "http://127.0.0.1:8080"; +type MonitorSignalProviderOptions = Parameters[0]; + +function createMonitorRuntime() { + return { + log: vi.fn(), + error: vi.fn(), + exit: ((code: number): never => { + throw new Error(`exit ${code}`); + }) as (code: number) => never, + }; +} + +function createSignalConfig(overrides: Record = {}): Record { + const base = config as OpenClawConfig; + const channels = (base.channels ?? {}) as Record; + const signal = (channels.signal ?? {}) as Record; + return { + ...base, + channels: { + ...channels, + signal: { + ...signal, + autoStart: true, + dmPolicy: "open", + allowFrom: ["*"], + ...overrides, + }, + }, + }; +} + +function setSignalAutoStartConfig(overrides: Record = {}) { + setSignalToolResultTestConfig(createSignalConfig(overrides)); +} + +function createAutoAbortController() { + const abortController = new AbortController(); + streamMock.mockImplementation(async () => { + abortController.abort(); + return; + }); + return abortController; +} + +async function runMonitorWithMocks(opts: MonitorSignalProviderOptions) { + return monitorSignalProvider({ + config: config as OpenClawConfig, + waitForTransportReady: waitForTransportReadyMock as any, + ...opts, + }); +} + +function expectWaitForTransportReadyTimeout(timeoutMs: number) { + expect(waitForTransportReadyMock).toHaveBeenCalledTimes(1); + expect(waitForTransportReadyMock).toHaveBeenCalledWith( + expect.objectContaining({ + timeoutMs, + }), + ); +} + +describe("monitorSignalProvider autostart", () => { + it("uses bounded readiness checks when auto-starting the daemon", async () => { + const runtime = createMonitorRuntime(); + setSignalAutoStartConfig(); + const abortController = createAutoAbortController(); + await runMonitorWithMocks({ + autoStart: true, + baseUrl: SIGNAL_BASE_URL, + abortSignal: abortController.signal, + runtime, + }); + + expect(waitForTransportReadyMock).toHaveBeenCalledTimes(1); + expect(waitForTransportReadyMock).toHaveBeenCalledWith( + expect.objectContaining({ + label: "signal daemon", + timeoutMs: 30_000, + logAfterMs: 10_000, + logIntervalMs: 10_000, + pollIntervalMs: 150, + runtime, + abortSignal: expect.any(AbortSignal), + }), + ); + }); + + it("uses startupTimeoutMs override when provided", async () => { + const runtime = createMonitorRuntime(); + setSignalAutoStartConfig({ startupTimeoutMs: 60_000 }); + const abortController = createAutoAbortController(); + + await runMonitorWithMocks({ + autoStart: true, + baseUrl: SIGNAL_BASE_URL, + abortSignal: abortController.signal, + runtime, + startupTimeoutMs: 90_000, + }); + + expectWaitForTransportReadyTimeout(90_000); + }); + + it("caps startupTimeoutMs at 2 minutes", async () => { + const runtime = createMonitorRuntime(); + setSignalAutoStartConfig({ startupTimeoutMs: 180_000 }); + const abortController = createAutoAbortController(); + + await runMonitorWithMocks({ + autoStart: true, + baseUrl: SIGNAL_BASE_URL, + abortSignal: abortController.signal, + runtime, + }); + + expectWaitForTransportReadyTimeout(120_000); + }); + + it("fails fast when auto-started signal daemon exits during startup", async () => { + const runtime = createMonitorRuntime(); + setSignalAutoStartConfig(); + spawnSignalDaemonMock.mockReturnValueOnce( + createMockSignalDaemonHandle({ + exited: Promise.resolve({ source: "process", code: 1, signal: null }), + isExited: () => true, + }), + ); + waitForTransportReadyMock.mockImplementationOnce( + async (params: { abortSignal?: AbortSignal | null }) => { + await new Promise((_resolve, reject) => { + if (params.abortSignal?.aborted) { + reject(params.abortSignal.reason); + return; + } + params.abortSignal?.addEventListener( + "abort", + () => reject(params.abortSignal?.reason ?? new Error("aborted")), + { once: true }, + ); + }); + }, + ); + + await expect( + runMonitorWithMocks({ + autoStart: true, + baseUrl: SIGNAL_BASE_URL, + runtime, + }), + ).rejects.toThrow(/signal daemon exited/i); + }); + + it("treats daemon exit after user abort as clean shutdown", async () => { + const runtime = createMonitorRuntime(); + setSignalAutoStartConfig(); + const abortController = new AbortController(); + let exited = false; + let resolveExit!: (value: SignalDaemonExitEvent) => void; + const exitedPromise = new Promise((resolve) => { + resolveExit = resolve; + }); + const stop = vi.fn(() => { + if (exited) { + return; + } + exited = true; + resolveExit({ source: "process", code: null, signal: "SIGTERM" }); + }); + spawnSignalDaemonMock.mockReturnValueOnce( + createMockSignalDaemonHandle({ + stop, + exited: exitedPromise, + isExited: () => exited, + }), + ); + streamMock.mockImplementationOnce(async () => { + abortController.abort(new Error("stop")); + }); + + await expect( + runMonitorWithMocks({ + autoStart: true, + baseUrl: SIGNAL_BASE_URL, + runtime, + abortSignal: abortController.signal, + }), + ).resolves.toBeUndefined(); + }); +}); diff --git a/extensions/signal/src/monitor.tool-result.sends-tool-summaries-responseprefix.test.ts b/extensions/signal/src/monitor.tool-result.sends-tool-summaries-responseprefix.test.ts index 14fa9bf1f19..13809e30f9b 100644 --- a/extensions/signal/src/monitor.tool-result.sends-tool-summaries-responseprefix.test.ts +++ b/extensions/signal/src/monitor.tool-result.sends-tool-summaries-responseprefix.test.ts @@ -2,9 +2,7 @@ import { resolveAgentRoute } from "openclaw/plugin-sdk/routing"; import { normalizeE164 } from "openclaw/plugin-sdk/text-runtime"; import { describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../../../src/config/config.js"; -import type { SignalDaemonExitEvent } from "./daemon.js"; import { - createMockSignalDaemonHandle, config, flush, getSignalToolResultTestMocks, @@ -26,26 +24,11 @@ const { enqueueSystemEventMock, upsertPairingRequestMock, waitForTransportReadyMock, - spawnSignalDaemonMock, } = getSignalToolResultTestMocks(); const SIGNAL_BASE_URL = "http://127.0.0.1:8080"; type MonitorSignalProviderOptions = Parameters[0]; -function createMonitorRuntime() { - return { - log: vi.fn(), - error: vi.fn(), - exit: ((code: number): never => { - throw new Error(`exit ${code}`); - }) as (code: number) => never, - }; -} - -function setSignalAutoStartConfig(overrides: Record = {}) { - setSignalToolResultTestConfig(createSignalConfig(overrides)); -} - function createSignalConfig(overrides: Record = {}): Record { const base = config as OpenClawConfig; const channels = (base.channels ?? {}) as Record; @@ -65,15 +48,6 @@ function createSignalConfig(overrides: Record = {}): Record { - abortController.abort(); - return; - }); - return abortController; -} - async function runMonitorWithMocks(opts: MonitorSignalProviderOptions) { return monitorSignalProvider({ config: config as OpenClawConfig, @@ -163,143 +137,7 @@ function setReactionNotificationConfig(mode: "all" | "own", extra: Record { - it("uses bounded readiness checks when auto-starting the daemon", async () => { - const runtime = createMonitorRuntime(); - setSignalAutoStartConfig(); - const abortController = createAutoAbortController(); - await runMonitorWithMocks({ - autoStart: true, - baseUrl: SIGNAL_BASE_URL, - abortSignal: abortController.signal, - runtime, - }); - - expect(waitForTransportReadyMock).toHaveBeenCalledTimes(1); - expect(waitForTransportReadyMock).toHaveBeenCalledWith( - expect.objectContaining({ - label: "signal daemon", - timeoutMs: 30_000, - logAfterMs: 10_000, - logIntervalMs: 10_000, - pollIntervalMs: 150, - runtime, - abortSignal: expect.any(AbortSignal), - }), - ); - }); - - it("uses startupTimeoutMs override when provided", async () => { - const runtime = createMonitorRuntime(); - setSignalAutoStartConfig({ startupTimeoutMs: 60_000 }); - const abortController = createAutoAbortController(); - - await runMonitorWithMocks({ - autoStart: true, - baseUrl: SIGNAL_BASE_URL, - abortSignal: abortController.signal, - runtime, - startupTimeoutMs: 90_000, - }); - - expectWaitForTransportReadyTimeout(90_000); - }); - - it("caps startupTimeoutMs at 2 minutes", async () => { - const runtime = createMonitorRuntime(); - setSignalAutoStartConfig({ startupTimeoutMs: 180_000 }); - const abortController = createAutoAbortController(); - - await runMonitorWithMocks({ - autoStart: true, - baseUrl: SIGNAL_BASE_URL, - abortSignal: abortController.signal, - runtime, - }); - - expectWaitForTransportReadyTimeout(120_000); - }); - - it("fails fast when auto-started signal daemon exits during startup", async () => { - const runtime = createMonitorRuntime(); - setSignalAutoStartConfig(); - spawnSignalDaemonMock.mockReturnValueOnce( - createMockSignalDaemonHandle({ - exited: Promise.resolve({ source: "process", code: 1, signal: null }), - isExited: () => true, - }), - ); - waitForTransportReadyMock.mockImplementationOnce( - async (params: { abortSignal?: AbortSignal | null }) => { - await new Promise((_resolve, reject) => { - if (params.abortSignal?.aborted) { - reject(params.abortSignal.reason); - return; - } - params.abortSignal?.addEventListener( - "abort", - () => reject(params.abortSignal?.reason ?? new Error("aborted")), - { once: true }, - ); - }); - }, - ); - - await expect( - runMonitorWithMocks({ - autoStart: true, - baseUrl: SIGNAL_BASE_URL, - runtime, - }), - ).rejects.toThrow(/signal daemon exited/i); - }); - - it("treats daemon exit after user abort as clean shutdown", async () => { - const runtime = createMonitorRuntime(); - setSignalAutoStartConfig(); - const abortController = new AbortController(); - let exited = false; - let resolveExit!: (value: SignalDaemonExitEvent) => void; - const exitedPromise = new Promise((resolve) => { - resolveExit = resolve; - }); - const stop = vi.fn(() => { - if (exited) { - return; - } - exited = true; - resolveExit({ source: "process", code: null, signal: "SIGTERM" }); - }); - spawnSignalDaemonMock.mockReturnValueOnce( - createMockSignalDaemonHandle({ - stop, - exited: exitedPromise, - isExited: () => exited, - }), - ); - streamMock.mockImplementationOnce(async () => { - abortController.abort(new Error("stop")); - }); - - await expect( - runMonitorWithMocks({ - autoStart: true, - baseUrl: SIGNAL_BASE_URL, - runtime, - abortSignal: abortController.signal, - }), - ).resolves.toBeUndefined(); - }); - it("skips tool summaries with responsePrefix", async () => { replyMock.mockResolvedValue({ text: "final reply" }); diff --git a/extensions/telegram/src/bot.create-telegram-bot.test.ts b/extensions/telegram/src/bot.create-telegram-bot.test.ts index b5ddb7187ef..2b124c806d4 100644 --- a/extensions/telegram/src/bot.create-telegram-bot.test.ts +++ b/extensions/telegram/src/bot.create-telegram-bot.test.ts @@ -51,6 +51,7 @@ const createTelegramBot = (opts: Parameters[0]) => }); const loadConfig = getLoadConfigMock(); +const loadWebMedia = getLoadWebMediaMock(); const readChannelAllowFromStore = getReadChannelAllowFromStoreMock(); const upsertChannelPairingRequest = getUpsertChannelPairingRequestMock(); @@ -1259,7 +1260,6 @@ describe("createTelegramBot", () => { }); it("sends GIF replies as animations", async () => { - const loadWebMedia = getLoadWebMediaMock(); replySpy.mockResolvedValueOnce({ text: "caption", mediaUrl: "https://example.com/fun", @@ -1269,7 +1269,6 @@ describe("createTelegramBot", () => { contentType: "image/gif", fileName: "fun.gif", }); - createTelegramBot({ token: "tok" }); const handler = getOnHandler("message") as (ctx: Record) => Promise; @@ -1292,6 +1291,8 @@ describe("createTelegramBot", () => { reply_to_message_id: undefined, }); expect(sendPhotoSpy).not.toHaveBeenCalled(); + expect(loadWebMedia).toHaveBeenCalledTimes(1); + expect(loadWebMedia.mock.calls[0]?.[0]).toBe("https://example.com/fun"); }); function resetHarnessSpies() {