From 35dcd0676423a88127d6ee510a44f8c08bf174ff Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 17 Apr 2026 04:59:26 +0100 Subject: [PATCH] test: trim agent test hotspots --- .../matrix/src/matrix/monitor/events.test.ts | 24 ++++++ .../matrix/src/matrix/monitor/events.ts | 4 + .../src/matrix/monitor/verification-events.ts | 24 ++++-- src/agents/acp-spawn.test.ts | 3 + src/agents/command/delivery.ts | 11 +-- .../pi-embedded-runner/extensions.test.ts | 4 + ...mpt.spawn-workspace.context-engine.test.ts | 75 +++++++++++-------- ...ession-history.tool-result-details.test.ts | 4 + src/agents/pi-model-discovery.auth.test.ts | 54 ------------- src/agents/tools/message-tool.test.ts | 2 - src/agents/tools/message-tool.ts | 29 +++---- src/agents/tools/web-shared.ts | 8 +- 12 files changed, 126 insertions(+), 116 deletions(-) diff --git a/extensions/matrix/src/matrix/monitor/events.test.ts b/extensions/matrix/src/matrix/monitor/events.test.ts index 0920a2cef33..aff00e9374b 100644 --- a/extensions/matrix/src/matrix/monitor/events.test.ts +++ b/extensions/matrix/src/matrix/monitor/events.test.ts @@ -73,8 +73,27 @@ function createHarness(params?: { emoji?: Array<[string, string]>; }; } | null>; + sasNoticeRetryDelayMs?: number; }) { const listeners = new Map void>(); + const pendingTasks = new Set>(); + const runDetachedTask = vi.fn((_label: string, task: () => Promise) => { + const promise = Promise.resolve() + .then(task) + .catch((error) => { + throw error; + }) + .finally(() => { + pendingTasks.delete(promise); + }); + pendingTasks.add(promise); + return promise; + }); + const flushTasks = async () => { + while (pendingTasks.size > 0) { + await Promise.all(Array.from(pendingTasks)); + } + }; const onRoomMessage = vi.fn(async () => {}); const listVerifications = vi.fn(async () => params?.verifications ?? []); const ensureVerificationDmTracked = vi.fn( @@ -154,6 +173,8 @@ function createHarness(params?: { (typeof params?.startupMs === "number" ? () => params.startupMs : undefined), formatNativeDependencyHint, onRoomMessage, + runDetachedTask, + sasNoticeRetryDelayMs: params?.sasNoticeRetryDelayMs ?? 0, }); const roomEventListener = listeners.get("room.event") as RoomEventListener | undefined; @@ -172,6 +193,8 @@ function createHarness(params?: { logger, formatNativeDependencyHint, logVerboseMessage, + flushTasks, + runDetachedTask, roomMessageListener: listeners.get("room.message") as RoomEventListener | undefined, failedDecryptListener: listeners.get("room.failed_decryption") as | FailedDecryptListener @@ -885,6 +908,7 @@ describe("registerMatrixMonitorEvents verification routing", () => { "!dm:example.org": ["@alice:example.org", "@bot:example.org"], }, verifications, + sasNoticeRetryDelayMs: 750, }); try { diff --git a/extensions/matrix/src/matrix/monitor/events.ts b/extensions/matrix/src/matrix/monitor/events.ts index 6f65278cbec..06b21d05e34 100644 --- a/extensions/matrix/src/matrix/monitor/events.ts +++ b/extensions/matrix/src/matrix/monitor/events.ts @@ -186,6 +186,7 @@ export function registerMatrixMonitorEvents(params: { formatNativeDependencyHint: PluginRuntime["system"]["formatNativeDependencyHint"]; onRoomMessage: (roomId: string, event: MatrixRawEvent) => void | Promise; runDetachedTask?: (label: string, task: () => Promise) => Promise; + sasNoticeRetryDelayMs?: number; }): void { const { cfg, @@ -205,6 +206,7 @@ export function registerMatrixMonitorEvents(params: { formatNativeDependencyHint, onRoomMessage, runDetachedTask, + sasNoticeRetryDelayMs, } = params; const postHealthySyncDecryptFailureTracker = createMatrixPostHealthySyncDecryptFailureTracker({ getHealthySyncSinceMs, @@ -217,6 +219,8 @@ export function registerMatrixMonitorEvents(params: { dmPolicy, readStoreAllowFrom, logVerboseMessage, + runDetachedTask, + sasNoticeRetryDelayMs, }); const runMonitorTask = (label: string, task: () => Promise) => { diff --git a/extensions/matrix/src/matrix/monitor/verification-events.ts b/extensions/matrix/src/matrix/monitor/verification-events.ts index e3c7c13a099..44a4cd86c17 100644 --- a/extensions/matrix/src/matrix/monitor/verification-events.ts +++ b/extensions/matrix/src/matrix/monitor/verification-events.ts @@ -280,6 +280,7 @@ async function resolveVerificationSasNoticeForSignal( senderId: string; flowId: string | null; stage: MatrixVerificationStage; + sasNoticeRetryDelayMs?: number; }, ): Promise<{ summary: MatrixVerificationSummaryLike | null; sasNotice: string | null }> { const summary = await resolveVerificationSummaryForSignal(client, params); @@ -292,7 +293,9 @@ async function resolveVerificationSasNoticeForSignal( }; } - await new Promise((resolve) => setTimeout(resolve, SAS_NOTICE_RETRY_DELAY_MS)); + await new Promise((resolve) => + setTimeout(resolve, params.sasNoticeRetryDelayMs ?? SAS_NOTICE_RETRY_DELAY_MS), + ); const retriedSummary = await resolveVerificationSummaryForSignal(client, params); return { summary: retriedSummary, @@ -385,6 +388,8 @@ export function createMatrixVerificationEventRouter(params: { dmPolicy: "open" | "pairing" | "allowlist" | "disabled"; readStoreAllowFrom: () => Promise; logVerboseMessage: (message: string) => void; + sasNoticeRetryDelayMs?: number; + runDetachedTask?: (label: string, task: () => Promise) => Promise; }) { const routerStartedAtMs = Date.now(); const routedVerificationEvents = new Set(); @@ -539,7 +544,7 @@ export function createMatrixVerificationEventRouter(params: { } rememberVerificationRoom(roomId, event, signal.flowId); - void (async () => { + const routeTask = async () => { if (!shouldEmitVerificationEventNotice(event)) { params.logVerboseMessage( `matrix: ignoring historical verification event room=${roomId} id=${event.event_id ?? "unknown"} type=${event.type ?? "unknown"}`, @@ -586,6 +591,7 @@ export function createMatrixVerificationEventRouter(params: { senderId, flowId, stage: signal.stage, + sasNoticeRetryDelayMs: params.sasNoticeRetryDelayMs, }).catch(() => ({ summary: null, sasNotice: null })); const notices: string[] = []; @@ -613,9 +619,17 @@ export function createMatrixVerificationEventRouter(params: { logVerboseMessage: params.logVerboseMessage, }); } - })().catch((err) => { - params.logVerboseMessage(`matrix: failed routing verification event: ${String(err)}`); - }); + }; + if (params.runDetachedTask) { + void params.runDetachedTask( + `verification event handler room=${roomId} id=${event.event_id ?? "unknown"}`, + routeTask, + ); + } else { + void routeTask().catch((err) => { + params.logVerboseMessage(`matrix: failed routing verification event: ${String(err)}`); + }); + } return true; } diff --git a/src/agents/acp-spawn.test.ts b/src/agents/acp-spawn.test.ts index d57b18c1697..9e090397170 100644 --- a/src/agents/acp-spawn.test.ts +++ b/src/agents/acp-spawn.test.ts @@ -4,6 +4,7 @@ import path from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import * as acpSessionManager from "../acp/control-plane/manager.js"; import type { AcpInitializeSessionInput } from "../acp/control-plane/manager.types.js"; +import * as channelPlugins from "../channels/plugins/index.js"; import { clearRuntimeConfigSnapshot, setRuntimeConfigSnapshot, @@ -82,6 +83,7 @@ const hoisted = vi.hoisted(() => { }); const callGatewaySpy = vi.spyOn(gatewayCall, "callGateway"); +const getChannelPluginSpy = vi.spyOn(channelPlugins, "getChannelPlugin"); const getAcpSessionManagerSpy = vi.spyOn(acpSessionManager, "getAcpSessionManager"); const loadSessionStoreSpy = vi.spyOn(sessionStore, "loadSessionStore"); const resolveStorePathSpy = vi.spyOn(sessionPaths, "resolveStorePath"); @@ -350,6 +352,7 @@ describe("spawnAcpDirect", () => { replaceSpawnConfig(createDefaultSpawnConfig()); resetTaskRegistryForTests(); hoisted.areHeartbeatsEnabledMock.mockReset().mockReturnValue(true); + getChannelPluginSpy.mockReset().mockReturnValue(undefined); hoisted.callGatewayMock.mockReset(); hoisted.callGatewayMock.mockImplementation(async (argsUnknown: unknown) => { diff --git a/src/agents/command/delivery.ts b/src/agents/command/delivery.ts index af8000fef0b..dd148209f2b 100644 --- a/src/agents/command/delivery.ts +++ b/src/agents/command/delivery.ts @@ -88,7 +88,8 @@ export function normalizeAgentCommandReplyPayloads(params: { if (!channel) { return payloads as ReplyPayload[]; } - const deliveryPlugin = getChannelPlugin(channel); + const applyChannelTransforms = params.applyChannelTransforms ?? true; + const deliveryPlugin = applyChannelTransforms ? getChannelPlugin(channel) : undefined; const sessionKey = params.outboundSession?.key ?? params.opts.sessionKey; const agentId = @@ -113,7 +114,6 @@ export function normalizeAgentCommandReplyPayloads(params: { }); } const responsePrefixContext = replyPrefix.responsePrefixContextProvider(); - const applyChannelTransforms = params.applyChannelTransforms ?? true; const transformReplyPayload = deliveryPlugin?.messaging?.transformReplyPayload ? (payload: ReplyPayload) => deliveryPlugin.messaging?.transformReplyPayload?.({ @@ -186,9 +186,10 @@ export async function deliverAgentCommandResult(params: { resolvedChannel: deliveryChannel, }; // Channel docking: delivery channels are resolved via plugin registry. - const deliveryPlugin = !isInternalMessageChannel(deliveryChannel) - ? getChannelPlugin(normalizeChannelId(deliveryChannel) ?? deliveryChannel) - : undefined; + const deliveryPlugin = + deliver && !isInternalMessageChannel(deliveryChannel) + ? getChannelPlugin(normalizeChannelId(deliveryChannel) ?? deliveryChannel) + : undefined; const isDeliveryChannelKnown = isInternalMessageChannel(deliveryChannel) || Boolean(deliveryPlugin); diff --git a/src/agents/pi-embedded-runner/extensions.test.ts b/src/agents/pi-embedded-runner/extensions.test.ts index e30eb508d8d..e2472042d6b 100644 --- a/src/agents/pi-embedded-runner/extensions.test.ts +++ b/src/agents/pi-embedded-runner/extensions.test.ts @@ -12,6 +12,10 @@ vi.mock("../../plugins/provider-runtime.js", () => ({ resolveProviderRuntimePlugin: () => undefined, })); +vi.mock("../../plugins/provider-hook-runtime.js", () => ({ + resolveProviderRuntimePlugin: () => undefined, +})); + function buildSafeguardFactories(cfg: OpenClawConfig) { const sessionManager = {} as SessionManager; const model = { diff --git a/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.context-engine.test.ts b/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.context-engine.test.ts index 4aa12036413..70f06a69097 100644 --- a/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.context-engine.test.ts +++ b/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.context-engine.test.ts @@ -5,6 +5,7 @@ import { clearMemoryPluginState, registerMemoryPromptSection, } from "../../../plugins/memory-state.js"; +import { derivePromptTokens } from "../../usage.js"; import { type AttemptContextEngine, buildLoopPromptCacheInfo, @@ -15,9 +16,8 @@ import { resolvePromptCacheTouchTimestamp, runAttemptContextEngineBootstrap, } from "./attempt.context-engine-helpers.js"; +import { buildAfterTurnRuntimeContext } from "./attempt.prompt-helpers.js"; import { - cleanupTempPaths, - createContextEngineAttemptRunner, createContextEngineBootstrapAndAssemble, expectCalledWithSessionKey, getHoisted, @@ -113,7 +113,6 @@ async function finalizeTurn( describe("runEmbeddedAttempt context engine sessionKey forwarding", () => { const sessionKey = "agent:main:discord:channel:test-ctx-engine"; - const tempPaths: string[] = []; beforeEach(() => { resetEmbeddedAttemptHarness(); clearMemoryPluginState(); @@ -121,7 +120,6 @@ describe("runEmbeddedAttempt context engine sessionKey forwarding", () => { }); afterEach(async () => { - await cleanupTempPaths(tempPaths); clearMemoryPluginState(); vi.restoreAllMocks(); }); @@ -493,33 +491,48 @@ describe("runEmbeddedAttempt context engine sessionKey forwarding", () => { }) => {}, ); - await createContextEngineAttemptRunner({ - sessionKey, - tempPaths, - contextEngine: { - assemble: async ({ messages }) => ({ - messages, - estimatedTokens: 1, - }), - afterTurn, - }, - sessionPrompt: async (session) => { - session.messages = [ - ...session.messages, - { - role: "assistant", - content: "done", - timestamp: 2, - usage: { - input: 10, - output: 5, - cacheRead: 40, - cacheWrite: 2, - total: 57, - }, - } as unknown as AgentMessage, - ]; - }, + const messagesSnapshot = [ + seedMessage, + { + role: "assistant", + content: "done", + timestamp: 2, + usage: { + input: 10, + output: 5, + cacheRead: 40, + cacheWrite: 2, + total: 57, + }, + } as unknown as AgentMessage, + ]; + const promptCache = buildLoopPromptCacheInfo({ + messagesSnapshot, + prePromptMessageCount: 1, + }); + + await finalizeTurn(sessionKey, createTestContextEngine({ afterTurn }), { + messagesSnapshot, + prePromptMessageCount: 1, + runtimeContext: buildAfterTurnRuntimeContext({ + attempt: { + sessionKey, + config: {} as never, + skillsSnapshot: undefined, + senderIsOwner: true, + provider: "openai", + modelId: "gpt-test", + thinkLevel: "off", + reasoningLevel: undefined, + extraSystemPrompt: undefined, + ownerNumbers: undefined, + }, + workspaceDir: "/tmp/workspace", + agentDir: "/tmp/agent", + tokenBudget: 2048, + currentTokenCount: derivePromptTokens(promptCache?.lastCallUsage), + promptCache, + }), }); expect(afterTurn).toHaveBeenCalledWith( diff --git a/src/agents/pi-embedded-runner/sanitize-session-history.tool-result-details.test.ts b/src/agents/pi-embedded-runner/sanitize-session-history.tool-result-details.test.ts index 5c93c07063d..54d1afb027a 100644 --- a/src/agents/pi-embedded-runner/sanitize-session-history.tool-result-details.test.ts +++ b/src/agents/pi-embedded-runner/sanitize-session-history.tool-result-details.test.ts @@ -11,6 +11,10 @@ vi.mock("../../plugins/provider-runtime.js", () => ({ validateProviderReplayTurnsWithPlugin: () => undefined, })); +vi.mock("../../plugins/provider-hook-runtime.js", () => ({ + resolveProviderRuntimePlugin: () => undefined, +})); + describe("sanitizeSessionHistory toolResult details stripping", () => { it("strips toolResult.details so untrusted payloads are not fed back to the model", async () => { const sm = SessionManager.inMemory(); diff --git a/src/agents/pi-model-discovery.auth.test.ts b/src/agents/pi-model-discovery.auth.test.ts index 4e0c61aa18f..65ab37d3e6a 100644 --- a/src/agents/pi-model-discovery.auth.test.ts +++ b/src/agents/pi-model-discovery.auth.test.ts @@ -5,7 +5,6 @@ import { describe, expect, it } from "vitest"; import { resolvePiCredentialMapFromStore } from "./pi-auth-credentials.js"; import { addEnvBackedPiCredentials, - normalizeDiscoveredPiModel, scrubLegacyStaticAuthJsonEntriesForDiscovery, } from "./pi-model-discovery.js"; @@ -154,57 +153,4 @@ describe("discoverAuthStorage", () => { } } }); - - it("normalizes stale discovered openai-codex rows when api metadata is missing", () => { - const normalized = normalizeDiscoveredPiModel( - { - id: "gpt-5.4", - name: "gpt-5.4", - provider: "openai-codex", - baseUrl: "https://chatgpt.com/backend-api", - reasoning: true, - input: ["text", "image"], - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: 1_050_000, - contextTokens: 272_000, - maxTokens: 128_000, - }, - "/tmp/agent", - ) as { - api?: string; - baseUrl?: string; - }; - - expect(normalized).toMatchObject({ - api: "openai-codex-responses", - baseUrl: "https://chatgpt.com/backend-api", - }); - }); - - it("canonicalizes stale discovered openai-codex backend-api/v1 rows", () => { - const normalized = normalizeDiscoveredPiModel( - { - id: "gpt-5.4", - name: "gpt-5.4", - provider: "openai-codex", - api: "openai-codex-responses", - baseUrl: "https://chatgpt.com/backend-api/v1", - reasoning: true, - input: ["text", "image"], - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: 1_050_000, - contextTokens: 272_000, - maxTokens: 128_000, - }, - "/tmp/agent", - ) as { - api?: string; - baseUrl?: string; - }; - - expect(normalized).toMatchObject({ - api: "openai-codex-responses", - baseUrl: "https://chatgpt.com/backend-api", - }); - }); }); diff --git a/src/agents/tools/message-tool.test.ts b/src/agents/tools/message-tool.test.ts index c4235bb0875..19a8e7bf1b1 100644 --- a/src/agents/tools/message-tool.test.ts +++ b/src/agents/tools/message-tool.test.ts @@ -337,7 +337,6 @@ describe("message tool agent routing", () => { describe("message tool explicit target guard", () => { it("requires an explicit target for upload-file when configured", async () => { const tool = createMessageTool({ - config: {} as never, runMessageAction: mocks.runMessageAction as never, requireExplicitTarget: true, currentChannelProvider: "slack", @@ -365,7 +364,6 @@ describe("message tool explicit target guard", () => { }); const tool = createMessageTool({ - config: {} as never, runMessageAction: mocks.runMessageAction as never, requireExplicitTarget: true, currentChannelProvider: "slack", diff --git a/src/agents/tools/message-tool.ts b/src/agents/tools/message-tool.ts index 2bc91f558c2..c081992368f 100644 --- a/src/agents/tools/message-tool.ts +++ b/src/agents/tools/message-tool.ts @@ -685,6 +685,21 @@ export function createMessageTool(options?: MessageToolOptions): AnyAgentTool { const action = readStringParam(params, "action", { required: true, }) as ChannelMessageActionName; + const requireExplicitTarget = options?.requireExplicitTarget === true; + if (requireExplicitTarget && actionNeedsExplicitTarget(action)) { + const explicitTarget = + (typeof params.target === "string" && params.target.trim().length > 0) || + (typeof params.to === "string" && params.to.trim().length > 0) || + (typeof params.channelId === "string" && params.channelId.trim().length > 0) || + (Array.isArray(params.targets) && + params.targets.some((value) => typeof value === "string" && value.trim().length > 0)); + if (!explicitTarget) { + throw new Error( + "Explicit message target required for this run. Provide target/targets (and channel when needed).", + ); + } + } + let cfg = options?.config; if (!cfg) { const loadedRaw = loadConfigForTool(); @@ -711,20 +726,6 @@ export function createMessageTool(options?: MessageToolOptions): AnyAgentTool { }) ).resolvedConfig; } - const requireExplicitTarget = options?.requireExplicitTarget === true; - if (requireExplicitTarget && actionNeedsExplicitTarget(action)) { - const explicitTarget = - (typeof params.target === "string" && params.target.trim().length > 0) || - (typeof params.to === "string" && params.to.trim().length > 0) || - (typeof params.channelId === "string" && params.channelId.trim().length > 0) || - (Array.isArray(params.targets) && - params.targets.some((value) => typeof value === "string" && value.trim().length > 0)); - if (!explicitTarget) { - throw new Error( - "Explicit message target required for this run. Provide target/targets (and channel when needed).", - ); - } - } const accountId = readStringParam(params, "accountId") ?? agentAccountId; if (accountId) { diff --git a/src/agents/tools/web-shared.ts b/src/agents/tools/web-shared.ts index d335df4a927..cd0bcd61627 100644 --- a/src/agents/tools/web-shared.ts +++ b/src/agents/tools/web-shared.ts @@ -151,11 +151,9 @@ export async function readResponseText( // Best-effort: return whatever we decoded so far. } finally { if (truncated) { - try { - await reader.cancel(); - } catch { - // ignore - } + // Some mocked or non-compliant streams never settle cancel(); do not + // let cleanup turn a bounded read into a hung fetch. + void reader.cancel().catch(() => undefined); } }