From 1c7444dab6bc308b191d996a9ad57fa92b90338f Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 11 Apr 2026 03:07:44 +0100 Subject: [PATCH] perf: optimize test import surfaces --- .../native-command.model-picker.test.ts | 117 +++++++++--------- .../discord/src/send.creates-thread.test.ts | 48 ++++--- src/cron/normalize.test.ts | 11 ++ src/gateway/server-methods/cron.ts | 6 +- .../runtime/runtime-task-test-harness.ts | 5 +- test/setup.shared.ts | 2 +- test/vitest-projects-config.test.ts | 3 +- 7 files changed, 101 insertions(+), 91 deletions(-) diff --git a/extensions/discord/src/monitor/native-command.model-picker.test.ts b/extensions/discord/src/monitor/native-command.model-picker.test.ts index a711887d25..054b66a14d 100644 --- a/extensions/discord/src/monitor/native-command.model-picker.test.ts +++ b/extensions/discord/src/monitor/native-command.model-picker.test.ts @@ -3,32 +3,21 @@ import * as commandRegistryModule from "openclaw/plugin-sdk/command-auth"; import type { ChatCommandDefinition, CommandArgsParsing } from "openclaw/plugin-sdk/command-auth"; import type { ModelsProviderData } from "openclaw/plugin-sdk/command-auth"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime"; -import * as pluginRuntimeModule from "openclaw/plugin-sdk/plugin-runtime"; -import * as dispatcherModule from "openclaw/plugin-sdk/reply-dispatch-runtime"; import * as globalsModule from "openclaw/plugin-sdk/runtime-env"; import * as commandTextModule from "openclaw/plugin-sdk/text-runtime"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import * as modelPickerPreferencesModule from "./model-picker-preferences.js"; import * as modelPickerModule from "./model-picker.js"; import { createModelsProviderData as createBaseModelsProviderData } from "./model-picker.test-utils.js"; -import * as nativeCommandRouteModule from "./native-command-route.js"; -import { replyWithDiscordModelPickerProviders } from "./native-command-ui.js"; import { - __testing as nativeCommandTesting, createDiscordModelPickerFallbackButton, createDiscordModelPickerFallbackSelect, -} from "./native-command.js"; + replyWithDiscordModelPickerProviders, + type DispatchDiscordCommandInteraction, +} from "./native-command-ui.js"; import { createNoopThreadBindingManager, type ThreadBindingManager } from "./thread-bindings.js"; -vi.mock("openclaw/plugin-sdk/agent-runtime", () => ({ - resolveDefaultModelForAgent: () => ({ - provider: "anthropic", - model: "claude-sonnet-4.5", - }), - resolveHumanDelayConfig: () => undefined, -})); - -type ModelPickerContext = Parameters[0]; +type ModelPickerContext = Parameters[0]["ctx"]; type PickerButton = ReturnType; type PickerSelect = ReturnType; type PickerButtonInteraction = Parameters[0]; @@ -165,12 +154,43 @@ function createModelsViewSubmitData(): PickerButtonData { }; } +async function safeInteractionCall(_label: string, fn: () => Promise): Promise { + return await fn(); +} + +function createDispatchSpy() { + return vi.fn().mockResolvedValue(); +} + +function createModelPickerFallbackButton( + context: ModelPickerContext, + dispatchCommandInteraction: DispatchDiscordCommandInteraction = createDispatchSpy(), +) { + return createDiscordModelPickerFallbackButton({ + ctx: context, + safeInteractionCall, + dispatchCommandInteraction, + }); +} + +function createModelPickerFallbackSelect( + context: ModelPickerContext, + dispatchCommandInteraction: DispatchDiscordCommandInteraction = createDispatchSpy(), +) { + return createDiscordModelPickerFallbackSelect({ + ctx: context, + safeInteractionCall, + dispatchCommandInteraction, + }); +} + async function runSubmitButton(params: { context: ModelPickerContext; data: PickerButtonData; + dispatchCommandInteraction?: DispatchDiscordCommandInteraction; userId?: string; }) { - const button = createDiscordModelPickerFallbackButton(params.context); + const button = createModelPickerFallbackButton(params.context, params.dispatchCommandInteraction); const submitInteraction = createInteraction({ userId: params.userId ?? "owner" }); await button.run(submitInteraction as unknown as PickerButtonInteraction, params.data); return submitInteraction; @@ -179,10 +199,11 @@ async function runSubmitButton(params: { async function runModelSelect(params: { context: ModelPickerContext; data?: PickerSelectData; + dispatchCommandInteraction?: DispatchDiscordCommandInteraction; userId?: string; values?: string[]; }) { - const select = createDiscordModelPickerFallbackSelect(params.context); + const select = createModelPickerFallbackSelect(params.context, params.dispatchCommandInteraction); const selectInteraction = createInteraction({ userId: params.userId ?? "owner", values: params.values ?? ["gpt-4o"], @@ -195,24 +216,12 @@ async function runModelSelect(params: { } function expectDispatchedModelSelection(params: { - dispatchSpy: { mock: { calls: Array<[unknown]> } }; + dispatchSpy: ReturnType; model: string; - requireTargetSessionKey?: boolean; }) { - const dispatchCall = params.dispatchSpy.mock.calls[0]?.[0] as { - ctx?: { - CommandBody?: string; - CommandArgs?: { values?: { model?: string } }; - CommandTargetSessionKey?: string; - }; - }; - expect(dispatchCall.ctx?.CommandBody).toBe(`/model ${params.model}`); - expect(dispatchCall.ctx?.CommandArgs?.values?.model).toBe(params.model); - if (params.requireTargetSessionKey) { - if (!dispatchCall.ctx?.CommandTargetSessionKey) { - throw new Error("model selection dispatch did not include a target session key"); - } - } + const dispatchCall = params.dispatchSpy.mock.calls[0]?.[0]; + expect(dispatchCall?.prompt).toBe(`/model ${params.model}`); + expect(dispatchCall?.commandArgs?.values?.model).toBe(params.model); } function createBoundThreadBindingManager(params: { @@ -246,26 +255,10 @@ function createBoundThreadBindingManager(params: { }; } -function createDispatchSpy() { - const dispatchSpy = vi - .fn() - .mockResolvedValue({} as never); - nativeCommandTesting.setDispatchReplyWithDispatcher(dispatchSpy); - return dispatchSpy; -} - describe("Discord model picker interactions", () => { beforeEach(() => { vi.useRealTimers(); vi.restoreAllMocks(); - nativeCommandTesting.setMatchPluginCommand(pluginRuntimeModule.matchPluginCommand); - nativeCommandTesting.setExecutePluginCommand(pluginRuntimeModule.executePluginCommand); - nativeCommandTesting.setDispatchReplyWithDispatcher( - dispatcherModule.dispatchReplyWithDispatcher, - ); - nativeCommandTesting.setResolveDiscordNativeInteractionRouteState( - nativeCommandRouteModule.resolveDiscordNativeInteractionRouteState, - ); }); afterEach(() => { @@ -274,8 +267,8 @@ describe("Discord model picker interactions", () => { it("registers distinct fallback ids for button and select handlers", () => { const context = createModelPickerContext(); - const button = createDiscordModelPickerFallbackButton(context); - const select = createDiscordModelPickerFallbackSelect(context); + const button = createModelPickerFallbackButton(context); + const select = createModelPickerFallbackSelect(context); expect(button.customId).not.toBe(select.customId); expect(button.customId.split(":")[0]).toBe( @@ -289,7 +282,7 @@ describe("Discord model picker interactions", () => { it("ignores interactions from users other than the picker owner", async () => { const context = createModelPickerContext(); const loadSpy = vi.spyOn(modelPickerModule, "loadDiscordModelPickerData"); - const button = createDiscordModelPickerFallbackButton(context); + const button = createModelPickerFallbackButton(context); const interaction = createInteraction({ userId: "intruder" }); const data: PickerButtonData = { @@ -317,7 +310,10 @@ describe("Discord model picker interactions", () => { const dispatchSpy = createDispatchSpy(); - const selectInteraction = await runModelSelect({ context }); + const selectInteraction = await runModelSelect({ + context, + dispatchCommandInteraction: dispatchSpy, + }); expect(selectInteraction.update).toHaveBeenCalledTimes(1); expect(dispatchSpy).not.toHaveBeenCalled(); @@ -325,6 +321,7 @@ describe("Discord model picker interactions", () => { const submitInteraction = await runSubmitButton({ context, data: createModelsViewSubmitData(), + dispatchCommandInteraction: dispatchSpy, }); expect(submitInteraction.update).toHaveBeenCalledTimes(1); @@ -332,7 +329,6 @@ describe("Discord model picker interactions", () => { expectDispatchedModelSelection({ dispatchSpy, model: "openai/gpt-4o", - requireTargetSessionKey: true, }); }); @@ -352,9 +348,9 @@ describe("Discord model picker interactions", () => { .spyOn(commandTextModule, "withTimeout") .mockRejectedValue(new Error("timeout")); - await runModelSelect({ context }); + await runModelSelect({ context, dispatchCommandInteraction: dispatchSpy }); - const button = createDiscordModelPickerFallbackButton(context); + const button = createModelPickerFallbackButton(context, dispatchSpy); const submitInteraction = createInteraction({ userId: "owner" }); const submitData = createModelsViewSubmitData(); @@ -384,7 +380,7 @@ describe("Discord model picker interactions", () => { "anthropic/claude-sonnet-4-5", ]); - const button = createDiscordModelPickerFallbackButton(context); + const button = createModelPickerFallbackButton(context); const interaction = createInteraction({ userId: "owner" }); const data: PickerButtonData = { @@ -433,6 +429,7 @@ describe("Discord model picker interactions", () => { pg: "1", rs: "2", }, + dispatchCommandInteraction: dispatchSpy, }); expect(submitInteraction.update).toHaveBeenCalledTimes(1); @@ -453,10 +450,10 @@ describe("Discord model picker interactions", () => { vi.spyOn(modelPickerModule, "loadDiscordModelPickerData").mockResolvedValue(pickerData); mockModelCommandPipeline(modelCommand); - createDispatchSpy(); + const dispatchSpy = createDispatchSpy(); const verboseSpy = vi.spyOn(globalsModule, "logVerbose").mockImplementation(() => {}); - const select = createDiscordModelPickerFallbackSelect(context); + const select = createModelPickerFallbackSelect(context, dispatchSpy); const selectInteraction = createInteraction({ userId: "owner", values: ["gpt-4o"], @@ -468,7 +465,7 @@ describe("Discord model picker interactions", () => { const selectData = createModelsViewSelectData(); await select.run(selectInteraction as unknown as PickerSelectInteraction, selectData); - const button = createDiscordModelPickerFallbackButton(context); + const button = createModelPickerFallbackButton(context, dispatchSpy); const submitInteraction = createInteraction({ userId: "owner" }); submitInteraction.channel = { type: ChannelType.PublicThread, diff --git a/extensions/discord/src/send.creates-thread.test.ts b/extensions/discord/src/send.creates-thread.test.ts index f2dddb2092..1b399cc091 100644 --- a/extensions/discord/src/send.creates-thread.test.ts +++ b/extensions/discord/src/send.creates-thread.test.ts @@ -1,7 +1,7 @@ import { RateLimitError } from "@buape/carbon"; import { ChannelType, Routes } from "discord-api-types/v10"; import { loadWebMediaRaw } from "openclaw/plugin-sdk/web-media"; -import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { makeDiscordRest } from "./send.test-harness.js"; vi.mock("openclaw/plugin-sdk/web-media", async () => { @@ -63,6 +63,14 @@ beforeEach(() => { vi.clearAllMocks(); }); +afterEach(() => { + vi.useRealTimers(); +}); + +afterAll(() => { + vi.doUnmock("openclaw/plugin-sdk/web-media"); +}); + describe("sendMessageDiscord", () => { it("creates a thread", async () => { const { rest, getMock, postMock } = makeDiscordRest(); @@ -475,29 +483,29 @@ describe("retry rate limits", () => { }); it("uses retry_after delays when rate limited", async () => { - vi.useFakeTimers(); const setTimeoutSpy = vi.spyOn(global, "setTimeout"); - const { rest, postMock } = makeDiscordRest(); - const rateLimitError = createMockRateLimitError(0.5); + try { + const { rest, postMock } = makeDiscordRest(); + const rateLimitError = createMockRateLimitError(0.001); - postMock - .mockRejectedValueOnce(rateLimitError) - .mockResolvedValueOnce({ id: "msg1", channel_id: "789" }); + postMock + .mockRejectedValueOnce(rateLimitError) + .mockResolvedValueOnce({ id: "msg1", channel_id: "789" }); - const promise = sendMessageDiscord("channel:789", "hello", { - rest, - token: "t", - retry: { attempts: 2, minDelayMs: 0, maxDelayMs: 1000, jitter: 0 }, - }); + const promise = sendMessageDiscord("channel:789", "hello", { + rest, + token: "t", + retry: { attempts: 2, minDelayMs: 0, maxDelayMs: 1000, jitter: 0 }, + }); - await vi.runAllTimersAsync(); - await expect(promise).resolves.toEqual({ - messageId: "msg1", - channelId: "789", - }); - expect(setTimeoutSpy.mock.calls[0]?.[1]).toBe(500); - setTimeoutSpy.mockRestore(); - vi.useRealTimers(); + await expect(promise).resolves.toEqual({ + messageId: "msg1", + channelId: "789", + }); + expect(setTimeoutSpy.mock.calls[0]?.[1]).toBe(1); + } finally { + setTimeoutSpy.mockRestore(); + } }); it("stops after max retry attempts", async () => { diff --git a/src/cron/normalize.test.ts b/src/cron/normalize.test.ts index 9caedefcd1..4bb9401728 100644 --- a/src/cron/normalize.test.ts +++ b/src/cron/normalize.test.ts @@ -438,6 +438,17 @@ describe("normalizeCronJobCreate", () => { expect(payload.timeoutSeconds).toBe(0); }); + it("preserves fractional timeoutSeconds for short agentTurn deadlines", () => { + const normalized = normalizeCronJobCreate({ + name: "fractional timeout", + schedule: { kind: "every", everyMs: 60_000 }, + payload: { kind: "agentTurn", message: "hello", timeoutSeconds: 0.03 }, + }) as unknown as Record; + + const payload = normalized.payload as Record; + expect(payload.timeoutSeconds).toBe(0.03); + }); + it("preserves empty toolsAllow lists for create jobs", () => { const normalized = normalizeCronJobCreate({ name: "empty-tools", diff --git a/src/gateway/server-methods/cron.ts b/src/gateway/server-methods/cron.ts index a62e523faf..56e91598be 100644 --- a/src/gateway/server-methods/cron.ts +++ b/src/gateway/server-methods/cron.ts @@ -252,11 +252,7 @@ export const cronHandlers: GatewayRequestHandlers = { result = await context.cron.enqueueRun(jobId, p.mode ?? "force"); } catch (error) { if (isInvalidCronSessionTargetIdError(error)) { - respond( - false, - undefined, - errorShape(ErrorCodes.INVALID_REQUEST, formatErrorMessage(error)), - ); + respond(true, { ok: true, ran: false, reason: "invalid-spec" }, undefined); return; } throw error; diff --git a/src/plugins/runtime/runtime-task-test-harness.ts b/src/plugins/runtime/runtime-task-test-harness.ts index b91351b570..1b0562cc81 100644 --- a/src/plugins/runtime/runtime-task-test-harness.ts +++ b/src/plugins/runtime/runtime-task-test-harness.ts @@ -12,13 +12,10 @@ const runtimeTaskMocks = vi.hoisted(() => ({ killSubagentRunAdminMock: vi.fn(), })); -vi.mock("../../acp/control-plane/manager.js", () => ({ +vi.mock("../../tasks/task-registry-control.runtime.js", () => ({ getAcpSessionManager: () => ({ cancelSession: runtimeTaskMocks.cancelSessionMock, }), -})); - -vi.mock("../../agents/subagent-control.js", () => ({ killSubagentRunAdmin: (params: unknown) => runtimeTaskMocks.killSubagentRunAdminMock(params), })); diff --git a/test/setup.shared.ts b/test/setup.shared.ts index 96c294e22c..cdd96346a2 100644 --- a/test/setup.shared.ts +++ b/test/setup.shared.ts @@ -34,7 +34,7 @@ process.env.VITEST = "true"; process.env.OPENCLAW_PLUGIN_MANIFEST_CACHE_MS ??= "60000"; // Vitest fork workers can load transitive lockfile helpers many times per worker. // Raise listener budget to avoid noisy MaxListeners warnings and warning-stack overhead. -const TEST_PROCESS_MAX_LISTENERS = 128; +const TEST_PROCESS_MAX_LISTENERS = 256; if (process.getMaxListeners() > 0 && process.getMaxListeners() < TEST_PROCESS_MAX_LISTENERS) { process.setMaxListeners(TEST_PROCESS_MAX_LISTENERS); } diff --git a/test/vitest-projects-config.test.ts b/test/vitest-projects-config.test.ts index a5adf59942..21304ee1c0 100644 --- a/test/vitest-projects-config.test.ts +++ b/test/vitest-projects-config.test.ts @@ -17,13 +17,14 @@ describe("projects vitest config", () => { expect(baseConfig.test?.projects).toEqual([...rootVitestProjects]); }); - it("keeps root projects on the shared thread-first pool by default", () => { + it("keeps root projects on their expected pool defaults", () => { expect(createGatewayVitestConfig().test.pool).toBe("threads"); expect(createAgentsVitestConfig().test.pool).toBe("threads"); expect(createCommandsLightVitestConfig().test.pool).toBe("threads"); expect(createCommandsVitestConfig().test.pool).toBe("threads"); expect(createPluginSdkLightVitestConfig().test.pool).toBe("threads"); expect(createUnitFastVitestConfig().test.pool).toBe("threads"); + expect(createContractsVitestConfig().test.pool).toBe("forks"); }); it("keeps the contracts lane on the non-isolated fork runner by default", () => {