From e750dab6d12617babc0982d5d138d400a876b985 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 9 Aug 2026 08:47:20 -0700 Subject: [PATCH] test(channels): consolidate regression fixtures (#114409) --- .../reply/get-reply-run.media-only.test.ts | 2436 ++++++++--------- src/auto-reply/reply/queue.collect.test.ts | 691 +++-- .../plugins/message-actions.security.test.ts | 147 +- .../progress-draft-compositor.test.ts | 184 +- src/channels/turn/kernel.test.ts | 134 +- 5 files changed, 1596 insertions(+), 1996 deletions(-) diff --git a/src/auto-reply/reply/get-reply-run.media-only.test.ts b/src/auto-reply/reply/get-reply-run.media-only.test.ts index b9800cc6ec6f..66e89d40e9ce 100644 --- a/src/auto-reply/reply/get-reply-run.media-only.test.ts +++ b/src/auto-reply/reply/get-reply-run.media-only.test.ts @@ -290,6 +290,10 @@ function baseParams( } as Parameters[0]; } +function runPrepared(overrides: Partial[0]> = {}) { + return runPreparedReply(baseParams(overrides)); +} + function ownerParams(): Parameters[0] { const params = baseParams(); params.command = { @@ -364,13 +368,11 @@ describe("runPreparedReply media-only handling", () => { }); it("passes approved elevated defaults to the runner", async () => { - await runPreparedReply( - baseParams({ - resolvedElevatedLevel: "on", - elevatedEnabled: true, - elevatedAllowed: true, - }), - ); + await runPrepared({ + resolvedElevatedLevel: "on", + elevatedEnabled: true, + elevatedAllowed: true, + }); const call = requireRunReplyAgentCall(); expect(call.followupRun.run.bashElevated).toEqual({ @@ -382,17 +384,15 @@ describe("runPreparedReply media-only handling", () => { }); it("includes current exec overrides in the queued runner prompt", async () => { - await runPreparedReply( - baseParams({ - execOverrides: { - host: "gateway", - security: "full", - ask: "always", - node: "worker-1", - }, - resolvedElevatedLevel: "off", - }), - ); + await runPrepared({ + execOverrides: { + host: "gateway", + security: "full", + ask: "always", + node: "worker-1", + }, + resolvedElevatedLevel: "off", + }); const prompt = requireRunReplyAgentCall().followupRun.run.extraSystemPromptStatic; expect(prompt).toContain( @@ -405,30 +405,26 @@ describe("runPreparedReply media-only handling", () => { it("preserves parent session provenance in queued runs", async () => { const spawnedBy = "agent:main:telegram:group:parent"; - await runPreparedReply( - baseParams({ - sessionEntry: { - sessionId: "child-session", - updatedAt: Date.now(), - spawnedBy, - } as SessionEntry, - }), - ); + await runPrepared({ + sessionEntry: { + sessionId: "child-session", + updatedAt: Date.now(), + spawnedBy, + } as SessionEntry, + }); expect(requireRunReplyAgentCall().followupRun.run.spawnedBy).toBe(spawnedBy); }); it("propagates non-visible assistant silence for group runs", async () => { - await runPreparedReply(baseParams()); + await runPrepared(); let call = requireLastRunReplyAgentCall(); expect(call?.followupRun.run.allowEmptyAssistantReplyAsSilent).toBe(true); - await runPreparedReply( - baseParams({ - defaultActivation: "mention", - }), - ); + await runPrepared({ + defaultActivation: "mention", + }); call = requireLastRunReplyAgentCall(); expect(call?.followupRun.run.allowEmptyAssistantReplyAsSilent).toBe(true); @@ -443,24 +439,22 @@ describe("runPreparedReply media-only handling", () => { }, ]); - await runPreparedReply( - baseParams({ - provider: "openai", - model: "chat-latest", - resolvedThinkLevel: "high", - modelState: { - resolveDefaultThinkingLevel: async () => "high", - resolveThinkingCatalog, - allowedModelCatalog: [ - { - provider: "openai", - id: "chat-latest", - name: "Chat Latest", - }, - ], - } as never, - }), - ); + await runPrepared({ + provider: "openai", + model: "chat-latest", + resolvedThinkLevel: "high", + modelState: { + resolveDefaultThinkingLevel: async () => "high", + resolveThinkingCatalog, + allowedModelCatalog: [ + { + provider: "openai", + id: "chat-latest", + name: "Chat Latest", + }, + ], + } as never, + }); expect(resolveThinkingCatalog).toHaveBeenCalledOnce(); const call = requireRunReplyAgentCall(); @@ -475,31 +469,29 @@ describe("runPreparedReply media-only handling", () => { }); it("reports unsupported explicit one-turn thinking overrides", async () => { - const result = await runPreparedReply( - baseParams({ - provider: "openai", - model: "chat-latest", - resolvedThinkLevel: "xhigh", - opts: { thinkingLevelOverride: "xhigh" }, - modelState: { - resolveDefaultThinkingLevel: async () => "high", - resolveThinkingCatalog: async () => [ - { - provider: "openai", - id: "chat-latest", - reasoning: false, - }, - ], - allowedModelCatalog: [ - { - provider: "openai", - id: "chat-latest", - name: "Chat Latest", - }, - ], - } as never, - }), - ); + const result = await runPrepared({ + provider: "openai", + model: "chat-latest", + resolvedThinkLevel: "xhigh", + opts: { thinkingLevelOverride: "xhigh" }, + modelState: { + resolveDefaultThinkingLevel: async () => "high", + resolveThinkingCatalog: async () => [ + { + provider: "openai", + id: "chat-latest", + reasoning: false, + }, + ], + allowedModelCatalog: [ + { + provider: "openai", + id: "chat-latest", + name: "Chat Latest", + }, + ], + } as never, + }); expect(Array.isArray(result) ? undefined : result?.text).toContain( 'Thinking level "xhigh" is not supported', @@ -518,33 +510,31 @@ describe("runPreparedReply media-only handling", () => { "session-key": sessionEntry, }; - await runPreparedReply( - baseParams({ - provider: "openai", - model: "chat-latest", - resolvedThinkLevel: "high", - sessionEntry, - sessionStore, - storePath: "/tmp/openclaw-sessions.json", - modelState: { - resolveDefaultThinkingLevel: async () => "high", - resolveThinkingCatalog: async () => [ - { - provider: "openai", - id: "chat-latest", - reasoning: false, - }, - ], - allowedModelCatalog: [ - { - provider: "openai", - id: "chat-latest", - name: "Chat Latest", - }, - ], - } as never, - }), - ); + await runPrepared({ + provider: "openai", + model: "chat-latest", + resolvedThinkLevel: "high", + sessionEntry, + sessionStore, + storePath: "/tmp/openclaw-sessions.json", + modelState: { + resolveDefaultThinkingLevel: async () => "high", + resolveThinkingCatalog: async () => [ + { + provider: "openai", + id: "chat-latest", + reasoning: false, + }, + ], + allowedModelCatalog: [ + { + provider: "openai", + id: "chat-latest", + name: "Chat Latest", + }, + ], + } as never, + }); const call = requireRunReplyAgentCall(); expect(call.followupRun.run.thinkLevel).toBe("off"); @@ -553,53 +543,49 @@ describe("runPreparedReply media-only handling", () => { }); it("keeps empty-assistant silence disabled for direct runs by default", async () => { - await runPreparedReply( - baseParams({ - ctx: { - ...createInboundBody(""), - ThreadHistoryBody: "Earlier direct message", - OriginatingChannel: "slack", - OriginatingTo: "D123", - ChatType: "direct", - }, - sessionCtx: { - ...createSessionBody(""), - ThreadHistoryBody: "Earlier direct message", - media: [{ path: "/tmp/input.png" }], - Provider: "slack", - ChatType: "direct", - OriginatingChannel: "slack", - OriginatingTo: "D123", - }, - }), - ); + await runPrepared({ + ctx: { + ...createInboundBody(""), + ThreadHistoryBody: "Earlier direct message", + OriginatingChannel: "slack", + OriginatingTo: "D123", + ChatType: "direct", + }, + sessionCtx: { + ...createSessionBody(""), + ThreadHistoryBody: "Earlier direct message", + media: [{ path: "/tmp/input.png" }], + Provider: "slack", + ChatType: "direct", + OriginatingChannel: "slack", + OriginatingTo: "D123", + }, + }); const call = requireLastRunReplyAgentCall(); expect(call?.followupRun.run.allowEmptyAssistantReplyAsSilent).toBe(false); }); it("passes message-tool-only delivery into direct chat prompt context", async () => { - await runPreparedReply( - baseParams({ - opts: { sourceReplyDeliveryMode: "message_tool_only" }, - ctx: { - ...createInboundBody("yo"), - ThreadHistoryBody: "Earlier direct message", - OriginatingChannel: "telegram", - OriginatingTo: "telegram-direct-test-id", - ChatType: "direct", - }, - sessionCtx: { - ...createSessionBody("yo"), - ThreadHistoryBody: "Earlier direct message", - media: [{ path: "/tmp/input.png" }], - Provider: "telegram", - ChatType: "direct", - OriginatingChannel: "telegram", - OriginatingTo: "telegram-direct-test-id", - }, - }), - ); + await runPrepared({ + opts: { sourceReplyDeliveryMode: "message_tool_only" }, + ctx: { + ...createInboundBody("yo"), + ThreadHistoryBody: "Earlier direct message", + OriginatingChannel: "telegram", + OriginatingTo: "telegram-direct-test-id", + ChatType: "direct", + }, + sessionCtx: { + ...createSessionBody("yo"), + ThreadHistoryBody: "Earlier direct message", + media: [{ path: "/tmp/input.png" }], + Provider: "telegram", + ChatType: "direct", + OriginatingChannel: "telegram", + OriginatingTo: "telegram-direct-test-id", + }, + }); expect(buildDirectChatContext).toHaveBeenCalledTimes(1); const directContextParams = requireMockCallArg( @@ -637,28 +623,26 @@ describe("runPreparedReply media-only handling", () => { "Current message:\nchat_id=-100123\ninbound_event_kind: user_request", ); - await runPreparedReply( - baseParams({ - opts: { sourceReplyDeliveryMode: "message_tool_only" }, - ctx: { - Body: "@bot please answer here", - RawBody: "@bot please answer here", - CommandBody: "please answer here", - OriginatingChannel: "telegram", - OriginatingTo: "-100123", - ChatType: "group", - }, - sessionCtx: { - Body: "@bot please answer here", - BodyStripped: "please answer here", - Provider: "telegram", - OriginatingChannel: "telegram", - OriginatingTo: "-100123", - ChatType: "group", - InboundEventKind: "user_request", - }, - }), - ); + await runPrepared({ + opts: { sourceReplyDeliveryMode: "message_tool_only" }, + ctx: { + Body: "@bot please answer here", + RawBody: "@bot please answer here", + CommandBody: "please answer here", + OriginatingChannel: "telegram", + OriginatingTo: "-100123", + ChatType: "group", + }, + sessionCtx: { + Body: "@bot please answer here", + BodyStripped: "please answer here", + Provider: "telegram", + OriginatingChannel: "telegram", + OriginatingTo: "-100123", + ChatType: "group", + InboundEventKind: "user_request", + }, + }); const call = requireLastRunReplyAgentCall(); expect(call.commandBody).toBe("please answer here"); @@ -685,31 +669,29 @@ describe("runPreparedReply media-only handling", () => { it.each(["direct", "dm"] as const)( "does not propagate empty-assistant silence for %s runs", async (chatType) => { - await runPreparedReply( - baseParams({ - ctx: { - ...createInboundBody(""), - ThreadHistoryBody: "Earlier direct message", - OriginatingChannel: "slack", - OriginatingTo: "D123", - ChatType: chatType, - }, - sessionCtx: { - ...createSessionBody(""), - ThreadHistoryBody: "Earlier direct message", - media: [{ path: "/tmp/input.png" }], - Provider: "slack", - ChatType: chatType, - OriginatingChannel: "slack", - OriginatingTo: "D123", - }, - cfg: { - session: {}, - channels: {}, - agents: {}, - }, - }), - ); + await runPrepared({ + ctx: { + ...createInboundBody(""), + ThreadHistoryBody: "Earlier direct message", + OriginatingChannel: "slack", + OriginatingTo: "D123", + ChatType: chatType, + }, + sessionCtx: { + ...createSessionBody(""), + ThreadHistoryBody: "Earlier direct message", + media: [{ path: "/tmp/input.png" }], + Provider: "slack", + ChatType: chatType, + OriginatingChannel: "slack", + OriginatingTo: "D123", + }, + cfg: { + session: {}, + channels: {}, + agents: {}, + }, + }); const call = requireLastRunReplyAgentCall(); expect(call?.followupRun.run.allowEmptyAssistantReplyAsSilent).toBe(false); @@ -717,40 +699,38 @@ describe("runPreparedReply media-only handling", () => { ); it("does not borrow target-session silence for native commands sent from direct chats", async () => { - await runPreparedReply( - baseParams({ - sessionKey: "agent:main:telegram:group:target", - ctx: { - ...createInboundBody(""), - ThreadHistoryBody: "Earlier direct message", - OriginatingChannel: "telegram", - OriginatingTo: "D123", - ChatType: "direct", - CommandSource: "native", - SessionKey: "agent:main:telegram:direct:source", - CommandTargetSessionKey: "agent:main:telegram:group:target", - }, - sessionCtx: { - ...createSessionBody(""), - ThreadHistoryBody: "Earlier direct message", - media: [{ path: "/tmp/input.png" }], - Provider: "telegram", - ChatType: "direct", - OriginatingChannel: "telegram", - OriginatingTo: "D123", - CommandSource: "native", - SessionKey: "agent:main:telegram:direct:source", - CommandTargetSessionKey: "agent:main:telegram:group:target", - }, - }), - ); + await runPrepared({ + sessionKey: "agent:main:telegram:group:target", + ctx: { + ...createInboundBody(""), + ThreadHistoryBody: "Earlier direct message", + OriginatingChannel: "telegram", + OriginatingTo: "D123", + ChatType: "direct", + CommandSource: "native", + SessionKey: "agent:main:telegram:direct:source", + CommandTargetSessionKey: "agent:main:telegram:group:target", + }, + sessionCtx: { + ...createSessionBody(""), + ThreadHistoryBody: "Earlier direct message", + media: [{ path: "/tmp/input.png" }], + Provider: "telegram", + ChatType: "direct", + OriginatingChannel: "telegram", + OriginatingTo: "D123", + CommandSource: "native", + SessionKey: "agent:main:telegram:direct:source", + CommandTargetSessionKey: "agent:main:telegram:group:target", + }, + }); const call = requireLastRunReplyAgentCall(); expect(call?.followupRun.run.allowEmptyAssistantReplyAsSilent).toBe(false); }); it("allows media-only prompts and preserves thread context in queued followups", async () => { - const result = await runPreparedReply(baseParams()); + const result = await runPrepared(); expect(result).toEqual({ text: "ok" }); const call = requireRunReplyAgentCall(); @@ -852,16 +832,14 @@ describe("runPreparedReply media-only handling", () => { vi.mocked(embeddedAgentRuntime.isEmbeddedAgentRunActive).mockReturnValueOnce(true); vi.mocked(embeddedAgentRuntime.isEmbeddedAgentRunStreaming).mockReturnValueOnce(true); - await runPreparedReply( - baseParams({ - sessionEntry: { - sessionId: "active-session", - updatedAt: Date.now(), - queueMode: "followup", - }, - opts: { queueModeOverride: "steer" }, - }), - ); + await runPrepared({ + sessionEntry: { + sessionId: "active-session", + updatedAt: Date.now(), + queueMode: "followup", + }, + opts: { queueModeOverride: "steer" }, + }); expect(queueSettings.resolveQueueSettings).toHaveBeenCalledWith( expect.objectContaining({ inlineMode: "steer" }), @@ -873,11 +851,9 @@ describe("runPreparedReply media-only handling", () => { }); it("keeps thread history context on follow-up turns", async () => { - const result = await runPreparedReply( - baseParams({ - isNewSession: false, - }), - ); + const result = await runPrepared({ + isNewSession: false, + }); expect(result).toEqual({ text: "ok" }); const call = requireRunReplyAgentCall(); @@ -886,29 +862,27 @@ describe("runPreparedReply media-only handling", () => { }); it("falls back to thread starter context on follow-up turns when history is absent", async () => { - const result = await runPreparedReply( - baseParams({ - isNewSession: false, - ctx: { - ...createInboundBody(""), - ThreadStarterBody: "starter message", - ThreadHistoryBody: undefined, - OriginatingChannel: "slack", - OriginatingTo: "C123", - ChatType: "group", - }, - sessionCtx: { - ...createSessionBody(""), - ThreadStarterBody: "starter message", - ThreadHistoryBody: undefined, - media: [{ path: "/tmp/input.png" }], - Provider: "slack", - ChatType: "group", - OriginatingChannel: "slack", - OriginatingTo: "C123", - }, - }), - ); + const result = await runPrepared({ + isNewSession: false, + ctx: { + ...createInboundBody(""), + ThreadStarterBody: "starter message", + ThreadHistoryBody: undefined, + OriginatingChannel: "slack", + OriginatingTo: "C123", + ChatType: "group", + }, + sessionCtx: { + ...createSessionBody(""), + ThreadStarterBody: "starter message", + ThreadHistoryBody: undefined, + media: [{ path: "/tmp/input.png" }], + Provider: "slack", + ChatType: "group", + OriginatingChannel: "slack", + OriginatingTo: "C123", + }, + }); expect(result).toEqual({ text: "ok" }); const call = requireRunReplyAgentCall(); @@ -917,29 +891,27 @@ describe("runPreparedReply media-only handling", () => { }); it("prefers thread history over thread starter on follow-up turns", async () => { - const result = await runPreparedReply( - baseParams({ - isNewSession: false, - ctx: { - ...createInboundBody(""), - ThreadStarterBody: "starter message", - ThreadHistoryBody: "Earlier message in this thread", - OriginatingChannel: "slack", - OriginatingTo: "C123", - ChatType: "group", - }, - sessionCtx: { - ...createSessionBody(""), - ThreadStarterBody: "starter message", - ThreadHistoryBody: "Earlier message in this thread", - media: [{ path: "/tmp/input.png" }], - Provider: "slack", - ChatType: "group", - OriginatingChannel: "slack", - OriginatingTo: "C123", - }, - }), - ); + const result = await runPrepared({ + isNewSession: false, + ctx: { + ...createInboundBody(""), + ThreadStarterBody: "starter message", + ThreadHistoryBody: "Earlier message in this thread", + OriginatingChannel: "slack", + OriginatingTo: "C123", + ChatType: "group", + }, + sessionCtx: { + ...createSessionBody(""), + ThreadStarterBody: "starter message", + ThreadHistoryBody: "Earlier message in this thread", + media: [{ path: "/tmp/input.png" }], + Provider: "slack", + ChatType: "group", + OriginatingChannel: "slack", + OriginatingTo: "C123", + }, + }); expect(result).toEqual({ text: "ok" }); const call = requireRunReplyAgentCall(); @@ -952,26 +924,24 @@ describe("runPreparedReply media-only handling", () => { ["Thread starter:", "```json", '{"body":"starter message"}', "```"].join("\n"), ); - const result = await runPreparedReply( - baseParams({ - ctx: { - ...createInboundBody(""), - ThreadStarterBody: "starter message", - OriginatingChannel: "slack", - OriginatingTo: "C123", - ChatType: "group", - }, - sessionCtx: { - ...createSessionBody(""), - ThreadStarterBody: "starter message", - media: [{ path: "/tmp/input.png" }], - Provider: "slack", - ChatType: "group", - OriginatingChannel: "slack", - OriginatingTo: "C123", - }, - }), - ); + const result = await runPrepared({ + ctx: { + ...createInboundBody(""), + ThreadStarterBody: "starter message", + OriginatingChannel: "slack", + OriginatingTo: "C123", + ChatType: "group", + }, + sessionCtx: { + ...createSessionBody(""), + ThreadStarterBody: "starter message", + media: [{ path: "/tmp/input.png" }], + Provider: "slack", + ChatType: "group", + OriginatingChannel: "slack", + OriginatingTo: "C123", + }, + }); expect(result).toEqual({ text: "ok" }); const call = requireRunReplyAgentCall(); @@ -980,17 +950,15 @@ describe("runPreparedReply media-only handling", () => { }); it("returns the empty-body reply when there is no text and no media", async () => { - const result = await runPreparedReply( - baseParams({ - ctx: { - ...createInboundBody(""), - }, - sessionCtx: { - ...createSessionBody(""), - Provider: "slack", - }, - }), - ); + const result = await runPrepared({ + ctx: { + ...createInboundBody(""), + }, + sessionCtx: { + ...createSessionBody(""), + Provider: "slack", + }, + }); expect(result).toEqual({ text: "I didn't receive any text in your message. Please resend or add a caption.", @@ -1211,20 +1179,18 @@ describe("runPreparedReply media-only handling", () => { ].join("\n"), ); - const result = await runPreparedReply( - baseParams({ - ctx: { - ...createInboundBody(""), - }, - sessionCtx: { - ...createSessionBody(""), - Provider: "paperclip", - OriginatingChannel: "paperclip", - OriginatingTo: "paperclip:issue:abc", - ChatType: "direct", - }, - }), - ); + const result = await runPrepared({ + ctx: { + ...createInboundBody(""), + }, + sessionCtx: { + ...createSessionBody(""), + Provider: "paperclip", + OriginatingChannel: "paperclip", + OriginatingTo: "paperclip:issue:abc", + ChatType: "direct", + }, + }); expect(result).toEqual({ text: "I didn't receive any text in your message. Please resend or add a caption.", @@ -1246,26 +1212,22 @@ describe("runPreparedReply media-only handling", () => { ].join("\n"), ); - const result = await runPreparedReply( - baseParams({ - ctx: { - ...createInboundBody(""), - ChatType: "group", - WasMentioned: true, - }, - sessionCtx: { - ...createSessionBody(""), - Provider: "feishu", - OriginatingChannel: "feishu", - OriginatingTo: "chat-1", - ChatType: "group", - WasMentioned: true, - InboundHistory: [ - { sender: "Alice", timestamp: 1_700_000_000_000, body: "what changed?" }, - ], - }, - }), - ); + const result = await runPrepared({ + ctx: { + ...createInboundBody(""), + ChatType: "group", + WasMentioned: true, + }, + sessionCtx: { + ...createSessionBody(""), + Provider: "feishu", + OriginatingChannel: "feishu", + OriginatingTo: "chat-1", + ChatType: "group", + WasMentioned: true, + InboundHistory: [{ sender: "Alice", timestamp: 1_700_000_000_000, body: "what changed?" }], + }, + }); expect(result).toEqual({ text: "ok" }); expect(vi.mocked(runReplyAgent)).toHaveBeenCalledOnce(); @@ -1288,24 +1250,22 @@ describe("runPreparedReply media-only handling", () => { ].join("\n"), ); - const result = await runPreparedReply( - baseParams({ - ctx: { - ...createInboundBody(""), - ChatType: "group", - WasMentioned: true, - }, - sessionCtx: { - ...createSessionBody(""), - Provider: "feishu", - OriginatingChannel: "feishu", - OriginatingTo: "chat-1", - ChatType: "group", - WasMentioned: true, - InboundHistory: [{ sender: "Alice", timestamp: 1_700_000_000_000, body: "\u0000 " }], - }, - }), - ); + const result = await runPrepared({ + ctx: { + ...createInboundBody(""), + ChatType: "group", + WasMentioned: true, + }, + sessionCtx: { + ...createSessionBody(""), + Provider: "feishu", + OriginatingChannel: "feishu", + OriginatingTo: "chat-1", + ChatType: "group", + WasMentioned: true, + InboundHistory: [{ sender: "Alice", timestamp: 1_700_000_000_000, body: "\u0000 " }], + }, + }); expect(result).toEqual({ text: "I didn't receive any text in your message. Please resend or add a caption.", @@ -1323,28 +1283,26 @@ describe("runPreparedReply media-only handling", () => { ].join("\n"), ); - const result = await runPreparedReply( - baseParams({ - ctx: { - ...createInboundBody(""), - }, - sessionCtx: { - ...createSessionBody(""), - Provider: "webchat", - OriginatingChannel: "webchat", - OriginatingTo: "webchat:local", - ChatType: "direct", - }, - opts: { - images: [ - { - type: "input_image", - image_url: "data:image/png;base64,AAAA", - }, - ] as never, - }, - }), - ); + const result = await runPrepared({ + ctx: { + ...createInboundBody(""), + }, + sessionCtx: { + ...createSessionBody(""), + Provider: "webchat", + OriginatingChannel: "webchat", + OriginatingTo: "webchat:local", + ChatType: "direct", + }, + opts: { + images: [ + { + type: "input_image", + image_url: "data:image/png;base64,AAAA", + }, + ] as never, + }, + }); expect(result).toEqual({ text: "ok" }); expect(vi.mocked(runReplyAgent)).toHaveBeenCalledOnce(); @@ -1365,25 +1323,23 @@ describe("runPreparedReply media-only handling", () => { ), ); - const result = await runPreparedReply( - baseParams({ - ctx: { - ...createInboundBody("describe this"), - media: [{ path: imagePath, workspaceDir: tmpDir }], - OriginatingChannel: "discord", - OriginatingTo: "C123", - ChatType: "group", - }, - sessionCtx: { - ...createSessionBody("describe this"), - Provider: "discord", - OriginatingChannel: "discord", - OriginatingTo: "C123", - ChatType: "group", - media: [{ path: imagePath, workspaceDir: tmpDir }], - }, - }), - ); + const result = await runPrepared({ + ctx: { + ...createInboundBody("describe this"), + media: [{ path: imagePath, workspaceDir: tmpDir }], + OriginatingChannel: "discord", + OriginatingTo: "C123", + ChatType: "group", + }, + sessionCtx: { + ...createSessionBody("describe this"), + Provider: "discord", + OriginatingChannel: "discord", + OriginatingTo: "C123", + ChatType: "group", + media: [{ path: imagePath, workspaceDir: tmpDir }], + }, + }); expect(result).toEqual({ text: "ok" }); expect(vi.mocked(runReplyAgent)).toHaveBeenCalledOnce(); @@ -1407,24 +1363,22 @@ describe("runPreparedReply media-only handling", () => { }); it("does not copy prior session media onto text-only followups", async () => { - await runPreparedReply( - baseParams({ - ctx: { - ...createInboundBody("follow up without media"), - OriginatingChannel: "telegram", - OriginatingTo: "42", - ChatType: "direct", - }, - sessionCtx: { - ...createSessionBody("follow up without media"), - Provider: "telegram", - OriginatingChannel: "telegram", - OriginatingTo: "42", - ChatType: "direct", - media: [{ path: "/tmp/previous-image.png", contentType: "image/png" }], - }, - }), - ); + await runPrepared({ + ctx: { + ...createInboundBody("follow up without media"), + OriginatingChannel: "telegram", + OriginatingTo: "42", + ChatType: "direct", + }, + sessionCtx: { + ...createSessionBody("follow up without media"), + Provider: "telegram", + OriginatingChannel: "telegram", + OriginatingTo: "42", + ChatType: "direct", + media: [{ path: "/tmp/previous-image.png", contentType: "image/png" }], + }, + }); const call = requireRunReplyAgentCall(); expect(call.followupRun.userTurnTranscriptRecorder?.message).toMatchObject({ @@ -1440,32 +1394,30 @@ describe("runPreparedReply media-only handling", () => { ["channel", true], ["direct", false], ] as const)("persists sender attribution for %s turns only", async (chatType, shouldPersist) => { - await runPreparedReply( - baseParams({ - ctx: { - ...createInboundBody("hello"), - OriginatingChannel: "telegram", - OriginatingTo: "chat-1", - ChatType: chatType, - }, - sessionCtx: { - ...createSessionBody("hello"), - Provider: "telegram", - OriginatingChannel: "telegram", - OriginatingTo: "chat-1", - ChatType: chatType, - SenderId: "user-42", - SenderName: "Ada", - SenderUsername: "ada", - }, - sessionEntry: { - sessionId: "session-1", - updatedAt: 1, - chatType, - channel: "telegram", - } as SessionEntry, - }), - ); + await runPrepared({ + ctx: { + ...createInboundBody("hello"), + OriginatingChannel: "telegram", + OriginatingTo: "chat-1", + ChatType: chatType, + }, + sessionCtx: { + ...createSessionBody("hello"), + Provider: "telegram", + OriginatingChannel: "telegram", + OriginatingTo: "chat-1", + ChatType: chatType, + SenderId: "user-42", + SenderName: "Ada", + SenderUsername: "ada", + }, + sessionEntry: { + sessionId: "session-1", + updatedAt: 1, + chatType, + channel: "telegram", + } as SessionEntry, + }); const message = requireRunReplyAgentCall().followupRun.userTurnTranscriptRecorder?.message; if (shouldPersist) { @@ -1484,24 +1436,22 @@ describe("runPreparedReply media-only handling", () => { }); it("normalizes second-based inbound timestamps before preparing user turns", async () => { - await runPreparedReply( - baseParams({ - ctx: { - ...createInboundBody("timestamped followup"), - OriginatingChannel: "whatsapp", - OriginatingTo: "+15550001", - ChatType: "direct", - Timestamp: 1_710_000_000, - }, - sessionCtx: { - ...createSessionBody("timestamped followup"), - Provider: "whatsapp", - OriginatingChannel: "whatsapp", - OriginatingTo: "+15550001", - ChatType: "direct", - }, - }), - ); + await runPrepared({ + ctx: { + ...createInboundBody("timestamped followup"), + OriginatingChannel: "whatsapp", + OriginatingTo: "+15550001", + ChatType: "direct", + Timestamp: 1_710_000_000, + }, + sessionCtx: { + ...createSessionBody("timestamped followup"), + Provider: "whatsapp", + OriginatingChannel: "whatsapp", + OriginatingTo: "+15550001", + ChatType: "direct", + }, + }); const call = requireRunReplyAgentCall(); expect(call.followupRun.userTurnTranscriptRecorder?.message).toMatchObject({ @@ -1531,47 +1481,45 @@ describe("runPreparedReply media-only handling", () => { ), ); - const result = await runPreparedReply( - baseParams({ - ctx: { - ...createInboundBody("describe this\n\n[Image]\nDescription:\na tiny dot image"), - media: [ - { path: imagePath, contentType: "image/png", workspaceDir: tmpDir }, - { path: secondImagePath, contentType: "image/png", workspaceDir: tmpDir }, - ], - MediaUnderstanding: [ - { - kind: "image.description", - attachmentIndex: 0, - provider: "openai", - model: "gpt-4o", - text: "a tiny dot image", - }, - { - kind: "image.description", - attachmentIndex: 1, - provider: "openai", - model: "gpt-4o", - text: "another tiny dot image", - }, - ], - OriginatingChannel: "webchat", - OriginatingTo: "webchat:local", - ChatType: "direct", - }, - sessionCtx: { - ...createSessionBody("describe this\n\n[Image]\nDescription:\na tiny dot image"), - Provider: "webchat", - OriginatingChannel: "webchat", - OriginatingTo: "webchat:local", - ChatType: "direct", - media: [ - { path: imagePath, contentType: "image/png", workspaceDir: tmpDir }, - { path: secondImagePath, contentType: "image/png", workspaceDir: tmpDir }, - ], - }, - }), - ); + const result = await runPrepared({ + ctx: { + ...createInboundBody("describe this\n\n[Image]\nDescription:\na tiny dot image"), + media: [ + { path: imagePath, contentType: "image/png", workspaceDir: tmpDir }, + { path: secondImagePath, contentType: "image/png", workspaceDir: tmpDir }, + ], + MediaUnderstanding: [ + { + kind: "image.description", + attachmentIndex: 0, + provider: "openai", + model: "gpt-4o", + text: "a tiny dot image", + }, + { + kind: "image.description", + attachmentIndex: 1, + provider: "openai", + model: "gpt-4o", + text: "another tiny dot image", + }, + ], + OriginatingChannel: "webchat", + OriginatingTo: "webchat:local", + ChatType: "direct", + }, + sessionCtx: { + ...createSessionBody("describe this\n\n[Image]\nDescription:\na tiny dot image"), + Provider: "webchat", + OriginatingChannel: "webchat", + OriginatingTo: "webchat:local", + ChatType: "direct", + media: [ + { path: imagePath, contentType: "image/png", workspaceDir: tmpDir }, + { path: secondImagePath, contentType: "image/png", workspaceDir: tmpDir }, + ], + }, + }); expect(result).toEqual({ text: "ok" }); expect(vi.mocked(runReplyAgent)).toHaveBeenCalledOnce(); @@ -1603,40 +1551,38 @@ describe("runPreparedReply media-only handling", () => { const secondImagePath = path.join(tmpDir, "second.png"); await writeFile(secondImagePath, secondImageData); - const result = await runPreparedReply( - baseParams({ - ctx: { - ...createInboundBody("describe this\n\n[Image]\nDescription:\na tiny dot image"), - media: [ - { path: imagePath, contentType: "image/png", workspaceDir: tmpDir }, - { path: secondImagePath, contentType: "image/png", workspaceDir: tmpDir }, - ], - MediaUnderstanding: [ - { - kind: "image.description", - attachmentIndex: 0, - provider: "openai", - model: "gpt-4o", - text: "a tiny dot image", - }, - ], - OriginatingChannel: "webchat", - OriginatingTo: "webchat:local", - ChatType: "direct", - }, - sessionCtx: { - ...createSessionBody("describe this\n\n[Image]\nDescription:\na tiny dot image"), - Provider: "webchat", - OriginatingChannel: "webchat", - OriginatingTo: "webchat:local", - ChatType: "direct", - media: [ - { path: imagePath, contentType: "image/png", workspaceDir: tmpDir }, - { path: secondImagePath, contentType: "image/png", workspaceDir: tmpDir }, - ], - }, - }), - ); + const result = await runPrepared({ + ctx: { + ...createInboundBody("describe this\n\n[Image]\nDescription:\na tiny dot image"), + media: [ + { path: imagePath, contentType: "image/png", workspaceDir: tmpDir }, + { path: secondImagePath, contentType: "image/png", workspaceDir: tmpDir }, + ], + MediaUnderstanding: [ + { + kind: "image.description", + attachmentIndex: 0, + provider: "openai", + model: "gpt-4o", + text: "a tiny dot image", + }, + ], + OriginatingChannel: "webchat", + OriginatingTo: "webchat:local", + ChatType: "direct", + }, + sessionCtx: { + ...createSessionBody("describe this\n\n[Image]\nDescription:\na tiny dot image"), + Provider: "webchat", + OriginatingChannel: "webchat", + OriginatingTo: "webchat:local", + ChatType: "direct", + media: [ + { path: imagePath, contentType: "image/png", workspaceDir: tmpDir }, + { path: secondImagePath, contentType: "image/png", workspaceDir: tmpDir }, + ], + }, + }); expect(result).toEqual({ text: "ok" }); expect(vi.mocked(runReplyAgent)).toHaveBeenCalledOnce(); @@ -1663,19 +1609,17 @@ describe("runPreparedReply media-only handling", () => { }); it("does not send a standalone reset notice for reply-producing /new turns", async () => { - await runPreparedReply( - baseParams({ - ctx: { - ...createInboundBody("/new"), - }, - command: { - ...(baseParams().command as Record), - commandBodyNormalized: "/new", - rawBodyNormalized: "/new", - } as never, - resetTriggered: true, - }), - ); + await runPrepared({ + ctx: { + ...createInboundBody("/new"), + }, + command: { + ...(baseParams().command as Record), + commandBodyNormalized: "/new", + rawBodyNormalized: "/new", + } as never, + resetTriggered: true, + }); const call = requireRunReplyAgentCall(); expect(call?.resetTriggered).toBe(true); @@ -1684,24 +1628,22 @@ describe("runPreparedReply media-only handling", () => { }); it("keeps /reset soft tails even when the bare reset prompt is empty", async () => { - const result = await runPreparedReply( - baseParams({ - ctx: { - ...createInboundBody("/reset soft re-read persona files"), - }, - sessionCtx: { - ...createSessionBody(""), - Provider: "slack", - }, - command: { - ...(baseParams().command as Record), - commandBodyNormalized: "/reset soft re-read persona files", - softResetTriggered: true, - softResetTail: "re-read persona files", - } as never, - workspaceDir: "" as never, - }), - ); + const result = await runPrepared({ + ctx: { + ...createInboundBody("/reset soft re-read persona files"), + }, + sessionCtx: { + ...createSessionBody(""), + Provider: "slack", + }, + command: { + ...(baseParams().command as Record), + commandBodyNormalized: "/reset soft re-read persona files", + softResetTriggered: true, + softResetTail: "re-read persona files", + } as never, + workspaceDir: "" as never, + }); expect(result).toEqual({ text: "ok" }); const call = requireRunReplyAgentCall(); @@ -1716,11 +1658,9 @@ describe("runPreparedReply media-only handling", () => { vi.mocked(runReplyAgent).mockRejectedValueOnce(createGatewayDrainingError()); await expect( - runPreparedReply( - baseParams({ - resetTriggered: true, - }), - ), + runPrepared({ + resetTriggered: true, + }), ).rejects.toThrow("Gateway is draining for restart; new tasks are not accepted"); expect(vi.mocked(routeReply)).not.toHaveBeenCalled(); @@ -1734,11 +1674,9 @@ describe("runPreparedReply media-only handling", () => { vi.mocked(resolveSessionAuthProfileOverride).mockRejectedValueOnce(new Error("auth failed")); await expect( - runPreparedReply( - baseParams({ - sessionId, - }), - ), + runPrepared({ + sessionId, + }), ).rejects.toThrow("auth failed"); expect(getActiveReplyRunCount()).toBe(activeBefore); @@ -1747,12 +1685,10 @@ describe("runPreparedReply media-only handling", () => { const queueSettings = await import("./queue/settings-runtime.js"); vi.mocked(queueSettings.resolveQueueSettings).mockReturnValueOnce({ mode: "interrupt" }); - const result = await runPreparedReply( - baseParams({ - isNewSession: false, - sessionId: "session-overlap", - }), - ); + const result = await runPrepared({ + isNewSession: false, + sessionId: "session-overlap", + }); expect(result).toEqual({ text: "ok" }); expect(vi.mocked(runReplyAgent)).toHaveBeenCalledOnce(); @@ -1769,12 +1705,10 @@ describe("runPreparedReply media-only handling", () => { }; setActiveEmbeddedRun("session-embedded-only", embeddedHandle, "session-key"); - const runPromise = runPreparedReply( - baseParams({ - isNewSession: false, - sessionId: "session-embedded-only", - }), - ); + const runPromise = runPrepared({ + isNewSession: false, + sessionId: "session-embedded-only", + }); await Promise.resolve(); expect(vi.mocked(runReplyAgent)).not.toHaveBeenCalled(); @@ -1873,13 +1807,11 @@ describe("runPreparedReply media-only handling", () => { }); try { - const result = await runPreparedReply( - baseParams({ - resetTriggered: true, - isNewSession: true, - sessionId: "session-reset-new", - }), - ); + const result = await runPrepared({ + resetTriggered: true, + isNewSession: true, + sessionId: "session-reset-new", + }); expect(result).toEqual({ text: "ok" }); expect(commandQueue.clearCommandLane).toHaveBeenCalledWith("session:session-key"); @@ -1909,11 +1841,9 @@ describe("runPreparedReply media-only handling", () => { vi.mocked(embeddedAgentRuntime.isEmbeddedAgentRunActive).mockReturnValueOnce(true); vi.mocked(embeddedAgentRuntime.isEmbeddedAgentRunStreaming).mockReturnValueOnce(true); - await runPreparedReply( - baseParams({ - opts: { isHeartbeat: true }, - }), - ); + await runPrepared({ + opts: { isHeartbeat: true }, + }); const call = vi.mocked(runReplyAgent).mock.calls.at(-1)?.[0]; expect(call?.shouldSteer).toBe(false); @@ -1949,23 +1879,21 @@ describe("runPreparedReply media-only handling", () => { vi.mocked(embeddedAgentRuntime.isEmbeddedAgentRunStreaming).mockReturnValueOnce(true); try { - await runPreparedReply( - baseParams({ - isNewSession: false, - ctx: { - ...createInboundTurn("second top-level DM", "slack", "direct"), - OriginatingChannel: "slack", - OriginatingTo: "user:U1", - ...threadContext, - }, - sessionCtx: { - ...createSessionTurn("second top-level DM", "slack", "direct"), - OriginatingChannel: "slack", - OriginatingTo: "user:U1", - ...threadContext, - }, - }), - ); + await runPrepared({ + isNewSession: false, + ctx: { + ...createInboundTurn("second top-level DM", "slack", "direct"), + OriginatingChannel: "slack", + OriginatingTo: "user:U1", + ...threadContext, + }, + sessionCtx: { + ...createSessionTurn("second top-level DM", "slack", "direct"), + OriginatingChannel: "slack", + OriginatingTo: "user:U1", + ...threadContext, + }, + }); } finally { activeRun.complete(); } @@ -2001,23 +1929,21 @@ describe("runPreparedReply media-only handling", () => { vi.mocked(embeddedAgentRuntime.isEmbeddedAgentRunStreaming).mockReturnValueOnce(true); try { - await runPreparedReply( - baseParams({ - isNewSession: false, - ctx: { - ...createInboundTurn("follow-up in another transport thread", "telegram", "direct"), - OriginatingChannel: "telegram", - OriginatingTo: "user:1", - MessageThreadId: 43, - }, - sessionCtx: { - ...createSessionTurn("follow-up in another transport thread", "telegram", "direct"), - OriginatingChannel: "telegram", - OriginatingTo: "user:1", - MessageThreadId: 43, - }, - }), - ); + await runPrepared({ + isNewSession: false, + ctx: { + ...createInboundTurn("follow-up in another transport thread", "telegram", "direct"), + OriginatingChannel: "telegram", + OriginatingTo: "user:1", + MessageThreadId: 43, + }, + sessionCtx: { + ...createSessionTurn("follow-up in another transport thread", "telegram", "direct"), + OriginatingChannel: "telegram", + OriginatingTo: "user:1", + MessageThreadId: 43, + }, + }); } finally { activeRun.complete(); } @@ -2044,12 +1970,10 @@ describe("runPreparedReply media-only handling", () => { ); vi.mocked(queueSettings.resolveQueueSettings).mockReturnValueOnce({ mode: "interrupt" }); - const runPromise = runPreparedReply( - baseParams({ - isNewSession: false, - sessionId: "session-auth-race", - }), - ); + const runPromise = runPrepared({ + isNewSession: false, + sessionId: "session-auth-race", + }); await Promise.resolve(); expect(vi.mocked(runReplyAgent)).not.toHaveBeenCalled(); @@ -2088,13 +2012,11 @@ describe("runPreparedReply media-only handling", () => { try { await expect( - runPreparedReply( - baseParams({ - isNewSession: false, - sessionId: "session-pre-dispatch-owner", - opts: { replyOperation: operation } as never, - }), - ), + runPrepared({ + isNewSession: false, + sessionId: "session-pre-dispatch-owner", + opts: { replyOperation: operation } as never, + }), ).resolves.toEqual({ text: "ok" }); const call = requireLastRunReplyAgentCall(); @@ -2118,13 +2040,11 @@ describe("runPreparedReply media-only handling", () => { try { await expect( - runPreparedReply( - baseParams({ - isNewSession: true, - sessionId: "session-after-rollover", - opts: { replyOperation: operation } as never, - }), - ), + runPrepared({ + isNewSession: true, + sessionId: "session-after-rollover", + opts: { replyOperation: operation } as never, + }), ).resolves.toEqual({ text: "ok" }); const call = requireLastRunReplyAgentCall(); @@ -2190,14 +2110,12 @@ describe("runPreparedReply media-only handling", () => { try { await expect( - runPreparedReply( - baseParams({ - resetTriggered: true, - isNewSession: true, - sessionId: "session-reset-owner", - opts: { replyOperation: operation } as never, - }), - ), + runPrepared({ + resetTriggered: true, + isNewSession: true, + sessionId: "session-reset-owner", + opts: { replyOperation: operation } as never, + }), ).resolves.toEqual({ text: "ok" }); const call = requireLastRunReplyAgentCall(); @@ -2282,14 +2200,12 @@ describe("runPreparedReply media-only handling", () => { }); previousRun.setPhase("running"); - const runPromise = runPreparedReply( - baseParams({ - isNewSession: false, - sessionId: "session-auth-profile", - sessionEntry: expectDefined(sessionStore["session-key"], "stored session entry"), - sessionStore, - }), - ); + const runPromise = runPrepared({ + isNewSession: false, + sessionId: "session-auth-profile", + sessionEntry: expectDefined(sessionStore["session-key"], "stored session entry"), + sessionStore, + }); await Promise.resolve(); sessionStore["session-key"] = { @@ -2329,16 +2245,14 @@ describe("runPreparedReply media-only handling", () => { vi.mocked(queueSettings.resolveQueueSettings).mockReturnValueOnce({ mode: "interrupt" }); const onSessionPrepared = vi.fn(); - const runPromise = runPreparedReply( - baseParams({ - isNewSession: false, - sessionId: "session-before-rotation", - sessionEntry: sessionStore["session-key"], - sessionStore, - storePath: "/tmp/sessions.json", - opts: { onSessionPrepared } as never, - }), - ); + const runPromise = runPrepared({ + isNewSession: false, + sessionId: "session-before-rotation", + sessionEntry: sessionStore["session-key"], + sessionStore, + storePath: "/tmp/sessions.json", + opts: { onSessionPrepared } as never, + }); await Promise.resolve(); const rotatedRun = createReplyOperation({ @@ -2385,12 +2299,10 @@ describe("runPreparedReply media-only handling", () => { }); previousRun.setPhase("running"); - const runPromise = runPreparedReply( - baseParams({ - isNewSession: false, - sessionId: "session-before-wait", - }), - ); + const runPromise = runPrepared({ + isNewSession: false, + sessionId: "session-before-wait", + }); await Promise.resolve(); expect(vi.mocked(runReplyAgent)).not.toHaveBeenCalled(); @@ -2426,12 +2338,10 @@ describe("runPreparedReply media-only handling", () => { }); previousRun.setPhase("running"); - const runPromise = runPreparedReply( - baseParams({ - isNewSession: false, - sessionId: "session-events-after-wait", - }), - ); + const runPromise = runPrepared({ + isNewSession: false, + sessionId: "session-events-after-wait", + }); await Promise.resolve(); previousRun.complete(); @@ -2452,19 +2362,17 @@ describe("runPreparedReply media-only handling", () => { ); vi.mocked(resolveInboundUserContextPromptJoiner).mockReturnValueOnce(" "); - await runPreparedReply( - baseParams({ - ctx: { - ...createInboundTurn("what does this mean?", "telegram", "group"), - }, - sessionCtx: { - ...createSessionTurn("what does this mean?", "telegram", "group"), - ReplyToSender: "Jake", - ReplyToBody: "quoted status body", - ReplyToIsQuote: true, - }, - }), - ); + await runPrepared({ + ctx: { + ...createInboundTurn("what does this mean?", "telegram", "group"), + }, + sessionCtx: { + ...createSessionTurn("what does this mean?", "telegram", "group"), + ReplyToSender: "Jake", + ReplyToBody: "quoted status body", + ReplyToIsQuote: true, + }, + }); const call = requireLastRunReplyAgentCall(); expect(call?.commandBody).toContain("what does this mean?"); @@ -2492,33 +2400,31 @@ describe("runPreparedReply media-only handling", () => { ].join("\n"), ); - const result = await runPreparedReply( - baseParams({ - ctx: { - Body: "", - RawBody: "@bot", - CommandBody: "@bot", - ...createProviderSurface("telegram"), - ChatType: "group", - ReplyToBody: "quoted status body", - ReplyToSender: "Bot", - }, - sessionCtx: { - ...createSessionBody(""), - RawBody: "@bot", - CommandBody: "@bot", - ...createProviderSurface("telegram"), - ChatType: "group", - ReplyToBody: "quoted status body", - ReplyToSender: "Bot", - }, - command: { - ...baseParams().command, - rawBodyNormalized: "@bot", - commandBodyNormalized: "", - } as never, - }), - ); + const result = await runPrepared({ + ctx: { + Body: "", + RawBody: "@bot", + CommandBody: "@bot", + ...createProviderSurface("telegram"), + ChatType: "group", + ReplyToBody: "quoted status body", + ReplyToSender: "Bot", + }, + sessionCtx: { + ...createSessionBody(""), + RawBody: "@bot", + CommandBody: "@bot", + ...createProviderSurface("telegram"), + ChatType: "group", + ReplyToBody: "quoted status body", + ReplyToSender: "Bot", + }, + command: { + ...baseParams().command, + rawBodyNormalized: "@bot", + commandBodyNormalized: "", + } as never, + }); expect(result).toEqual({ text: "ok" }); const call = requireLastRunReplyAgentCall(); @@ -2647,20 +2553,18 @@ describe("runPreparedReply media-only handling", () => { vi.mocked(embeddedAgentRuntime.waitForEmbeddedAgentRunEnd).mockClear(); vi.mocked(buildInboundUserContextPrefix).mockReturnValueOnce("room context"); - await runPreparedReply( - baseParams({ - opts: { abortSignal: abortController.signal }, - ctx: { - ...createInboundTurn("ambient", "telegram", "group"), - }, - sessionCtx: { - ...createSessionTurn("ambient", "telegram", "group"), - InboundEventKind: "room_event", - MessageSid: "992", - SenderName: "Alice", - }, - }), - ); + await runPrepared({ + opts: { abortSignal: abortController.signal }, + ctx: { + ...createInboundTurn("ambient", "telegram", "group"), + }, + sessionCtx: { + ...createSessionTurn("ambient", "telegram", "group"), + InboundEventKind: "room_event", + MessageSid: "992", + SenderName: "Alice", + }, + }); const call = requireLastRunReplyAgentCall(); expect(call.shouldSteer).toBe(false); @@ -2691,25 +2595,23 @@ describe("runPreparedReply media-only handling", () => { vi.mocked(embeddedAgentRuntime.isEmbeddedAgentRunStreaming).mockReturnValueOnce(true); vi.mocked(buildInboundUserContextPrefix).mockReturnValueOnce("room context"); - await runPreparedReply( - baseParams({ - opts: { - abortSignal: activeLaneAbortController.signal, - queuedFollowupAbortSignal: sourceAbortController.signal, - } as NonNullable[0]["opts"]> & { - queuedFollowupAbortSignal?: AbortSignal; - }, - ctx: { - ...createInboundTurn("ambient", "telegram", "group"), - }, - sessionCtx: { - ...createSessionTurn("ambient", "telegram", "group"), - InboundEventKind: "room_event", - MessageSid: "993", - SenderName: "Alice", - }, - }), - ); + await runPrepared({ + opts: { + abortSignal: activeLaneAbortController.signal, + queuedFollowupAbortSignal: sourceAbortController.signal, + } as NonNullable[0]["opts"]> & { + queuedFollowupAbortSignal?: AbortSignal; + }, + ctx: { + ...createInboundTurn("ambient", "telegram", "group"), + }, + sessionCtx: { + ...createSessionTurn("ambient", "telegram", "group"), + InboundEventKind: "room_event", + MessageSid: "993", + SenderName: "Alice", + }, + }); const call = requireLastRunReplyAgentCall(); expect(call.shouldFollowup).toBe(true); @@ -2735,20 +2637,18 @@ describe("runPreparedReply media-only handling", () => { vi.mocked(embeddedAgentRuntime.isEmbeddedAgentRunStreaming).mockReturnValueOnce(true); vi.mocked(buildInboundUserContextPrefix).mockReturnValueOnce("user request context"); - await runPreparedReply( - baseParams({ - opts: { abortSignal: abortController.signal }, - ctx: { - ...createInboundTurn("@bot keep this", "telegram", "group"), - }, - sessionCtx: { - ...createSessionTurn("@bot keep this", "telegram", "group"), - InboundEventKind: "user_request", - MessageSid: "994", - SenderName: "Alice", - }, - }), - ); + await runPrepared({ + opts: { abortSignal: abortController.signal }, + ctx: { + ...createInboundTurn("@bot keep this", "telegram", "group"), + }, + sessionCtx: { + ...createSessionTurn("@bot keep this", "telegram", "group"), + InboundEventKind: "user_request", + MessageSid: "994", + SenderName: "Alice", + }, + }); const call = requireLastRunReplyAgentCall(); expect(call.shouldFollowup).toBe(true); @@ -2773,19 +2673,17 @@ describe("runPreparedReply media-only handling", () => { vi.mocked(embeddedAgentRuntime.isEmbeddedAgentRunStreaming).mockReturnValueOnce(true); vi.mocked(buildInboundUserContextPrefix).mockReturnValueOnce("room context"); - await runPreparedReply( - baseParams({ - ctx: { - ...createInboundTurn("ambient", "telegram", "group"), - }, - sessionCtx: { - ...createSessionTurn("ambient", "telegram", "group"), - InboundEventKind: "room_event", - MessageSid: "993", - SenderName: "Alice", - }, - }), - ); + await runPrepared({ + ctx: { + ...createInboundTurn("ambient", "telegram", "group"), + }, + sessionCtx: { + ...createSessionTurn("ambient", "telegram", "group"), + InboundEventKind: "room_event", + MessageSid: "993", + SenderName: "Alice", + }, + }); const call = requireLastRunReplyAgentCall(); expect(call.shouldSteer).toBe(false); @@ -2799,20 +2697,18 @@ describe("runPreparedReply media-only handling", () => { it("keeps room events tool-only when group replies are automatic", async () => { vi.mocked(buildInboundUserContextPrefix).mockReturnValueOnce("room context"); - await runPreparedReply( - baseParams({ - opts: { sourceReplyDeliveryMode: "automatic" }, - ctx: { - ...createInboundTurn("ambient", "telegram", "group"), - }, - sessionCtx: { - ...createSessionTurn("ambient", "telegram", "group"), - InboundEventKind: "room_event", - MessageSid: "991", - SenderName: "Alice", - }, - }), - ); + await runPrepared({ + opts: { sourceReplyDeliveryMode: "automatic" }, + ctx: { + ...createInboundTurn("ambient", "telegram", "group"), + }, + sessionCtx: { + ...createSessionTurn("ambient", "telegram", "group"), + InboundEventKind: "room_event", + MessageSid: "991", + SenderName: "Alice", + }, + }); const call = requireLastRunReplyAgentCall(); expect(call?.followupRun.run.sourceReplyDeliveryMode).toBe("message_tool_only"); @@ -2823,20 +2719,18 @@ describe("runPreparedReply media-only handling", () => { }); it("keeps webchat room events on automatic source delivery", async () => { - await runPreparedReply( - baseParams({ - opts: { sourceReplyDeliveryMode: "automatic" }, - ctx: { - ...createInboundTurn("webchat prompt", "webchat", "direct"), - }, - sessionCtx: { - ...createSessionTurn("webchat prompt", "webchat", "direct"), - InboundEventKind: "room_event", - MessageSid: "webchat-room-event", - SenderName: "Operator", - }, - }), - ); + await runPrepared({ + opts: { sourceReplyDeliveryMode: "automatic" }, + ctx: { + ...createInboundTurn("webchat prompt", "webchat", "direct"), + }, + sessionCtx: { + ...createSessionTurn("webchat prompt", "webchat", "direct"), + InboundEventKind: "room_event", + MessageSid: "webchat-room-event", + SenderName: "Operator", + }, + }); const call = requireLastRunReplyAgentCall(); expect(call?.followupRun.run.sourceReplyDeliveryMode).toBe("automatic"); @@ -2848,26 +2742,24 @@ describe("runPreparedReply media-only handling", () => { it("keeps routed external room events tool-only when provider is webchat", async () => { vi.mocked(buildInboundUserContextPrefix).mockReturnValueOnce("room context"); - await runPreparedReply( - baseParams({ - opts: { sourceReplyDeliveryMode: "automatic" }, - ctx: { - ...createInboundBody("ambient"), - Provider: "webchat", - Surface: "telegram", - ChatType: "group", - }, - sessionCtx: { - ...createSessionBody("ambient"), - Provider: "webchat", - Surface: "telegram", - ChatType: "group", - InboundEventKind: "room_event", - MessageSid: "routed-room-event", - SenderName: "Alice", - }, - }), - ); + await runPrepared({ + opts: { sourceReplyDeliveryMode: "automatic" }, + ctx: { + ...createInboundBody("ambient"), + Provider: "webchat", + Surface: "telegram", + ChatType: "group", + }, + sessionCtx: { + ...createSessionBody("ambient"), + Provider: "webchat", + Surface: "telegram", + ChatType: "group", + InboundEventKind: "room_event", + MessageSid: "routed-room-event", + SenderName: "Alice", + }, + }); const call = requireLastRunReplyAgentCall(); expect(call?.followupRun.run.sourceReplyDeliveryMode).toBe("message_tool_only"); @@ -2878,19 +2770,17 @@ describe("runPreparedReply media-only handling", () => { }); it("keeps webchat direct replies automatic when message-tool mode is requested", async () => { - await runPreparedReply( - baseParams({ - opts: { sourceReplyDeliveryMode: "message_tool_only" }, - ctx: { - ...createInboundTurn("webchat prompt", "webchat", "direct"), - }, - sessionCtx: { - ...createSessionTurn("webchat prompt", "webchat", "direct"), - MessageSid: "webchat-direct", - SenderName: "Operator", - }, - }), - ); + await runPrepared({ + opts: { sourceReplyDeliveryMode: "message_tool_only" }, + ctx: { + ...createInboundTurn("webchat prompt", "webchat", "direct"), + }, + sessionCtx: { + ...createSessionTurn("webchat prompt", "webchat", "direct"), + MessageSid: "webchat-direct", + SenderName: "Operator", + }, + }); const directContextParams = requireMockCallArg( vi.mocked(buildDirectChatContext), @@ -2904,24 +2794,22 @@ describe("runPreparedReply media-only handling", () => { it("keeps heartbeat prompts out of visible transcript prompt", async () => { const heartbeatPrompt = "Read HEARTBEAT.md and run any due maintenance."; - await runPreparedReply( - baseParams({ - opts: { isHeartbeat: true }, - ctx: { - Body: heartbeatPrompt, - RawBody: heartbeatPrompt, - CommandBody: heartbeatPrompt, - ...createProviderSurface("heartbeat"), - ChatType: "direct", - }, - sessionCtx: { - Body: heartbeatPrompt, - BodyStripped: heartbeatPrompt, - ...createProviderSurface("heartbeat"), - ChatType: "direct", - }, - }), - ); + await runPrepared({ + opts: { isHeartbeat: true }, + ctx: { + Body: heartbeatPrompt, + RawBody: heartbeatPrompt, + CommandBody: heartbeatPrompt, + ...createProviderSurface("heartbeat"), + ChatType: "direct", + }, + sessionCtx: { + Body: heartbeatPrompt, + BodyStripped: heartbeatPrompt, + ...createProviderSurface("heartbeat"), + ChatType: "direct", + }, + }); const call = requireLastRunReplyAgentCall(); expect(call?.commandBody).toContain(heartbeatPrompt); @@ -2950,13 +2838,11 @@ describe("runPreparedReply media-only handling", () => { }, }; - await runPreparedReply( - baseParams({ - opts: { isHeartbeat: true }, - sessionEntry, - sessionStore: { "session-key": sessionEntry }, - }), - ); + await runPrepared({ + opts: { isHeartbeat: true }, + sessionEntry, + sessionStore: { "session-key": sessionEntry }, + }); expect(buildInboundUserContextPrefix).toHaveBeenCalledWith( expect.anything(), @@ -2970,39 +2856,37 @@ describe("runPreparedReply media-only handling", () => { [`group`, sessionCtx.Provider, sessionCtx.ChatType, sessionCtx.GroupChannel].join(":"), ); - await runPreparedReply( - baseParams({ - opts: { isHeartbeat: true }, - isNewSession: false, + await runPrepared({ + opts: { isHeartbeat: true }, + isNewSession: false, + systemSent: true, + ctx: { + ...createInboundBody("scheduled wake"), + Provider: "cron-event", + SessionKey: "agent:main:discord:guild-1:channel-1", + }, + sessionCtx: { + ...createSessionBody("scheduled wake"), + Provider: "cron-event", + }, + sessionEntry: { + sessionId: "session-1", + updatedAt: 1, systemSent: true, - ctx: { - ...createInboundBody("scheduled wake"), - Provider: "cron-event", - SessionKey: "agent:main:discord:guild-1:channel-1", - }, - sessionCtx: { - ...createSessionBody("scheduled wake"), - Provider: "cron-event", - }, - sessionEntry: { - sessionId: "session-1", - updatedAt: 1, - systemSent: true, - chatType: "channel", - groupId: "guild-1", - groupChannel: "#ops", - delivery: normalizeSessionDeliveryState({ - context: { channel: "discord", to: "channel-1" }, - origin: { - provider: "discord", - surface: "discord", - chatType: "channel", - to: "channel-1", - }, - }), - }, - }), - ); + chatType: "channel", + groupId: "guild-1", + groupChannel: "#ops", + delivery: normalizeSessionDeliveryState({ + context: { channel: "discord", to: "channel-1" }, + origin: { + provider: "discord", + surface: "discord", + chatType: "channel", + to: "channel-1", + }, + }), + }, + }); const call = requireLastRunReplyAgentCall(); expect(buildGroupChatContext).toHaveBeenCalledTimes(1); @@ -3064,66 +2948,60 @@ describe("runPreparedReply media-only handling", () => { }), }; - await runPreparedReply( - baseParams({ - opts: { - sourceReplyDeliveryMode: "message_tool_only", - sessionPromptSourceReplyDeliveryMode: stableMode, - }, - isNewSession: false, - systemSent: true, - sessionEntry, - ctx: { - ...createInboundTurn("@bot check this", "telegram", "group"), - MessageSid: "msg-1", - }, - sessionCtx: { - ...createSessionTurn("@bot check this", "telegram", "group"), - InboundEventKind: "room_event", - MessageSid: "msg-1", - }, - }), - ); - await runPreparedReply( - baseParams({ - opts: { - sourceReplyDeliveryMode: stableMode, - sessionPromptSourceReplyDeliveryMode: stableMode, - }, - isNewSession: false, - systemSent: true, - sessionEntry, - ctx: { - ...createInboundTurn("@bot check this", "telegram", "group"), - MessageSid: "msg-2", - }, - sessionCtx: { - ...createSessionTurn("@bot check this", "telegram", "group"), - MessageSid: "msg-2", - }, - }), - ); - await runPreparedReply( - baseParams({ - opts: { - isHeartbeat: true, - sourceReplyDeliveryMode: stableMode, - sessionPromptSourceReplyDeliveryMode: stableMode, - }, - isNewSession: false, - systemSent: true, - sessionEntry, - ctx: { - ...createInboundBody("scheduled wake"), - Provider: "cron-event", - SessionKey: "agent:main:telegram:-100123", - }, - sessionCtx: { - ...createSessionBody("scheduled wake"), - Provider: "cron-event", - }, - }), - ); + await runPrepared({ + opts: { + sourceReplyDeliveryMode: "message_tool_only", + sessionPromptSourceReplyDeliveryMode: stableMode, + }, + isNewSession: false, + systemSent: true, + sessionEntry, + ctx: { + ...createInboundTurn("@bot check this", "telegram", "group"), + MessageSid: "msg-1", + }, + sessionCtx: { + ...createSessionTurn("@bot check this", "telegram", "group"), + InboundEventKind: "room_event", + MessageSid: "msg-1", + }, + }); + await runPrepared({ + opts: { + sourceReplyDeliveryMode: stableMode, + sessionPromptSourceReplyDeliveryMode: stableMode, + }, + isNewSession: false, + systemSent: true, + sessionEntry, + ctx: { + ...createInboundTurn("@bot check this", "telegram", "group"), + MessageSid: "msg-2", + }, + sessionCtx: { + ...createSessionTurn("@bot check this", "telegram", "group"), + MessageSid: "msg-2", + }, + }); + await runPrepared({ + opts: { + isHeartbeat: true, + sourceReplyDeliveryMode: stableMode, + sessionPromptSourceReplyDeliveryMode: stableMode, + }, + isNewSession: false, + systemSent: true, + sessionEntry, + ctx: { + ...createInboundBody("scheduled wake"), + Provider: "cron-event", + SessionKey: "agent:main:telegram:-100123", + }, + sessionCtx: { + ...createSessionBody("scheduled wake"), + Provider: "cron-event", + }, + }); const roomEventRun = requireRunReplyAgentCall(0).followupRun.run; const primaryRun = requireRunReplyAgentCall(1).followupRun.run; @@ -3172,20 +3050,16 @@ describe("runPreparedReply media-only handling", () => { }, }; - await runPreparedReply( - baseParams({ - ...baseRoomEvent, - ctx: { ...baseRoomEvent.ctx, MessageSid: "msg-1", Timestamp: 1_710_000_000_000 }, - sessionCtx: { ...baseRoomEvent.sessionCtx, MessageSid: "msg-1" }, - }), - ); - await runPreparedReply( - baseParams({ - ...baseRoomEvent, - ctx: { ...baseRoomEvent.ctx, MessageSid: "msg-2", Timestamp: 1_710_000_005_000 }, - sessionCtx: { ...baseRoomEvent.sessionCtx, MessageSid: "msg-2" }, - }), - ); + await runPrepared({ + ...baseRoomEvent, + ctx: { ...baseRoomEvent.ctx, MessageSid: "msg-1", Timestamp: 1_710_000_000_000 }, + sessionCtx: { ...baseRoomEvent.sessionCtx, MessageSid: "msg-1" }, + }); + await runPrepared({ + ...baseRoomEvent, + ctx: { ...baseRoomEvent.ctx, MessageSid: "msg-2", Timestamp: 1_710_000_005_000 }, + sessionCtx: { ...baseRoomEvent.sessionCtx, MessageSid: "msg-2" }, + }); const firstRun = requireRunReplyAgentCall(0).followupRun.run; const secondRun = requireRunReplyAgentCall(1).followupRun.run; @@ -3199,26 +3073,24 @@ describe("runPreparedReply media-only handling", () => { it("keeps explicit mention state in user context and out of CLI binding facts", async () => { vi.mocked(buildGroupChatContext).mockReturnValue("group:telegram:group:automatic"); - await runPreparedReply( - baseParams({ - opts: { - sourceReplyDeliveryMode: "automatic", - sessionPromptSourceReplyDeliveryMode: "automatic", - }, - isNewSession: false, - systemSent: true, - ctx: { - ...createInboundTurn("@SirPinchALotBot check this", "telegram", "group"), - BotUsername: "SirPinchALotBot", - ExplicitlyMentionedBot: true, - }, - sessionCtx: { - ...createSessionTurn("@SirPinchALotBot check this", "telegram", "group"), - BotUsername: "SirPinchALotBot", - ExplicitlyMentionedBot: true, - }, - }), - ); + await runPrepared({ + opts: { + sourceReplyDeliveryMode: "automatic", + sessionPromptSourceReplyDeliveryMode: "automatic", + }, + isNewSession: false, + systemSent: true, + ctx: { + ...createInboundTurn("@SirPinchALotBot check this", "telegram", "group"), + BotUsername: "SirPinchALotBot", + ExplicitlyMentionedBot: true, + }, + sessionCtx: { + ...createSessionTurn("@SirPinchALotBot check this", "telegram", "group"), + BotUsername: "SirPinchALotBot", + ExplicitlyMentionedBot: true, + }, + }); const run = requireRunReplyAgentCall(0).followupRun.run; const inboundCtx = requireMockCallArg( @@ -3253,40 +3125,36 @@ describe("runPreparedReply media-only handling", () => { }), }; - await runPreparedReply( - baseParams({ - opts: { - sourceReplyDeliveryMode: "automatic", - sessionPromptSourceReplyDeliveryMode: "automatic", - }, - isNewSession: true, - systemSent: false, - sessionEntry, - ctx: { - ...createInboundTurn("@bot first", "telegram", "group"), - }, - sessionCtx: { - ...createSessionTurn("@bot first", "telegram", "group"), - }, - }), - ); - await runPreparedReply( - baseParams({ - opts: { - sourceReplyDeliveryMode: "automatic", - sessionPromptSourceReplyDeliveryMode: "automatic", - }, - isNewSession: false, - systemSent: true, - sessionEntry, - ctx: { - ...createInboundTurn("second", "telegram", "group"), - }, - sessionCtx: { - ...createSessionTurn("second", "telegram", "group"), - }, - }), - ); + await runPrepared({ + opts: { + sourceReplyDeliveryMode: "automatic", + sessionPromptSourceReplyDeliveryMode: "automatic", + }, + isNewSession: true, + systemSent: false, + sessionEntry, + ctx: { + ...createInboundTurn("@bot first", "telegram", "group"), + }, + sessionCtx: { + ...createSessionTurn("@bot first", "telegram", "group"), + }, + }); + await runPrepared({ + opts: { + sourceReplyDeliveryMode: "automatic", + sessionPromptSourceReplyDeliveryMode: "automatic", + }, + isNewSession: false, + systemSent: true, + sessionEntry, + ctx: { + ...createInboundTurn("second", "telegram", "group"), + }, + sessionCtx: { + ...createSessionTurn("second", "telegram", "group"), + }, + }); const firstRun = requireRunReplyAgentCall(0).followupRun.run; const secondRun = requireRunReplyAgentCall(1).followupRun.run; @@ -3307,36 +3175,34 @@ describe("runPreparedReply media-only handling", () => { ["Conversation info:", "Sender:", "sender_id", "telegram-user-1"].join("\n"), ); - await runPreparedReply( - baseParams({ - ctx: { - Body: commandText, - RawBody: commandText, - CommandBody: commandText, - ...createProviderSurface("webchat"), - ChatType: "direct", - ReplyToBody: "quoted reset target", - ReplyToSender: "Ada Lovelace", - }, - sessionCtx: { - ...createSessionTurn("", "webchat", "direct"), - SenderId: "telegram-user-1", - SenderName: "Ada Lovelace", - ReplyToBody: "quoted reset target", - ReplyToSender: "Ada Lovelace", - }, - command: { - surface: "webchat", - channel: "webchat", - isAuthorizedSender: true, - abortKey: "session-key", - ownerList: [], - senderIsOwner: true, - rawBodyNormalized: commandText, - commandBodyNormalized: commandText, - } as never, - }), - ); + await runPrepared({ + ctx: { + Body: commandText, + RawBody: commandText, + CommandBody: commandText, + ...createProviderSurface("webchat"), + ChatType: "direct", + ReplyToBody: "quoted reset target", + ReplyToSender: "Ada Lovelace", + }, + sessionCtx: { + ...createSessionTurn("", "webchat", "direct"), + SenderId: "telegram-user-1", + SenderName: "Ada Lovelace", + ReplyToBody: "quoted reset target", + ReplyToSender: "Ada Lovelace", + }, + command: { + surface: "webchat", + channel: "webchat", + isAuthorizedSender: true, + abortKey: "session-key", + ownerList: [], + senderIsOwner: true, + rawBodyNormalized: commandText, + commandBodyNormalized: commandText, + } as never, + }); const call = requireLastRunReplyAgentCall(); expect(call?.commandBody).toContain("A new session was started via /new or /reset."); @@ -3352,28 +3218,26 @@ describe("runPreparedReply media-only handling", () => { ); it("keeps reset user notes visible while hiding startup instructions", async () => { - await runPreparedReply( - baseParams({ - ctx: { - ...createInboundTurn("/reset summarize my workspace", "webchat", "direct"), - }, - sessionCtx: { - ...createSessionTurn("", "webchat", "direct"), - }, - command: { - surface: "webchat", - channel: "webchat", - isAuthorizedSender: true, - abortKey: "session-key", - ownerList: [], - senderIsOwner: true, - rawBodyNormalized: "/reset summarize my workspace", - commandBodyNormalized: "/reset summarize my workspace", - softResetTriggered: true, - softResetTail: "summarize my workspace", - } as never, - }), - ); + await runPrepared({ + ctx: { + ...createInboundTurn("/reset summarize my workspace", "webchat", "direct"), + }, + sessionCtx: { + ...createSessionTurn("", "webchat", "direct"), + }, + command: { + surface: "webchat", + channel: "webchat", + isAuthorizedSender: true, + abortKey: "session-key", + ownerList: [], + senderIsOwner: true, + rawBodyNormalized: "/reset summarize my workspace", + commandBodyNormalized: "/reset summarize my workspace", + softResetTriggered: true, + softResetTail: "summarize my workspace", + } as never, + }); const call = requireLastRunReplyAgentCall(); expect(call?.commandBody).toContain("A new session was started via /new or /reset."); @@ -3383,26 +3247,24 @@ describe("runPreparedReply media-only handling", () => { }); it("uses inbound origin channel for run messageProvider", async () => { - await runPreparedReply( - baseParams({ - ctx: { - ...createInboundBody(""), - ThreadHistoryBody: "Earlier message in this thread", - OriginatingChannel: "webchat", - OriginatingTo: "session:abc", - ChatType: "group", - }, - sessionCtx: { - ...createSessionBody(""), - ThreadHistoryBody: "Earlier message in this thread", - media: [{ path: "/tmp/input.png" }], - Provider: "telegram", - ChatType: "group", - OriginatingChannel: "telegram", - OriginatingTo: "telegram:123", - }, - }), - ); + await runPrepared({ + ctx: { + ...createInboundBody(""), + ThreadHistoryBody: "Earlier message in this thread", + OriginatingChannel: "webchat", + OriginatingTo: "session:abc", + ChatType: "group", + }, + sessionCtx: { + ...createSessionBody(""), + ThreadHistoryBody: "Earlier message in this thread", + media: [{ path: "/tmp/input.png" }], + Provider: "telegram", + ChatType: "group", + OriginatingChannel: "telegram", + OriginatingTo: "telegram:123", + }, + }); const call = requireRunReplyAgentCall(); expect(call?.followupRun.run.messageProvider).toBe("webchat"); @@ -3417,57 +3279,53 @@ describe("runPreparedReply media-only handling", () => { }); it("prefers Provider over Surface when origin channel is missing", async () => { - await runPreparedReply( - baseParams({ - ctx: { - ...createInboundBody(""), - ThreadHistoryBody: "Earlier message in this thread", - OriginatingChannel: undefined, - OriginatingTo: undefined, - Provider: "feishu", - Surface: "webchat", - ChatType: "group", - }, - sessionCtx: { - ...createSessionBody(""), - ThreadHistoryBody: "Earlier message in this thread", - media: [{ path: "/tmp/input.png" }], - Provider: "webchat", - ChatType: "group", - OriginatingChannel: undefined, - OriginatingTo: undefined, - }, - }), - ); + await runPrepared({ + ctx: { + ...createInboundBody(""), + ThreadHistoryBody: "Earlier message in this thread", + OriginatingChannel: undefined, + OriginatingTo: undefined, + Provider: "feishu", + Surface: "webchat", + ChatType: "group", + }, + sessionCtx: { + ...createSessionBody(""), + ThreadHistoryBody: "Earlier message in this thread", + media: [{ path: "/tmp/input.png" }], + Provider: "webchat", + ChatType: "group", + OriginatingChannel: undefined, + OriginatingTo: undefined, + }, + }); const call = requireRunReplyAgentCall(); expect(call?.followupRun.run.messageProvider).toBe("feishu"); }); it("uses the effective session account for followup originatingAccountId when AccountId is omitted", async () => { - await runPreparedReply( - baseParams({ - ctx: { - ...createInboundBody(""), - ThreadHistoryBody: "Earlier message in this thread", - OriginatingChannel: "discord", - OriginatingTo: "channel:24680", - ChatType: "group", - AccountId: undefined, - }, - sessionCtx: { - ...createSessionBody(""), - ThreadHistoryBody: "Earlier message in this thread", - media: [{ path: "/tmp/input.png" }], - Provider: "discord", - ChatType: "group", - OriginatingChannel: "discord", - OriginatingTo: "channel:24680", - ReplyToId: "reply-24680", - AccountId: "work", - }, - }), - ); + await runPrepared({ + ctx: { + ...createInboundBody(""), + ThreadHistoryBody: "Earlier message in this thread", + OriginatingChannel: "discord", + OriginatingTo: "channel:24680", + ChatType: "group", + AccountId: undefined, + }, + sessionCtx: { + ...createSessionBody(""), + ThreadHistoryBody: "Earlier message in this thread", + media: [{ path: "/tmp/input.png" }], + Provider: "discord", + ChatType: "group", + OriginatingChannel: "discord", + OriginatingTo: "channel:24680", + ReplyToId: "reply-24680", + AccountId: "work", + }, + }); const call = requireRunReplyAgentCall(); expect(call?.followupRun.originatingAccountId).toBe("work"); @@ -3475,35 +3333,33 @@ describe("runPreparedReply media-only handling", () => { }); it("captures the prepared reply policy for queued Slack runs", async () => { - await runPreparedReply( - baseParams({ - cfg: { - session: {}, - channels: { slack: { replyToMode: "all" } }, - agents: { defaults: {} }, - }, - ctx: { - ...createInboundBody(""), - ThreadHistoryBody: "Earlier message in this thread", - Provider: "slack", - OriginatingChannel: undefined, - OriginatingTo: "C123", - ChatType: "group", - ReplyToMode: "off", - }, - sessionCtx: { - ...createSessionBody(""), - ThreadHistoryBody: "Earlier message in this thread", - media: [{ path: "/tmp/input.png" }], - Provider: "slack", - ChatType: "group", - OriginatingChannel: "slack", - OriginatingTo: "C123", - ReplyToId: "101.001", - ReplyToMode: "off", - }, - }), - ); + await runPrepared({ + cfg: { + session: {}, + channels: { slack: { replyToMode: "all" } }, + agents: { defaults: {} }, + }, + ctx: { + ...createInboundBody(""), + ThreadHistoryBody: "Earlier message in this thread", + Provider: "slack", + OriginatingChannel: undefined, + OriginatingTo: "C123", + ChatType: "group", + ReplyToMode: "off", + }, + sessionCtx: { + ...createSessionBody(""), + ThreadHistoryBody: "Earlier message in this thread", + media: [{ path: "/tmp/input.png" }], + Provider: "slack", + ChatType: "group", + OriginatingChannel: "slack", + OriginatingTo: "C123", + ReplyToId: "101.001", + ReplyToMode: "off", + }, + }); const call = requireRunReplyAgentCall(); expect(call?.followupRun.originatingReplyToId).toBe("101.001"); @@ -3511,53 +3367,51 @@ describe("runPreparedReply media-only handling", () => { }); it("captures queued reply policy from hydrated system-event session context", async () => { - await runPreparedReply( - baseParams({ - cfg: { - session: {}, - channels: { - slack: { - replyToMode: "all", - replyToModeByChatType: { direct: "off" }, - }, + await runPrepared({ + cfg: { + session: {}, + channels: { + slack: { + replyToMode: "all", + replyToModeByChatType: { direct: "off" }, }, - agents: { defaults: {} }, }, - opts: { isHeartbeat: true }, - ctx: { - ...createInboundBody("scheduled wake"), - Provider: "cron-event", - SessionKey: "agent:main:slack:direct:U1", - OriginatingChannel: "slack", - OriginatingTo: "user:U1", - }, - sessionCtx: { - ...createSessionBody("scheduled wake"), - Provider: "cron-event", - OriginatingChannel: "slack", - OriginatingTo: "user:U1", - }, - sessionEntry: { - sessionId: "session-1", - updatedAt: 1, - chatType: "direct", - delivery: normalizeSessionDeliveryState({ - context: { - channel: "slack", - to: "user:U1", - accountId: "work", - }, - origin: { - provider: "matrix", - surface: "matrix", - chatType: "direct", - to: "room:origin", - accountId: "origin", - }, - }), - }, - }), - ); + agents: { defaults: {} }, + }, + opts: { isHeartbeat: true }, + ctx: { + ...createInboundBody("scheduled wake"), + Provider: "cron-event", + SessionKey: "agent:main:slack:direct:U1", + OriginatingChannel: "slack", + OriginatingTo: "user:U1", + }, + sessionCtx: { + ...createSessionBody("scheduled wake"), + Provider: "cron-event", + OriginatingChannel: "slack", + OriginatingTo: "user:U1", + }, + sessionEntry: { + sessionId: "session-1", + updatedAt: 1, + chatType: "direct", + delivery: normalizeSessionDeliveryState({ + context: { + channel: "slack", + to: "user:U1", + accountId: "work", + }, + origin: { + provider: "matrix", + surface: "matrix", + chatType: "direct", + to: "room:origin", + accountId: "origin", + }, + }), + }, + }); const call = requireRunReplyAgentCall(); expect(call?.followupRun.originatingChannel).toBe("slack"); @@ -3571,42 +3425,38 @@ describe("runPreparedReply media-only handling", () => { }); it("uses transport thread metadata for followup originatingThreadId", async () => { - await runPreparedReply( - baseParams({ - ctx: { - ...createInboundBody(""), - ThreadHistoryBody: "Earlier message in this thread", - OriginatingChannel: "slack", - OriginatingTo: "user:U1", - ChatType: "direct", - MessageThreadId: undefined, - TransportThreadId: "650.000", - }, - sessionCtx: { - ...createSessionBody(""), - ThreadHistoryBody: "Earlier message in this thread", - media: [{ path: "/tmp/input.png" }], - Provider: "slack", - ChatType: "direct", - OriginatingChannel: "slack", - OriginatingTo: "user:U1", - TransportThreadId: "650.000", - }, - }), - ); + await runPrepared({ + ctx: { + ...createInboundBody(""), + ThreadHistoryBody: "Earlier message in this thread", + OriginatingChannel: "slack", + OriginatingTo: "user:U1", + ChatType: "direct", + MessageThreadId: undefined, + TransportThreadId: "650.000", + }, + sessionCtx: { + ...createSessionBody(""), + ThreadHistoryBody: "Earlier message in this thread", + media: [{ path: "/tmp/input.png" }], + Provider: "slack", + ChatType: "direct", + OriginatingChannel: "slack", + OriginatingTo: "user:U1", + TransportThreadId: "650.000", + }, + }); const call = requireRunReplyAgentCall(); expect(call?.followupRun.originatingThreadId).toBe("650.000"); }); it("passes suppressTyping through typing mode resolution", async () => { - await runPreparedReply( - baseParams({ - opts: { - suppressTyping: true, - }, - }), - ); + await runPrepared({ + opts: { + suppressTyping: true, + }, + }); const call = requireMockCallArg(vi.mocked(resolveTypingMode), "typing mode params") as { suppressTyping?: boolean; @@ -3617,7 +3467,7 @@ describe("runPreparedReply media-only handling", () => { it("routes queued system events into user prompt text, not system prompt context", async () => { vi.mocked(drainFormattedSystemEvents).mockResolvedValueOnce("System: [t] Model switched."); - await runPreparedReply(baseParams()); + await runPrepared(); const call = requireRunReplyAgentCall(); expect(call.commandBody).toContain("System: [t] Model switched."); @@ -3625,15 +3475,13 @@ describe("runPreparedReply media-only handling", () => { }); it("does not drain queued system events for commitment-only heartbeat runs", async () => { - await runPreparedReply( - baseParams({ - abortedLastRun: true, - opts: { - isHeartbeat: true, - [HEARTBEAT_RUN_SCOPE]: "commitment-only", - }, - }), - ); + await runPrepared({ + abortedLastRun: true, + opts: { + isHeartbeat: true, + [HEARTBEAT_RUN_SCOPE]: "commitment-only", + }, + }); expect(drainFormattedSystemEvents).not.toHaveBeenCalled(); expect(applySessionHints).not.toHaveBeenCalled(); @@ -3708,13 +3556,11 @@ describe("runPreparedReply media-only handling", () => { // does not shadow the low|medium|high shorthand. vi.mocked(drainFormattedSystemEvents).mockResolvedValueOnce("System: [t] Node connected."); - await runPreparedReply( - baseParams({ - ctx: { Body: "low tell me about cats", RawBody: "low tell me about cats" }, - sessionCtx: { Body: "low tell me about cats", BodyStripped: "low tell me about cats" }, - resolvedThinkLevel: undefined, - }), - ); + await runPrepared({ + ctx: { Body: "low tell me about cats", RawBody: "low tell me about cats" }, + sessionCtx: { Body: "low tell me about cats", BodyStripped: "low tell me about cats" }, + resolvedThinkLevel: undefined, + }); const call = requireRunReplyAgentCall(); // Think hint extracted before events arrived — level must be "low", not the model default. @@ -3727,11 +3573,9 @@ describe("runPreparedReply media-only handling", () => { }); it("forwards resolved fast-mode override into the followup run", async () => { - await runPreparedReply( - baseParams({ - resolvedFastMode: "auto", - }), - ); + await runPrepared({ + resolvedFastMode: "auto", + }); const call = requireRunReplyAgentCall(); expect(call.followupRun.run.fastMode).toBe("auto"); @@ -3742,7 +3586,7 @@ describe("runPreparedReply media-only handling", () => { // effectiveBaseBody for the queue path so deferred turns see events. vi.mocked(drainFormattedSystemEvents).mockResolvedValueOnce("System: [t] Node connected."); - await runPreparedReply(baseParams()); + await runPrepared(); const call = requireRunReplyAgentCall(); expect(call.followupRun.prompt).toContain("System: [t] Node connected."); @@ -3787,15 +3631,13 @@ describe("runPreparedReply media-only handling", () => { // must not be stripped from the queue/steer body (followupRun.prompt). vi.mocked(drainFormattedSystemEvents).mockResolvedValueOnce(undefined); - await runPreparedReply( - baseParams({ - ctx: { Body: "low steer this conversation", RawBody: "low steer this conversation" }, - sessionCtx: { - ...createSessionBody("low steer this conversation"), - }, - resolvedThinkLevel: undefined, - }), - ); + await runPrepared({ + ctx: { Body: "low steer this conversation", RawBody: "low steer this conversation" }, + sessionCtx: { + ...createSessionBody("low steer this conversation"), + }, + resolvedThinkLevel: undefined, + }); const call = requireRunReplyAgentCall(); // Queue body (used by steer mode) must keep the full original text. diff --git a/src/auto-reply/reply/queue.collect.test.ts b/src/auto-reply/reply/queue.collect.test.ts index 592457b6ccb3..21d3dd12c763 100644 --- a/src/auto-reply/reply/queue.collect.test.ts +++ b/src/auto-reply/reply/queue.collect.test.ts @@ -58,6 +58,29 @@ function createDrainRecorder(expectedCalls = 1) { return { calls, done, runFollowup }; } +function createQueueCase(key: string, overrides: Partial = {}, expectedCalls = 1) { + return { key, ...createDrainRecorder(expectedCalls), settings: createQueueSettings(overrides) }; +} + +function enqueueTestRuns( + key: string, + settings: QueueSettings, + ...runs: Parameters[0][] +) { + for (const run of runs) { + enqueueTestRun(key, run, settings); + } +} + +async function drainRecordedQueue( + key: string, + runFollowup: ReturnType["runFollowup"], + done: ReturnType["done"], +) { + scheduleFollowupDrain(key, runFollowup); + await done.promise; +} + describe("followup queue collect routing", () => { it("carries queued local cron-authority unavailability through a followup drain", async () => { const key = `test-followup-cron-authority-${Date.now()}`; @@ -221,63 +244,55 @@ describe("followup queue collect routing", () => { }); it("does not collect when destinations differ", async () => { - const key = `test-collect-diff-to-${Date.now()}`; - const { calls, done, runFollowup } = createDrainRecorder(2); - const settings = createQueueSettings(); + const { key, calls, done, runFollowup, settings } = createQueueCase( + `test-collect-diff-to-${Date.now()}`, + {}, + 2, + ); - enqueueTestRun( + enqueueTestRuns( key, + settings, { prompt: "one", originatingChannel: "slack", originatingTo: "channel:A", }, - settings, - ); - enqueueTestRun( - key, { prompt: "two", originatingChannel: "slack", originatingTo: "channel:B", }, - settings, ); - scheduleFollowupDrain(key, runFollowup); - await done.promise; + await drainRecordedQueue(key, runFollowup, done); expect(calls[0]?.prompt).toBe("one"); expect(calls[1]?.prompt).toBe("two"); }); it("collects when channel+destination match", async () => { - const key = `test-collect-same-to-${Date.now()}`; - const { calls, done, runFollowup } = createDrainRecorder(); - const settings = createQueueSettings(); + const { key, calls, done, runFollowup, settings } = createQueueCase( + `test-collect-same-to-${Date.now()}`, + ); - enqueueTestRun( + enqueueTestRuns( key, + settings, { prompt: "one", originatingChannel: "slack", originatingTo: "channel:A", originatingChatType: "channel", }, - settings, - ); - enqueueTestRun( - key, { prompt: "two", originatingChannel: "slack", originatingTo: "channel:A", originatingChatType: "channel", }, - settings, ); - scheduleFollowupDrain(key, runFollowup); - await done.promise; + await drainRecordedQueue(key, runFollowup, done); expect(calls[0]?.prompt).toContain("[Queued messages while agent was busy]"); expect(calls[0]?.originatingChannel).toBe("slack"); expect(calls[0]?.originatingTo).toBe("channel:A"); @@ -285,9 +300,9 @@ describe("followup queue collect routing", () => { }); it("collects Slack top-level messages when reply anchors are disabled", async () => { - const key = `test-collect-slack-reply-off-${Date.now()}`; - const { calls, done, runFollowup } = createDrainRecorder(); - const settings = createQueueSettings(); + const { key, calls, done, runFollowup, settings } = createQueueCase( + `test-collect-slack-reply-off-${Date.now()}`, + ); for (const [prompt, replyToId] of [ ["one", "101.001"], @@ -308,8 +323,7 @@ describe("followup queue collect routing", () => { ); } - scheduleFollowupDrain(key, runFollowup); - await done.promise; + await drainRecordedQueue(key, runFollowup, done); expect(calls).toHaveLength(1); expect(calls[0]?.prompt).toContain("Queued #1\none"); @@ -317,9 +331,11 @@ describe("followup queue collect routing", () => { }); it("splits collect batches when enabled reply anchors differ", async () => { - const key = `test-collect-slack-reply-all-${Date.now()}`; - const { calls, done, runFollowup } = createDrainRecorder(2); - const settings = createQueueSettings(); + const { key, calls, done, runFollowup, settings } = createQueueCase( + `test-collect-slack-reply-all-${Date.now()}`, + {}, + 2, + ); for (const [prompt, replyToId] of [ ["one", "101.001"], @@ -340,8 +356,7 @@ describe("followup queue collect routing", () => { ); } - scheduleFollowupDrain(key, runFollowup); - await done.promise; + await drainRecordedQueue(key, runFollowup, done); expect(calls.map((call) => call.prompt)).toEqual(["one", "two"]); expect(calls.map((call) => call.messageId)).toEqual(["101.001", "101.002"]); @@ -353,9 +368,9 @@ describe("followup queue collect routing", () => { ] as const)( "splits standalone Slack collect batches by message id in %s reply mode", async (replyToMode, originatingChannel) => { - const key = `test-collect-slack-standalone-${replyToMode}-${Date.now()}`; - const { calls, done } = createDrainRecorder(); - const settings = createQueueSettings(); + const { key, calls, done, settings } = createQueueCase( + `test-collect-slack-standalone-${replyToMode}-${Date.now()}`, + ); for (const [prompt, messageId] of [ ["one", "101.001"], @@ -389,9 +404,9 @@ describe("followup queue collect routing", () => { ); it("collects distinct messages inside the same routed thread", async () => { - const key = `test-collect-shared-thread-${Date.now()}`; - const { calls, done, runFollowup } = createDrainRecorder(); - const settings = createQueueSettings(); + const { key, calls, done, runFollowup, settings } = createQueueCase( + `test-collect-shared-thread-${Date.now()}`, + ); for (const [prompt, messageId] of [ ["one", "message-1"], @@ -412,8 +427,7 @@ describe("followup queue collect routing", () => { ); } - scheduleFollowupDrain(key, runFollowup); - await done.promise; + await drainRecordedQueue(key, runFollowup, done); expect(calls).toHaveLength(1); expect(calls[0]?.prompt).toContain("Queued #1\none"); @@ -421,9 +435,11 @@ describe("followup queue collect routing", () => { }); it("does not collect when captured reply modes differ on the same anchor", async () => { - const key = `test-collect-slack-reply-mode-${Date.now()}`; - const { calls, done, runFollowup } = createDrainRecorder(2); - const settings = createQueueSettings(); + const { key, calls, done, runFollowup, settings } = createQueueCase( + `test-collect-slack-reply-mode-${Date.now()}`, + {}, + 2, + ); for (const [prompt, messageId, replyToMode] of [ ["first", "message-1", "first"], @@ -444,50 +460,48 @@ describe("followup queue collect routing", () => { ); } - scheduleFollowupDrain(key, runFollowup); - await done.promise; + await drainRecordedQueue(key, runFollowup, done); expect(calls.map((call) => call.prompt)).toEqual(["first", "all"]); expect(calls.map((call) => call.originatingReplyToMode)).toEqual(["first", "all"]); }); it("does not collect when chat types differ on the same destination", async () => { - const key = `test-collect-diff-chat-type-${Date.now()}`; - const { calls, done, runFollowup } = createDrainRecorder(2); - const settings = createQueueSettings(); + const { key, calls, done, runFollowup, settings } = createQueueCase( + `test-collect-diff-chat-type-${Date.now()}`, + {}, + 2, + ); - enqueueTestRun( + enqueueTestRuns( key, + settings, { prompt: "direct", originatingChannel: "slack", originatingTo: "same-target", originatingChatType: "direct", }, - settings, - ); - enqueueTestRun( - key, { prompt: "channel", originatingChannel: "slack", originatingTo: "same-target", originatingChatType: "channel", }, - settings, ); - scheduleFollowupDrain(key, runFollowup); - await done.promise; + await drainRecordedQueue(key, runFollowup, done); expect(calls.map((call) => call.prompt)).toEqual(["direct", "channel"]); expect(calls.map((call) => call.originatingChatType)).toEqual(["direct", "channel"]); }); it("does not collect when source delivery policy differs", async () => { - const key = `test-collect-diff-delivery-policy-${Date.now()}`; - const { calls, done, runFollowup } = createDrainRecorder(2); - const settings = createQueueSettings(); + const { key, calls, done, runFollowup, settings } = createQueueCase( + `test-collect-diff-delivery-policy-${Date.now()}`, + {}, + 2, + ); const createPolicyRun = ( prompt: string, sourceReplyDeliveryMode: NonNullable, @@ -509,8 +523,7 @@ describe("followup queue collect routing", () => { enqueueFollowupRun(key, createPolicyRun("automatic", "automatic"), settings); enqueueFollowupRun(key, createPolicyRun("private", "message_tool_only"), settings); - scheduleFollowupDrain(key, runFollowup); - await done.promise; + await drainRecordedQueue(key, runFollowup, done); expect(calls.map((call) => call.prompt)).toEqual([ "[Queued messages while agent was busy]\n\n---\nQueued #1\nautomatic", @@ -523,9 +536,11 @@ describe("followup queue collect routing", () => { }); it("does not collect when task suggestion delivery differs", async () => { - const key = `test-collect-diff-task-suggestion-delivery-${Date.now()}`; - const { calls, done, runFollowup } = createDrainRecorder(2); - const settings = createQueueSettings(); + const { key, calls, done, runFollowup, settings } = createQueueCase( + `test-collect-diff-task-suggestion-delivery-${Date.now()}`, + {}, + 2, + ); const createTaskRun = (prompt: string, taskSuggestionDeliveryMode?: "gateway") => { const base = createRun({ prompt, @@ -544,8 +559,7 @@ describe("followup queue collect routing", () => { enqueueFollowupRun(key, createTaskRun("legacy client"), settings); enqueueFollowupRun(key, createTaskRun("actionable client", "gateway"), settings); - scheduleFollowupDrain(key, runFollowup); - await done.promise; + await drainRecordedQueue(key, runFollowup, done); expect(calls.map((call) => call.run.taskSuggestionDeliveryMode)).toEqual([ undefined, @@ -554,33 +568,30 @@ describe("followup queue collect routing", () => { }); it("keeps overflow summaries on the dropped source chat type", async () => { - const key = `test-collect-overflow-chat-type-${Date.now()}`; - const { calls, done, runFollowup } = createDrainRecorder(2); - const settings = createQueueSettings({ cap: 1 }); + const { key, calls, done, runFollowup, settings } = createQueueCase( + `test-collect-overflow-chat-type-${Date.now()}`, + { cap: 1 }, + 2, + ); - enqueueTestRun( + enqueueTestRuns( key, + settings, { prompt: "private direct content", originatingChannel: "slack", originatingTo: "same-target", originatingChatType: "direct", }, - settings, - ); - enqueueTestRun( - key, { prompt: "public channel content", originatingChannel: "slack", originatingTo: "same-target", originatingChatType: "channel", }, - settings, ); - scheduleFollowupDrain(key, runFollowup); - await done.promise; + await drainRecordedQueue(key, runFollowup, done); expect(calls[0]?.prompt).toContain("[Queue overflow] Dropped 1 message due to cap."); expect(calls[0]?.prompt).toContain("- private direct content"); @@ -590,33 +601,30 @@ describe("followup queue collect routing", () => { }); it("keeps overflow summaries on the dropped source route", async () => { - const key = `test-collect-overflow-route-${Date.now()}`; - const { calls, done, runFollowup } = createDrainRecorder(2); - const settings = createQueueSettings({ cap: 1 }); + const { key, calls, done, runFollowup, settings } = createQueueCase( + `test-collect-overflow-route-${Date.now()}`, + { cap: 1 }, + 2, + ); - enqueueTestRun( + enqueueTestRuns( key, + settings, { prompt: "channel A content", originatingChannel: "slack", originatingTo: "channel:A", originatingChatType: "channel", }, - settings, - ); - enqueueTestRun( - key, { prompt: "channel B content", originatingChannel: "slack", originatingTo: "channel:B", originatingChatType: "channel", }, - settings, ); - scheduleFollowupDrain(key, runFollowup); - await done.promise; + await drainRecordedQueue(key, runFollowup, done); expect(calls[0]?.prompt).toContain("- channel A content"); expect(calls[0]?.originatingTo).toBe("channel:A"); @@ -626,43 +634,36 @@ describe("followup queue collect routing", () => { }); it("does not attribute elided private drops to a public summary", async () => { - const key = `test-collect-overflow-elided-context-${Date.now()}`; - const { calls, done, runFollowup } = createDrainRecorder(3); - const settings = createQueueSettings({ cap: 1 }); + const { key, calls, done, runFollowup, settings } = createQueueCase( + `test-collect-overflow-elided-context-${Date.now()}`, + { cap: 1 }, + 3, + ); - enqueueTestRun( + enqueueTestRuns( key, + settings, { prompt: "private direct content", originatingChannel: "slack", originatingTo: "direct:A", originatingChatType: "direct", }, - settings, - ); - enqueueTestRun( - key, { prompt: "older public content", originatingChannel: "slack", originatingTo: "channel:B", originatingChatType: "channel", }, - settings, - ); - enqueueTestRun( - key, { prompt: "newer public content", originatingChannel: "slack", originatingTo: "channel:B", originatingChatType: "channel", }, - settings, ); - scheduleFollowupDrain(key, runFollowup); - await done.promise; + await drainRecordedQueue(key, runFollowup, done); expect(calls[0]?.prompt).toContain("[Queue overflow] Dropped 1 message due to cap."); expect(calls[0]?.prompt).not.toContain("older public content"); @@ -823,9 +824,11 @@ describe("followup queue collect routing", () => { }); it("keeps retained excess contexts isolated after evicting the oldest metadata", async () => { - const key = `test-collect-overflow-evicted-context-${Date.now()}`; - const { calls, done, runFollowup } = createDrainRecorder(3); - const settings = createQueueSettings({ cap: 1 }); + const { key, calls, done, runFollowup, settings } = createQueueCase( + `test-collect-overflow-evicted-context-${Date.now()}`, + { cap: 1 }, + 3, + ); const accepted = ["A", "B", "C", "D"].map((target) => enqueueFollowupRun( @@ -841,8 +844,7 @@ describe("followup queue collect routing", () => { ); expect(accepted).toEqual([true, true, true, true]); - scheduleFollowupDrain(key, runFollowup); - await done.promise; + await drainRecordedQueue(key, runFollowup, done); expect(calls).toHaveLength(3); expect(calls.map((call) => call.originatingTo)).toEqual([ @@ -858,9 +860,11 @@ describe("followup queue collect routing", () => { }); it("keeps overflow summaries under the dropped sender authorization", async () => { - const key = `test-collect-overflow-auth-${Date.now()}`; - const { calls, done, runFollowup } = createDrainRecorder(2); - const settings = createQueueSettings({ cap: 1 }); + const { key, calls, done, runFollowup, settings } = createQueueCase( + `test-collect-overflow-auth-${Date.now()}`, + { cap: 1 }, + 2, + ); const dropped = createRun({ prompt: "guest content", originatingChannel: "slack", @@ -899,8 +903,7 @@ describe("followup queue collect routing", () => { settings, ); - scheduleFollowupDrain(key, runFollowup); - await done.promise; + await drainRecordedQueue(key, runFollowup, done); expect(calls[0]?.prompt).toContain("- guest content"); expect(calls[0]?.run.senderId).toBe("guest"); @@ -912,9 +915,11 @@ describe("followup queue collect routing", () => { }); it("uses the head item authorization for non-collect overflow delivery", async () => { - const key = `test-followup-overflow-auth-${Date.now()}`; - const { calls, done, runFollowup } = createDrainRecorder(3); - const settings = createQueueSettings({ mode: "followup", cap: 2 }); + const { key, calls, done, runFollowup, settings } = createQueueCase( + `test-followup-overflow-auth-${Date.now()}`, + { mode: "followup", cap: 2 }, + 3, + ); const guestRun = (prompt: string) => { const base = createRun({ prompt, @@ -953,8 +958,7 @@ describe("followup queue collect routing", () => { settings, ); - scheduleFollowupDrain(key, runFollowup); - await done.promise; + await drainRecordedQueue(key, runFollowup, done); expect(calls).toHaveLength(3); expect(calls[0]?.prompt).toContain("- dropped guest"); @@ -969,9 +973,11 @@ describe("followup queue collect routing", () => { }); it("batches compatible overflow sources into one summary run", async () => { - const key = `test-collect-overflow-group-${Date.now()}`; - const { calls, done, runFollowup } = createDrainRecorder(2); - const settings = createQueueSettings({ cap: 3 }); + const { key, calls, done, runFollowup, settings } = createQueueCase( + `test-collect-overflow-group-${Date.now()}`, + { cap: 3 }, + 2, + ); for (const [prompt, model] of [ ["direct A", "model-a"], @@ -1009,8 +1015,7 @@ describe("followup queue collect routing", () => { ); } - scheduleFollowupDrain(key, runFollowup); - await done.promise; + await drainRecordedQueue(key, runFollowup, done); expect(calls).toHaveLength(2); expect(calls[0]?.prompt).toContain("[Queue overflow] Dropped 3 messages due to cap."); @@ -1082,9 +1087,11 @@ describe("followup queue collect routing", () => { }); it("uses the newest run for a fully elided overflow segment", async () => { - const key = `test-collect-overflow-elided-latest-run-${Date.now()}`; - const { calls, done, runFollowup } = createDrainRecorder(3); - const settings = createQueueSettings({ cap: 1 }); + const { key, calls, done, runFollowup, settings } = createQueueCase( + `test-collect-overflow-elided-latest-run-${Date.now()}`, + { cap: 1 }, + 3, + ); for (const [prompt, model, authProfileId, chatType] of [ ["first", "model-a", "auth-a", "direct"], @@ -1124,9 +1131,11 @@ describe("followup queue collect routing", () => { }); it("splits overflow groups when source delivery policy changes", async () => { - const key = `test-collect-overflow-delivery-policy-${Date.now()}`; - const { calls, done, runFollowup } = createDrainRecorder(3); - const settings = createQueueSettings({ cap: 2 }); + const { key, calls, done, runFollowup, settings } = createQueueCase( + `test-collect-overflow-delivery-policy-${Date.now()}`, + { cap: 2 }, + 3, + ); const createSource = ( prompt: string, sourceReplyDeliveryMode: NonNullable, @@ -1161,8 +1170,7 @@ describe("followup queue collect routing", () => { ); } - scheduleFollowupDrain(key, runFollowup); - await done.promise; + await drainRecordedQueue(key, runFollowup, done); expect(calls).toHaveLength(3); expect(calls[0]?.prompt).toContain("- automatic source"); @@ -1174,9 +1182,11 @@ describe("followup queue collect routing", () => { }); it("splits overflow groups when runtime policy identity changes", async () => { - const key = `test-collect-overflow-runtime-policy-${Date.now()}`; - const { calls, done, runFollowup } = createDrainRecorder(3); - const settings = createQueueSettings({ cap: 2 }); + const { key, calls, done, runFollowup, settings } = createQueueCase( + `test-collect-overflow-runtime-policy-${Date.now()}`, + { cap: 2 }, + 3, + ); const createSource = (prompt: string, runtimePolicySessionKey: string) => { const base = createRun({ prompt, @@ -1208,8 +1218,7 @@ describe("followup queue collect routing", () => { ); } - scheduleFollowupDrain(key, runFollowup); - await done.promise; + await drainRecordedQueue(key, runFollowup, done); expect(calls).toHaveLength(3); expect(calls[0]?.prompt).toContain("- policy one"); @@ -1221,12 +1230,15 @@ describe("followup queue collect routing", () => { }); it("preserves the source message id for standalone overflow summaries", async () => { - const key = `test-collect-overflow-message-id-${Date.now()}`; - const { calls, done, runFollowup } = createDrainRecorder(2); - const settings = createQueueSettings({ cap: 1 }); + const { key, calls, done, runFollowup, settings } = createQueueCase( + `test-collect-overflow-message-id-${Date.now()}`, + { cap: 1 }, + 2, + ); - enqueueTestRun( + enqueueTestRuns( key, + settings, { prompt: "dropped source", messageId: "message-42", @@ -1234,21 +1246,15 @@ describe("followup queue collect routing", () => { originatingTo: "channel:A", originatingChatType: "channel", }, - settings, - ); - enqueueTestRun( - key, { prompt: "survivor", originatingChannel: "slack", originatingTo: "channel:B", originatingChatType: "channel", }, - settings, ); - scheduleFollowupDrain(key, runFollowup); - await done.promise; + await drainRecordedQueue(key, runFollowup, done); expect(calls[0]?.prompt).toContain("- dropped source"); expect(calls[0]?.messageId).toBe("message-42"); @@ -1261,33 +1267,30 @@ describe("followup queue collect routing", () => { ] as const)( "separates overflow when the %s chat type is missing", async (_missingSide, droppedChatType, survivingChatType) => { - const key = `test-collect-overflow-missing-chat-${_missingSide}-${Date.now()}`; - const { calls, done, runFollowup } = createDrainRecorder(2); - const settings = createQueueSettings({ cap: 1 }); + const { key, calls, done, runFollowup, settings } = createQueueCase( + `test-collect-overflow-missing-chat-${_missingSide}-${Date.now()}`, + { cap: 1 }, + 2, + ); - enqueueTestRun( + enqueueTestRuns( key, + settings, { prompt: "dropped content", originatingChannel: "slack", originatingTo: "same-target", originatingChatType: droppedChatType, }, - settings, - ); - enqueueTestRun( - key, { prompt: "surviving content", originatingChannel: "slack", originatingTo: "same-target", originatingChatType: survivingChatType, }, - settings, ); - scheduleFollowupDrain(key, runFollowup); - await done.promise; + await drainRecordedQueue(key, runFollowup, done); expect(calls[0]?.prompt).toContain("- dropped content"); expect(calls[0]?.originatingChatType).toBe(droppedChatType); @@ -1348,8 +1351,7 @@ describe("followup queue collect routing", () => { nextModel: "current-model", }); - scheduleFollowupDrain(key, runFollowup); - await done.promise; + await drainRecordedQueue(key, runFollowup, done); expect(calls).toHaveLength(1); expect(calls[0]?.run.model).toBe("current-model"); @@ -1514,29 +1516,26 @@ describe("followup queue collect routing", () => { }); it("keeps deferred overflow summary text paired with its source route", async () => { - const key = `test-collect-overflow-deferred-pairs-${Date.now()}`; - const { calls, done } = createDrainRecorder(); - const settings = createQueueSettings({ cap: 1 }); + const { key, calls, done, settings } = createQueueCase( + `test-collect-overflow-deferred-pairs-${Date.now()}`, + { cap: 1 }, + ); - enqueueTestRun( + enqueueTestRuns( key, + settings, { prompt: "source A", originatingChannel: "slack", originatingTo: "same-target", originatingChatType: "direct", }, - settings, - ); - enqueueTestRun( - key, { prompt: "source B", originatingChannel: "slack", originatingTo: "same-target", originatingChatType: "direct", }, - settings, ); scheduleFollowupDrain(key, async (run) => { @@ -1570,40 +1569,33 @@ describe("followup queue collect routing", () => { }); it("collects compatible items after one cross-channel drain", async () => { - const key = `test-collect-after-cross-${Date.now()}`; - const { calls, done, runFollowup } = createDrainRecorder(2); - const settings = createQueueSettings(); + const { key, calls, done, runFollowup, settings } = createQueueCase( + `test-collect-after-cross-${Date.now()}`, + {}, + 2, + ); - enqueueTestRun( + enqueueTestRuns( key, + settings, { prompt: "first route", originatingChannel: "slack", originatingTo: "channel:A", }, - settings, - ); - enqueueTestRun( - key, { prompt: "second route one", originatingChannel: "slack", originatingTo: "channel:B", }, - settings, - ); - enqueueTestRun( - key, { prompt: "second route two", originatingChannel: "slack", originatingTo: "channel:B", }, - settings, ); - scheduleFollowupDrain(key, runFollowup); - await done.promise; + await drainRecordedQueue(key, runFollowup, done); expect(calls).toHaveLength(2); expect(calls[0]?.prompt).toBe("first route"); @@ -1615,34 +1607,31 @@ describe("followup queue collect routing", () => { }); it("drains unresolved-origin items separately from a routed batch", async () => { - const key = `test-collect-unresolved-origin-${Date.now()}`; - const { calls, done, runFollowup } = createDrainRecorder(2); - const settings = createQueueSettings(); + const { key, calls, done, runFollowup, settings } = createQueueCase( + `test-collect-unresolved-origin-${Date.now()}`, + {}, + 2, + ); enqueueFollowupRun(key, createRun({ prompt: "unresolved origin" }), settings); - enqueueTestRun( + enqueueTestRuns( key, + settings, { prompt: "keyed one", originatingChannel: "slack", originatingTo: "channel:B", originatingChatType: "channel", }, - settings, - ); - enqueueTestRun( - key, { prompt: "keyed two", originatingChannel: "slack", originatingTo: "channel:B", originatingChatType: "channel", }, - settings, ); - scheduleFollowupDrain(key, runFollowup); - await done.promise; + await drainRecordedQueue(key, runFollowup, done); expect(calls).toHaveLength(2); expect(calls[0]?.prompt).toContain("[Queued messages while agent was busy]"); @@ -1657,41 +1646,34 @@ describe("followup queue collect routing", () => { }); it("does not collect known route-less chat types into another destination", async () => { - const key = `test-collect-known-chat-without-route-${Date.now()}`; - const { calls, done, runFollowup } = createDrainRecorder(2); - const settings = createQueueSettings(); + const { key, calls, done, runFollowup, settings } = createQueueCase( + `test-collect-known-chat-without-route-${Date.now()}`, + {}, + 2, + ); - enqueueTestRun( + enqueueTestRuns( key, + settings, { prompt: "unresolved direct", originatingChatType: "direct", }, - settings, - ); - enqueueTestRun( - key, { prompt: "channel one", originatingChannel: "slack", originatingTo: "channel:B", originatingChatType: "channel", }, - settings, - ); - enqueueTestRun( - key, { prompt: "channel two", originatingChannel: "slack", originatingTo: "channel:B", originatingChatType: "channel", }, - settings, ); - scheduleFollowupDrain(key, runFollowup); - await done.promise; + await drainRecordedQueue(key, runFollowup, done); expect(calls[0]?.prompt).toBe("unresolved direct"); expect(calls[0]?.originatingChatType).toBe("direct"); @@ -1702,33 +1684,28 @@ describe("followup queue collect routing", () => { }); it("collects ordinary user-request followups with current turn kind", async () => { - const key = `test-collect-user-request-kind-${Date.now()}`; - const { calls, done, runFollowup } = createDrainRecorder(); - const settings = createQueueSettings(); + const { key, calls, done, runFollowup, settings } = createQueueCase( + `test-collect-user-request-kind-${Date.now()}`, + ); - enqueueTestRun( + enqueueTestRuns( key, + settings, { prompt: "one", currentInboundEventKind: "user_request", originatingChannel: "slack", originatingTo: "channel:A", }, - settings, - ); - enqueueTestRun( - key, { prompt: "two", currentInboundEventKind: "user_request", originatingChannel: "slack", originatingTo: "channel:A", }, - settings, ); - scheduleFollowupDrain(key, runFollowup); - await done.promise; + await drainRecordedQueue(key, runFollowup, done); expect(calls).toHaveLength(1); expect(calls[0]?.prompt).toContain("[Queued messages while agent was busy]"); @@ -1737,9 +1714,11 @@ describe("followup queue collect routing", () => { }); it("drains runtime-context followups individually instead of collecting them", async () => { - const key = `test-collect-runtime-context-${Date.now()}`; - const { calls, done, runFollowup } = createDrainRecorder(2); - const settings = createQueueSettings(); + const { key, calls, done, runFollowup, settings } = createQueueCase( + `test-collect-runtime-context-${Date.now()}`, + {}, + 2, + ); const controller = new AbortController(); const begin = () => () => undefined; const lifecycle = { onAdopted: async () => {}, onSettled: () => undefined }; @@ -1773,8 +1752,7 @@ describe("followup queue collect routing", () => { settings, ); - scheduleFollowupDrain(key, runFollowup); - await done.promise; + await drainRecordedQueue(key, runFollowup, done); expect(calls).toHaveLength(2); expect(calls[0]?.prompt).toBe("[OpenClaw room event]"); @@ -1789,9 +1767,11 @@ describe("followup queue collect routing", () => { it("drains a disableCollectBatching retry individually instead of collecting it", async () => { const strandedReplyRetryMarker = "stranded-reply-retry"; - const key = `test-collect-disable-batching-${Date.now()}`; - const { calls, done, runFollowup } = createDrainRecorder(3); - const settings = createQueueSettings(); + const { key, calls, done, runFollowup, settings } = createQueueCase( + `test-collect-disable-batching-${Date.now()}`, + {}, + 3, + ); const route = { originatingChannel: "slack" as const, originatingTo: "channel:A" }; const retryPrompt = "[System] Please deliver this reply now by calling message(action=send)."; @@ -1808,8 +1788,7 @@ describe("followup queue collect routing", () => { ); enqueueFollowupRun(key, createRun({ prompt: "normal two", ...route }), settings); - scheduleFollowupDrain(key, runFollowup); - await done.promise; + await drainRecordedQueue(key, runFollowup, done); expect(calls).toHaveLength(3); const retryCall = calls.find((call) => call.prompt === retryPrompt); @@ -1973,9 +1952,11 @@ describe("followup queue collect routing", () => { }); it("drains protected priority followups before overflow summaries", async () => { - const key = `test-priority-followup-before-summary-${Date.now()}`; - const { calls, done, runFollowup } = createDrainRecorder(2); - const settings = createQueueSettings({ mode: "followup", cap: 1 }); + const { key, calls, done, runFollowup, settings } = createQueueCase( + `test-priority-followup-before-summary-${Date.now()}`, + { mode: "followup", cap: 1 }, + 2, + ); enqueueFollowupRun(key, createRun({ prompt: "overflowed normal" }), settings); enqueueFollowupRun( @@ -1988,8 +1969,7 @@ describe("followup queue collect routing", () => { { position: "front" }, ); - scheduleFollowupDrain(key, runFollowup); - await done.promise; + await drainRecordedQueue(key, runFollowup, done); expect(calls).toHaveLength(2); expect(calls[0]?.prompt).toBe("priority retry"); @@ -1998,9 +1978,9 @@ describe("followup queue collect routing", () => { }); it("carries image payloads across collected batches", async () => { - const key = `test-collect-images-${Date.now()}`; - const { calls, done, runFollowup } = createDrainRecorder(); - const settings = createQueueSettings(); + const { key, calls, done, runFollowup, settings } = createQueueCase( + `test-collect-images-${Date.now()}`, + ); const firstImage = { type: "image" as const, data: "first", mimeType: "image/png" }; const secondImage = { type: "image" as const, data: "second", mimeType: "image/png" }; @@ -2031,17 +2011,18 @@ describe("followup queue collect routing", () => { settings, ); - scheduleFollowupDrain(key, runFollowup); - await done.promise; + await drainRecordedQueue(key, runFollowup, done); expect(calls[0]?.images).toEqual([firstImage, secondImage]); expect(calls[0]?.imageOrder).toEqual(["inline", "inline"]); }); it("splits collect batches when sender authorization changes", async () => { - const key = `test-collect-auth-split-${Date.now()}`; - const { calls, done, runFollowup } = createDrainRecorder(2); - const settings = createQueueSettings(); + const { key, calls, done, runFollowup, settings } = createQueueCase( + `test-collect-auth-split-${Date.now()}`, + {}, + 2, + ); const nonOwner = createRun({ prompt: "use the gateway tool", @@ -2080,8 +2061,7 @@ describe("followup queue collect routing", () => { settings, ); - scheduleFollowupDrain(key, runFollowup); - await done.promise; + await drainRecordedQueue(key, runFollowup, done); expect(calls.map((call) => call.run.senderIsOwner)).toEqual([false, true]); expect(calls[0]?.prompt).toContain("use the gateway tool"); @@ -2113,8 +2093,7 @@ describe("followup queue collect routing", () => { ); } - scheduleFollowupDrain(key, runFollowup); - await done.promise; + await drainRecordedQueue(key, runFollowup, done); expect(calls).toHaveLength(2); expect(calls[0]?.prompt).toContain("first"); @@ -2124,9 +2103,9 @@ describe("followup queue collect routing", () => { }); it("keeps one collect batch when authorization context matches", async () => { - const key = `test-collect-auth-match-${Date.now()}`; - const { calls, done, runFollowup } = createDrainRecorder(); - const settings = createQueueSettings(); + const { key, calls, done, runFollowup, settings } = createQueueCase( + `test-collect-auth-match-${Date.now()}`, + ); const first = createRun({ prompt: "first", @@ -2168,8 +2147,7 @@ describe("followup queue collect routing", () => { settings, ); - scheduleFollowupDrain(key, runFollowup); - await done.promise; + await drainRecordedQueue(key, runFollowup, done); expect(calls).toHaveLength(1); expect(calls[0]?.prompt).toContain("first"); @@ -2179,9 +2157,9 @@ describe("followup queue collect routing", () => { }); it("keeps one collect batch when only sender display fields drift", async () => { - const key = `test-collect-auth-display-drift-${Date.now()}`; - const { calls, done, runFollowup } = createDrainRecorder(); - const settings = createQueueSettings(); + const { key, calls, done, runFollowup, settings } = createQueueCase( + `test-collect-auth-display-drift-${Date.now()}`, + ); const first = createRun({ prompt: "first", @@ -2223,8 +2201,7 @@ describe("followup queue collect routing", () => { settings, ); - scheduleFollowupDrain(key, runFollowup); - await done.promise; + await drainRecordedQueue(key, runFollowup, done); expect(calls).toHaveLength(1); expect(calls[0]?.prompt).toContain("first"); @@ -2234,9 +2211,11 @@ describe("followup queue collect routing", () => { }); it("splits collect batches when exec context changes", async () => { - const key = `test-collect-exec-split-${Date.now()}`; - const { calls, done, runFollowup } = createDrainRecorder(2); - const settings = createQueueSettings(); + const { key, calls, done, runFollowup, settings } = createQueueCase( + `test-collect-exec-split-${Date.now()}`, + {}, + 2, + ); const base = createRun({ prompt: "first", @@ -2276,8 +2255,7 @@ describe("followup queue collect routing", () => { settings, ); - scheduleFollowupDrain(key, runFollowup); - await done.promise; + await drainRecordedQueue(key, runFollowup, done); expect(calls[0]?.prompt).toContain("first"); expect(calls[0]?.prompt).not.toContain("second"); @@ -2287,9 +2265,9 @@ describe("followup queue collect routing", () => { }); it("uses the newest run within a matching authorization batch", async () => { - const key = `test-collect-latest-run-${Date.now()}`; - const { calls, done, runFollowup } = createDrainRecorder(); - const settings = createQueueSettings(); + const { key, calls, done, runFollowup, settings } = createQueueCase( + `test-collect-latest-run-${Date.now()}`, + ); const first = createRun({ prompt: "first", originatingChannel: "slack", originatingTo: "A" }); const second = createRun({ @@ -2329,8 +2307,7 @@ describe("followup queue collect routing", () => { settings, ); - scheduleFollowupDrain(key, runFollowup); - await done.promise; + await drainRecordedQueue(key, runFollowup, done); expect(calls).toHaveLength(1); expect(calls[0]?.run.provider).toBe("anthropic"); @@ -2338,40 +2315,33 @@ describe("followup queue collect routing", () => { }); it("delivers summary-only collect work under its source route", async () => { - const key = `test-collect-summary-only-${Date.now()}`; - const { calls, done, runFollowup } = createDrainRecorder(3); - const settings = createQueueSettings({ cap: 2 }); + const { key, calls, done, runFollowup, settings } = createQueueCase( + `test-collect-summary-only-${Date.now()}`, + { cap: 2 }, + 3, + ); - enqueueTestRun( + enqueueTestRuns( key, + settings, { prompt: "first", originatingChannel: "slack", originatingTo: "channel:A", }, - settings, - ); - enqueueTestRun( - key, { prompt: "second", originatingChannel: "slack", originatingTo: "channel:B", }, - settings, - ); - enqueueTestRun( - key, { prompt: "third", originatingChannel: "slack", originatingTo: "channel:C", }, - settings, ); - scheduleFollowupDrain(key, runFollowup); - await done.promise; + await drainRecordedQueue(key, runFollowup, done); expect(calls).toHaveLength(3); expect(calls[0]?.prompt).toContain("[Queue overflow] Dropped 1 message due to cap."); @@ -2382,9 +2352,11 @@ describe("followup queue collect routing", () => { }); it("preserves collect order when authorization changes more than once", async () => { - const key = `test-collect-auth-order-${Date.now()}`; - const { calls, done, runFollowup } = createDrainRecorder(3); - const settings = createQueueSettings(); + const { key, calls, done, runFollowup, settings } = createQueueCase( + `test-collect-auth-order-${Date.now()}`, + {}, + 3, + ); const first = createRun({ prompt: "first", originatingChannel: "slack", originatingTo: "A" }); const second = createRun({ prompt: "second", originatingChannel: "slack", originatingTo: "A" }); @@ -2415,8 +2387,7 @@ describe("followup queue collect routing", () => { settings, ); - scheduleFollowupDrain(key, runFollowup); - await done.promise; + await drainRecordedQueue(key, runFollowup, done); expect(calls.map((call) => call.prompt)).toEqual([ "[Queued messages while agent was busy]\n\n---\nQueued #1 (from A)\nfirst", @@ -2426,65 +2397,55 @@ describe("followup queue collect routing", () => { }); it("collects Slack messages in same thread and preserves string thread id", async () => { - const key = `test-collect-slack-thread-same-${Date.now()}`; - const { calls, done, runFollowup } = createDrainRecorder(); - const settings = createQueueSettings(); + const { key, calls, done, runFollowup, settings } = createQueueCase( + `test-collect-slack-thread-same-${Date.now()}`, + ); - enqueueTestRun( + enqueueTestRuns( key, + settings, { prompt: "one", originatingChannel: "slack", originatingTo: "channel:A", originatingThreadId: "1706000000.000001", }, - settings, - ); - enqueueTestRun( - key, { prompt: "two", originatingChannel: "slack", originatingTo: "channel:A", originatingThreadId: "1706000000.000001", }, - settings, ); - scheduleFollowupDrain(key, runFollowup); - await done.promise; + await drainRecordedQueue(key, runFollowup, done); expect(calls[0]?.prompt).toContain("[Queued messages while agent was busy]"); expect(calls[0]?.originatingThreadId).toBe("1706000000.000001"); }); it("collects messages when numeric and string thread ids share the route key", async () => { - const key = `test-collect-thread-normalized-${Date.now()}`; - const { calls, done, runFollowup } = createDrainRecorder(); - const settings = createQueueSettings(); + const { key, calls, done, runFollowup, settings } = createQueueCase( + `test-collect-thread-normalized-${Date.now()}`, + ); - enqueueTestRun( + enqueueTestRuns( key, + settings, { prompt: "one", originatingChannel: "telegram", originatingTo: "-100123", originatingThreadId: 42.9, }, - settings, - ); - enqueueTestRun( - key, { prompt: "two", originatingChannel: "telegram", originatingTo: "-100123", originatingThreadId: "42", }, - settings, ); - scheduleFollowupDrain(key, runFollowup); - await done.promise; + await drainRecordedQueue(key, runFollowup, done); expect(calls).toHaveLength(1); expect(calls[0]?.prompt).toContain("[Queued messages while agent was busy]"); expect(calls[0]?.prompt).toContain("one"); @@ -2496,28 +2457,23 @@ describe("followup queue collect routing", () => { const { calls, done, runFollowup } = createDrainRecorder(); const settings: QueueSettings = { mode: "collect", debounceMs: 0 }; - enqueueTestRun( + enqueueTestRuns( key, + settings, { prompt: "one", messageId: "webchat-message-1", originatingChannel: "webchat", originatingReplyToMode: "all", }, - settings, - ); - enqueueTestRun( - key, { prompt: "two", messageId: "webchat-message-2", originatingChannel: "webchat", originatingReplyToMode: "all", }, - settings, ); - scheduleFollowupDrain(key, runFollowup); - await done.promise; + await drainRecordedQueue(key, runFollowup, done); expect(calls).toHaveLength(1); expect(calls[0]?.prompt).toContain("one"); @@ -2525,33 +2481,30 @@ describe("followup queue collect routing", () => { }); it("does not collect Slack messages when thread ids differ", async () => { - const key = `test-collect-slack-thread-diff-${Date.now()}`; - const { calls, done, runFollowup } = createDrainRecorder(2); - const settings = createQueueSettings(); + const { key, calls, done, runFollowup, settings } = createQueueCase( + `test-collect-slack-thread-diff-${Date.now()}`, + {}, + 2, + ); - enqueueTestRun( + enqueueTestRuns( key, + settings, { prompt: "one", originatingChannel: "slack", originatingTo: "channel:A", originatingThreadId: "1706000000.000001", }, - settings, - ); - enqueueTestRun( - key, { prompt: "two", originatingChannel: "slack", originatingTo: "channel:A", originatingThreadId: "1706000000.000002", }, - settings, ); - scheduleFollowupDrain(key, runFollowup); - await done.promise; + await drainRecordedQueue(key, runFollowup, done); expect(calls[0]?.prompt).toBe("one"); expect(calls[1]?.prompt).toBe("two"); expect(calls[0]?.originatingThreadId).toBe("1706000000.000001"); @@ -2575,8 +2528,7 @@ describe("followup queue collect routing", () => { enqueueFollowupRun(key, createRun({ prompt: "one" }), settings); enqueueFollowupRun(key, createRun({ prompt: "two" }), settings); - scheduleFollowupDrain(key, runFollowup); - await done.promise; + await drainRecordedQueue(key, runFollowup, done); expect(calls[0]?.prompt).toContain("Queued #1\none"); expect(calls[0]?.prompt).toContain("Queued #2\ntwo"); }); @@ -2638,8 +2590,7 @@ describe("followup queue collect routing", () => { settings, ); - scheduleFollowupDrain(key, runFollowup); - await done.promise; + await drainRecordedQueue(key, runFollowup, done); const guestAttempts = attempts.filter((call) => call.prompt.includes("guest message")); const ownerAttempts = attempts.filter((call) => call.prompt.includes("owner message")); @@ -2670,8 +2621,7 @@ describe("followup queue collect routing", () => { enqueueFollowupRun(key, createRun({ prompt: "first" }), settings); enqueueFollowupRun(key, createRun({ prompt: "second" }), settings); - scheduleFollowupDrain(key, runFollowup); - await done.promise; + await drainRecordedQueue(key, runFollowup, done); expect(calls[0]?.prompt).toContain("[Queue overflow] Dropped 1 message due to cap."); expect(calls[0]?.prompt).toContain("- first"); }); @@ -2680,9 +2630,10 @@ describe("followup queue collect routing", () => { const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-overflow-session-")); const storePath = path.join(tempDir, "sessions.json"); const oldTranscriptPath = path.join(tempDir, "old-session.jsonl"); - const key = `test-overflow-summary-session-rotation-${Date.now()}`; - const { calls, done } = createDrainRecorder(); - const settings = createQueueSettings({ mode: "followup", cap: 1 }); + const { key, calls, done, settings } = createQueueCase( + `test-overflow-summary-session-rotation-${Date.now()}`, + { mode: "followup", cap: 1 }, + ); try { await replaceSessionEntry( @@ -2773,9 +2724,11 @@ describe("followup queue collect routing", () => { }); it("delivers the overflow summary before split auth groups", async () => { - const key = `test-collect-overflow-summary-once-${Date.now()}`; - const { calls, done, runFollowup } = createDrainRecorder(3); - const settings = createQueueSettings({ cap: 2 }); + const { key, calls, done, runFollowup, settings } = createQueueCase( + `test-collect-overflow-summary-once-${Date.now()}`, + { cap: 2 }, + 3, + ); const droppedGuest = createRun({ prompt: "dropped guest message", @@ -2833,8 +2786,7 @@ describe("followup queue collect routing", () => { settings, ); - scheduleFollowupDrain(key, runFollowup); - await done.promise; + await drainRecordedQueue(key, runFollowup, done); expect(calls).toHaveLength(3); expect(calls[0]?.prompt).toContain("[Queue overflow] Dropped 1 message due to cap."); @@ -2919,8 +2871,7 @@ describe("followup queue collect routing", () => { settings, ); - scheduleFollowupDrain(key, runFollowup); - await done.promise; + await drainRecordedQueue(key, runFollowup, done); expect(calls).toHaveLength(3); expect(calls[0]?.prompt).toContain("[Queue overflow] Dropped 1 message due to cap."); @@ -2933,12 +2884,14 @@ describe("followup queue collect routing", () => { }); it("preserves routing metadata on overflow summary followups", async () => { - const key = `test-overflow-summary-routing-${Date.now()}`; - const { calls, done, runFollowup } = createDrainRecorder(); - const settings = createQueueSettings({ mode: "followup", cap: 1 }); + const { key, calls, done, runFollowup, settings } = createQueueCase( + `test-overflow-summary-routing-${Date.now()}`, + { mode: "followup", cap: 1 }, + ); - enqueueTestRun( + enqueueTestRuns( key, + settings, { prompt: "first", originatingChannel: "discord", @@ -2946,10 +2899,6 @@ describe("followup queue collect routing", () => { originatingAccountId: "work", originatingThreadId: "1739142736.000100", }, - settings, - ); - enqueueTestRun( - key, { prompt: "second", originatingChannel: "discord", @@ -2957,11 +2906,9 @@ describe("followup queue collect routing", () => { originatingAccountId: "work", originatingThreadId: "1739142736.000100", }, - settings, ); - scheduleFollowupDrain(key, runFollowup); - await done.promise; + await drainRecordedQueue(key, runFollowup, done); expect(calls[0]?.originatingChannel).toBe("discord"); expect(calls[0]?.originatingTo).toBe("channel:C1"); @@ -3007,8 +2954,7 @@ describe("followup queue collect routing", () => { settings, ); - scheduleFollowupDrain(key, runFollowup); - await done.promise; + await drainRecordedQueue(key, runFollowup, done); expect(calls).toHaveLength(2); expect(calls[0]?.prompt).toContain("[Queue overflow] Dropped 1 message due to cap."); @@ -3025,9 +2971,11 @@ describe("followup queue collect routing", () => { }); it("keeps mixed overflow summaries as normal followups", async () => { - const key = `test-overflow-summary-mixed-kind-${Date.now()}`; - const { calls, done, runFollowup } = createDrainRecorder(2); - const settings = createQueueSettings({ mode: "followup", cap: 1 }); + const { key, calls, done, runFollowup, settings } = createQueueCase( + `test-overflow-summary-mixed-kind-${Date.now()}`, + { mode: "followup", cap: 1 }, + 2, + ); enqueueFollowupRun( key, @@ -3047,8 +2995,7 @@ describe("followup queue collect routing", () => { ); enqueueFollowupRun(key, createRun({ prompt: "live followup" }), settings); - scheduleFollowupDrain(key, runFollowup); - await done.promise; + await drainRecordedQueue(key, runFollowup, done); expect(calls).toHaveLength(2); expect(calls[0]?.prompt).toContain("[Queue overflow] Dropped 2 messages due to cap."); @@ -3083,8 +3030,7 @@ describe("followup queue collect routing", () => { expect(onComplete).not.toHaveBeenCalled(); - scheduleFollowupDrain(key, runFollowup); - await done.promise; + await drainRecordedQueue(key, runFollowup, done); expect(calls).toHaveLength(1); expect(calls[0]?.prompt).toBe("live followup"); @@ -3276,8 +3222,7 @@ describe("followup queue collect routing", () => { settings, ); - scheduleFollowupDrain(key, runFollowup); - await done.promise; + await drainRecordedQueue(key, runFollowup, done); expect(calls).toHaveLength(1); expect(calls[0]?.prompt).toContain("first"); @@ -3387,8 +3332,7 @@ describe("followup queue collect routing", () => { ); } - scheduleFollowupDrain(key, runFollowup); - await done.promise; + await drainRecordedQueue(key, runFollowup, done); expect(calls).toHaveLength(1); expect(calls[0]?.prompt).toContain("first"); @@ -3436,8 +3380,7 @@ describe("followup queue collect routing", () => { ); } - scheduleFollowupDrain(key, runFollowup); - await done.promise; + await drainRecordedQueue(key, runFollowup, done); expect(calls).toHaveLength(1); expect(calls[0]?.prompt).toContain("first"); @@ -3616,8 +3559,7 @@ describe("followup queue collect routing", () => { ); aborted.abort(); - scheduleFollowupDrain(key, runFollowup); - await done.promise; + await drainRecordedQueue(key, runFollowup, done); expect(calls).toHaveLength(1); expect(calls[0]?.prompt).toContain("owner A summary"); @@ -3656,8 +3598,7 @@ describe("followup queue collect routing", () => { enqueueFollowupRun(key, createRun({ prompt: "live item" }), settings); elided.abort(); - scheduleFollowupDrain(key, runFollowup); - await done.promise; + await drainRecordedQueue(key, runFollowup, done); expect(calls.map((call) => call.prompt).join("\n")).not.toContain("elided and cancelled"); expect(calls[0]?.prompt).toContain("retained summary"); diff --git a/src/channels/plugins/message-actions.security.test.ts b/src/channels/plugins/message-actions.security.test.ts index b4c3218ff0c1..f801e853d693 100644 --- a/src/channels/plugins/message-actions.security.test.ts +++ b/src/channels/plugins/message-actions.security.test.ts @@ -8,6 +8,15 @@ import { createTestRegistry, } from "../../test-utils/channel-plugins.js"; import { dispatchChannelMessageAction } from "./message-action-dispatch.js"; + +function dispatchTestChannelMessageAction( + overrides: Omit[0], "cfg">, +) { + return dispatchChannelMessageAction({ + cfg: {} as OpenClawConfig, + ...overrides, + }); +} import type { ChannelMessageActionContext, ChannelPlugin } from "./types.js"; const handleAction = vi.fn(async (_ctx: ChannelMessageActionContext) => jsonResult({ ok: true })); @@ -46,10 +55,9 @@ describe("dispatchChannelMessageAction trusted sender guard", () => { it("rejects privileged discord moderation action without trusted sender in tool context", async () => { await expect( - dispatchChannelMessageAction({ + dispatchTestChannelMessageAction({ channel: "discord", action: "kick", - cfg: {} as OpenClawConfig, params: { guildId: "g1", userId: "u1" }, toolContext: { currentChannelProvider: "discord" }, }), @@ -58,10 +66,9 @@ describe("dispatchChannelMessageAction trusted sender guard", () => { }); it("allows privileged discord moderation action with trusted sender in tool context", async () => { - await dispatchChannelMessageAction({ + await dispatchTestChannelMessageAction({ channel: "discord", action: "kick", - cfg: {} as OpenClawConfig, params: { guildId: "g1", userId: "u1" }, requesterSenderId: "trusted-user", toolContext: { currentChannelProvider: "discord" }, @@ -71,10 +78,9 @@ describe("dispatchChannelMessageAction trusted sender guard", () => { }); it("does not require trusted sender without tool context", async () => { - await dispatchChannelMessageAction({ + await dispatchTestChannelMessageAction({ channel: "discord", action: "kick", - cfg: {} as OpenClawConfig, params: { guildId: "g1", userId: "u1" }, }); @@ -150,10 +156,9 @@ describe("dispatchChannelMessageAction conversation-read provenance", () => { it("allows a non-bundled delegated read of the exact current conversation and account", async () => { setReadPlugin(); - await dispatchChannelMessageAction({ + await dispatchTestChannelMessageAction({ channel: "discord", action: "read", - cfg: {} as OpenClawConfig, params: { channelId: "channel:current" }, accountId: "Work", requesterAccountId: "work", @@ -176,10 +181,9 @@ describe("dispatchChannelMessageAction conversation-read provenance", () => { setReadPlugin(); await expect( - dispatchChannelMessageAction({ + dispatchTestChannelMessageAction({ channel: "discord", action: "read", - cfg: {} as OpenClawConfig, params: { channelId: "other", conversationReadOrigin: "direct-operator", @@ -205,10 +209,9 @@ describe("dispatchChannelMessageAction conversation-read provenance", () => { it("rejects a non-core runtime action before any plugin callback", async () => { setReadPlugin(); - const result = await dispatchChannelMessageAction({ + const result = await dispatchTestChannelMessageAction({ channel: "discord", action: "forged-read" as never, - cfg: {} as OpenClawConfig, params: { channelId: "other" }, conversationReadOrigin: "direct-operator", }); @@ -222,10 +225,9 @@ describe("dispatchChannelMessageAction conversation-read provenance", () => { it("matches a sanitized channelId to a typed current-channel target", async () => { setReadPlugin(); - await dispatchChannelMessageAction({ + await dispatchTestChannelMessageAction({ channel: "discord", action: "read", - cfg: {} as OpenClawConfig, params: { target: "current", channelId: "current", @@ -291,10 +293,9 @@ describe("dispatchChannelMessageAction conversation-read provenance", () => { setReadPlugin(); await expect( - dispatchChannelMessageAction({ + dispatchTestChannelMessageAction({ channel: "discord", action: "read", - cfg: {} as OpenClawConfig, params: testCase.params, accountId: testCase.accountId, requesterAccountId: testCase.requesterAccountId, @@ -314,10 +315,9 @@ describe("dispatchChannelMessageAction conversation-read provenance", () => { it("allows direct operators through a non-bundled adapter", async () => { setReadPlugin(); - await dispatchChannelMessageAction({ + await dispatchTestChannelMessageAction({ channel: "discord", action: "read", - cfg: {} as OpenClawConfig, params: { channelId: "other" }, conversationReadOrigin: "direct-operator", }); @@ -329,10 +329,9 @@ describe("dispatchChannelMessageAction conversation-read provenance", () => { setReadPlugin(); await expect( - dispatchChannelMessageAction({ + dispatchTestChannelMessageAction({ channel: "discord", action: "read", - cfg: {} as OpenClawConfig, params: { channelId: "channel:123" }, accountId: "default", requesterAccountId: "default", @@ -374,10 +373,9 @@ describe("dispatchChannelMessageAction conversation-read provenance", () => { setReadPlugin(); await expect( - dispatchChannelMessageAction({ + dispatchTestChannelMessageAction({ channel: "discord", action: "read", - cfg: {} as OpenClawConfig, params: { channelId: "CHANNEL:CURRENT" }, accountId: "default", requesterAccountId: "default", @@ -395,10 +393,9 @@ describe("dispatchChannelMessageAction conversation-read provenance", () => { setReadPlugin(); await expect( - dispatchChannelMessageAction({ + dispatchTestChannelMessageAction({ channel: "discord", action: "read", - cfg: {} as OpenClawConfig, params: { channelId: "current" }, requesterAccountId: "work", conversationReadOrigin: "delegated", @@ -415,10 +412,9 @@ describe("dispatchChannelMessageAction conversation-read provenance", () => { setReadPlugin(); await expect( - dispatchChannelMessageAction({ + dispatchTestChannelMessageAction({ channel: "discord", action: "read", - cfg: {} as OpenClawConfig, params: { target: "user:123" }, accountId: "default", requesterAccountId: "default", @@ -436,10 +432,9 @@ describe("dispatchChannelMessageAction conversation-read provenance", () => { setReadPlugin(); await expect( - dispatchChannelMessageAction({ + dispatchTestChannelMessageAction({ channel: "discord", action: "read", - cfg: {} as OpenClawConfig, params: { target: "channel:123", channelId: "123", @@ -461,10 +456,9 @@ describe("dispatchChannelMessageAction conversation-read provenance", () => { setReadPlugin(); await expect( - dispatchChannelMessageAction({ + dispatchTestChannelMessageAction({ channel: "discord", action: "read", - cfg: {} as OpenClawConfig, params: { target: "123", }, @@ -485,10 +479,9 @@ describe("dispatchChannelMessageAction conversation-read provenance", () => { setReadPlugin(); await expect( - dispatchChannelMessageAction({ + dispatchTestChannelMessageAction({ channel: "discord", action: "read", - cfg: {} as OpenClawConfig, params: { channelId: "current", target: "channel:other", @@ -508,10 +501,9 @@ describe("dispatchChannelMessageAction conversation-read provenance", () => { it("keeps non-read actions compatible on a non-bundled adapter", async () => { setReadPlugin(); - await dispatchChannelMessageAction({ + await dispatchTestChannelMessageAction({ channel: "discord", action: "send", - cfg: {} as OpenClawConfig, params: { to: "other" }, conversationReadOrigin: "delegated", }); @@ -522,10 +514,9 @@ describe("dispatchChannelMessageAction conversation-read provenance", () => { it("delegates configured-target policy to a bundled adapter", async () => { setReadPlugin({ origin: "bundled" }); - await dispatchChannelMessageAction({ + await dispatchTestChannelMessageAction({ channel: "discord", action: "read", - cfg: {} as OpenClawConfig, params: { channelId: "configured" }, conversationReadOrigin: "delegated", }); @@ -572,10 +563,9 @@ describe("dispatchChannelMessageAction conversation-read provenance", () => { setReadPlugin({ channel: "telegram", origin: "bundled" }); await expect( - dispatchChannelMessageAction({ + dispatchTestChannelMessageAction({ channel: "telegram", action: "read", - cfg: {} as OpenClawConfig, params: { channelId: "configured" }, accountId: "default", requesterAccountId: "default", @@ -594,10 +584,9 @@ describe("dispatchChannelMessageAction conversation-read provenance", () => { async (action) => { setReadPlugin({ channel: "telegram", origin: "bundled" }); - await dispatchChannelMessageAction({ + await dispatchTestChannelMessageAction({ channel: "telegram", action, - cfg: {} as OpenClawConfig, params: { chatId: "-1001" }, accountId: "default", requesterAccountId: "default", @@ -617,10 +606,9 @@ describe("dispatchChannelMessageAction conversation-read provenance", () => { setReadPlugin({ channel: "telegram", origin: "workspace" }); await expect( - dispatchChannelMessageAction({ + dispatchTestChannelMessageAction({ channel: "telegram", action: "react", - cfg: {} as OpenClawConfig, params: { chatId: "-1001" }, accountId: "default", requesterAccountId: "default", @@ -650,10 +638,9 @@ describe("dispatchChannelMessageAction conversation-read provenance", () => { normalizeTarget, }); - await dispatchChannelMessageAction({ + await dispatchTestChannelMessageAction({ channel: "nextcloud-talk", action: "read", - cfg: {} as OpenClawConfig, params: { to: "nc:room:Current" }, accountId: "default", requesterAccountId: "default", @@ -677,10 +664,9 @@ describe("dispatchChannelMessageAction conversation-read provenance", () => { }); await expect( - dispatchChannelMessageAction({ + dispatchTestChannelMessageAction({ channel: "discord", action: "read", - cfg: {} as OpenClawConfig, params: { channelId: "other" }, accountId: "default", requesterAccountId: "default", @@ -706,10 +692,9 @@ describe("dispatchChannelMessageAction conversation-read provenance", () => { normalizeTarget, }); - await dispatchChannelMessageAction({ + await dispatchTestChannelMessageAction({ channel: "nextcloud-talk", action: "read", - cfg: {} as OpenClawConfig, params: { target, to: "nextcloud-talk:current", @@ -860,10 +845,9 @@ describe("dispatchChannelMessageAction conversation-read provenance", () => { }); await expect( - dispatchChannelMessageAction({ + dispatchTestChannelMessageAction({ channel: "nextcloud-talk", action: "read", - cfg: {} as OpenClawConfig, params: { target: "user:current", to: "nextcloud-talk:current", @@ -888,10 +872,9 @@ describe("dispatchChannelMessageAction conversation-read provenance", () => { }); await expect( - dispatchChannelMessageAction({ + dispatchTestChannelMessageAction({ channel: "nextcloud-talk", action: "read", - cfg: {} as OpenClawConfig, params: { target: "room:current", }, @@ -915,10 +898,9 @@ describe("dispatchChannelMessageAction conversation-read provenance", () => { }); await expect( - dispatchChannelMessageAction({ + dispatchTestChannelMessageAction({ channel: "nextcloud-talk", action: "read", - cfg: {} as OpenClawConfig, params: { target: "group:current", to: "nextcloud-talk:current", @@ -949,10 +931,9 @@ describe("dispatchChannelMessageAction conversation-read provenance", () => { }); await expect( - dispatchChannelMessageAction({ + dispatchTestChannelMessageAction({ channel: "imessage", action: "read", - cfg: {} as OpenClawConfig, params: { target: "malformed-target", to: "chat_guid:iMessage;+;current", @@ -988,10 +969,9 @@ describe("dispatchChannelMessageAction conversation-read provenance", () => { }, }); - await dispatchChannelMessageAction({ + await dispatchTestChannelMessageAction({ channel: "imessage", action: "read", - cfg: {} as OpenClawConfig, params: { chatGuid: "iMessage;+;current" }, accountId: "default", requesterAccountId: "default", @@ -1021,10 +1001,9 @@ describe("dispatchChannelMessageAction conversation-read provenance", () => { }, }); - await dispatchChannelMessageAction({ + await dispatchTestChannelMessageAction({ channel: "whatsapp", action: "react", - cfg: {} as OpenClawConfig, params: { chatJid: "current@g.us", messageId: "current-message", @@ -1058,10 +1037,9 @@ describe("dispatchChannelMessageAction conversation-read provenance", () => { }); await expect( - dispatchChannelMessageAction({ + dispatchTestChannelMessageAction({ channel: "whatsapp", action: "react", - cfg: {} as OpenClawConfig, params: { chatJid: "sibling@g.us", messageId: "sibling-message", @@ -1103,10 +1081,9 @@ describe("dispatchChannelMessageAction conversation-read provenance", () => { }); await expect( - dispatchChannelMessageAction({ + dispatchTestChannelMessageAction({ channel: "whatsapp", action: "react", - cfg: {} as OpenClawConfig, params: { chatJid: "current@g.us", messageId: "current-message" }, accountId: testCase.accountId, requesterAccountId: testCase.requesterAccountId, @@ -1142,10 +1119,9 @@ describe("dispatchChannelMessageAction conversation-read provenance", () => { }, }); - await dispatchChannelMessageAction({ + await dispatchTestChannelMessageAction({ channel: "imessage", action: "react", - cfg: {} as OpenClawConfig, params: { chatId: 42, messageId: "current-message" }, accountId: "default", requesterAccountId: "default", @@ -1176,10 +1152,9 @@ describe("dispatchChannelMessageAction conversation-read provenance", () => { }, }); - await dispatchChannelMessageAction({ + await dispatchTestChannelMessageAction({ channel: "imessage", action: "react", - cfg: {} as OpenClawConfig, params: { chatId: 42, messageId: "current-message" }, accountId: "Work", requesterAccountId: "work", @@ -1219,10 +1194,9 @@ describe("dispatchChannelMessageAction conversation-read provenance", () => { }); const normalizedAliasTarget = "chat_id:42"; - await dispatchChannelMessageAction({ + await dispatchTestChannelMessageAction({ channel: "imessage", action: "react", - cfg: {} as OpenClawConfig, params: { target: normalizedAliasTarget, to: normalizedAliasTarget, @@ -1259,10 +1233,9 @@ describe("dispatchChannelMessageAction conversation-read provenance", () => { }); await expect( - dispatchChannelMessageAction({ + dispatchTestChannelMessageAction({ channel: "imessage", action: "react", - cfg: {} as OpenClawConfig, params: { chatId: 42, messageId: "current-message" }, accountId: "default", requesterAccountId: "default", @@ -1294,10 +1267,9 @@ describe("dispatchChannelMessageAction conversation-read provenance", () => { }); await expect( - dispatchChannelMessageAction({ + dispatchTestChannelMessageAction({ channel: "imessage", action: "react", - cfg: {} as OpenClawConfig, params: { chatId: 42, messageId: "current-message" }, accountId: "default", requesterAccountId: "default", @@ -1329,10 +1301,9 @@ describe("dispatchChannelMessageAction conversation-read provenance", () => { }); await expect( - dispatchChannelMessageAction({ + dispatchTestChannelMessageAction({ channel: "imessage", action: "react", - cfg: {} as OpenClawConfig, params: { target: "other-handle", chatId: 42, @@ -1368,10 +1339,9 @@ describe("dispatchChannelMessageAction conversation-read provenance", () => { }); await expect( - dispatchChannelMessageAction({ + dispatchTestChannelMessageAction({ channel: "imessage", action: "read", - cfg: {} as OpenClawConfig, params: { to: "chat_guid:iMessage;+;current", chatGuid: "iMessage;+;other", @@ -1407,10 +1377,9 @@ describe("dispatchChannelMessageAction conversation-read provenance", () => { }); await expect( - dispatchChannelMessageAction({ + dispatchTestChannelMessageAction({ channel: "imessage", action: testCase.action, - cfg: {} as OpenClawConfig, params: testCase.params, accountId: "work", requesterAccountId: "work", @@ -1437,10 +1406,9 @@ describe("dispatchChannelMessageAction conversation-read provenance", () => { }); await expect( - dispatchChannelMessageAction({ + dispatchTestChannelMessageAction({ channel: "imessage", action: "read", - cfg: {} as OpenClawConfig, params: { target: "chat_guid:iMessage;+;other", messageId: "current-message", @@ -1471,10 +1439,9 @@ describe("dispatchChannelMessageAction conversation-read provenance", () => { }); await expect( - dispatchChannelMessageAction({ + dispatchTestChannelMessageAction({ channel: "imessage", action: "read", - cfg: {} as OpenClawConfig, params: { messageId: "current-message" }, accountId: "default", requesterAccountId: "default", @@ -1492,10 +1459,9 @@ describe("dispatchChannelMessageAction conversation-read provenance", () => { it("allows bundled targetless sticker-cache reads only in matching current context", async () => { setReadPlugin({ channel: "telegram", origin: "bundled" }); - await dispatchChannelMessageAction({ + await dispatchTestChannelMessageAction({ channel: "telegram", action: "sticker-search", - cfg: {} as OpenClawConfig, params: { query: "party", limit: 5 }, accountId: "work", requesterAccountId: "work", @@ -1540,10 +1506,9 @@ describe("dispatchChannelMessageAction conversation-read provenance", () => { setReadPlugin({ channel: "telegram", origin: "bundled" }); await expect( - dispatchChannelMessageAction({ + dispatchTestChannelMessageAction({ channel: "telegram", action: "sticker-search", - cfg: {} as OpenClawConfig, params: { query: "party", limit: 5 }, accountId: testCase.accountId, requesterAccountId: testCase.requesterAccountId, @@ -1564,10 +1529,9 @@ describe("dispatchChannelMessageAction conversation-read provenance", () => { }); await expect( - dispatchChannelMessageAction({ + dispatchTestChannelMessageAction({ channel: "discord", action: "read", - cfg: {} as OpenClawConfig, params: { channelId: "configured" }, accountId: "default", requesterAccountId: "default", @@ -1587,10 +1551,9 @@ describe("dispatchChannelMessageAction conversation-read provenance", () => { setReadPlugin(origin ? { origin } : undefined); await expect( - dispatchChannelMessageAction({ + dispatchTestChannelMessageAction({ channel: "discord", action: "read", - cfg: {} as OpenClawConfig, params: { channelId: "configured" }, accountId: "default", requesterAccountId: "default", diff --git a/src/channels/progress-draft-compositor.test.ts b/src/channels/progress-draft-compositor.test.ts index 9916baedef9f..db69fad8e570 100644 --- a/src/channels/progress-draft-compositor.test.ts +++ b/src/channels/progress-draft-compositor.test.ts @@ -5,6 +5,20 @@ import { createChannelProgressReceiptTracker, PROGRESS_STATUS_PREAMBLE_FRESH_MS, } from "./progress-draft-compositor.js"; + +function createTestProgressDraftCompositor( + overrides: Omit< + Parameters[0], + "mode" | "active" | "seed" + >, +) { + return createChannelProgressDraftCompositor({ + mode: "progress", + active: true, + seed: "test", + ...overrides, + }); +} import { DEFAULT_PROGRESS_DRAFT_INITIAL_DELAY_MS } from "./streaming.js"; describe("createChannelProgressDraftCompositor", () => { @@ -28,11 +42,8 @@ describe("createChannelProgressDraftCompositor", () => { it("starts immediately for plans, replaces snapshots, and clears them on reset", async () => { const update = vi.fn(); - const progress = createChannelProgressDraftCompositor({ + const progress = createTestProgressDraftCompositor({ entry: { streaming: { mode: "progress", progress: { label: false } } }, - mode: "progress", - active: true, - seed: "test", update, }); @@ -78,11 +89,8 @@ describe("createChannelProgressDraftCompositor", () => { }); it("returns detached structured state for channel-native renderers", async () => { - const progress = createChannelProgressDraftCompositor({ + const progress = createTestProgressDraftCompositor({ entry: { streaming: { mode: "progress", progress: { label: false } } }, - mode: "progress", - active: true, - seed: "test", update: vi.fn(), }); @@ -135,13 +143,10 @@ describe("createChannelProgressDraftCompositor", () => { it("keeps the progress label visible when tool lines are hidden", async () => { const update = vi.fn(); - const progress = createChannelProgressDraftCompositor({ + const progress = createTestProgressDraftCompositor({ entry: { streaming: { mode: "progress", progress: { label: "Shelling", toolProgress: false } }, }, - mode: "progress", - active: true, - seed: "test", update, }); @@ -153,13 +158,10 @@ describe("createChannelProgressDraftCompositor", () => { it("gates window thinking on its own flag, independent of tool progress", async () => { // thinking: false hides thoughts even though toolProgress stays on… const hiddenUpdate = vi.fn(); - const hidden = createChannelProgressDraftCompositor({ + const hidden = createTestProgressDraftCompositor({ entry: { streaming: { mode: "progress", progress: { label: "Shelling" } }, }, - mode: "progress", - active: true, - seed: "test", reasoningGate: false, update: hiddenUpdate, }); @@ -168,16 +170,13 @@ describe("createChannelProgressDraftCompositor", () => { expect(hiddenUpdate.mock.calls.every(([text]) => !String(text).includes("Reading"))).toBe(true); const defaultUpdate = vi.fn(); - const sharedDefault = createChannelProgressDraftCompositor({ + const sharedDefault = createTestProgressDraftCompositor({ entry: { streaming: { mode: "progress", progress: { label: "Shelling", toolProgress: false }, }, }, - mode: "progress", - active: true, - seed: "test", update: defaultUpdate, }); await sharedDefault.pushToolProgress("🛠️ Exec", { startImmediately: true }); @@ -187,16 +186,13 @@ describe("createChannelProgressDraftCompositor", () => { ); const update = vi.fn(); - const progress = createChannelProgressDraftCompositor({ + const progress = createTestProgressDraftCompositor({ entry: { streaming: { mode: "progress", progress: { label: "Shelling", toolProgress: false }, }, }, - mode: "progress", - active: true, - seed: "test", reasoningLinePrefix: "🧠 ", reasoningGate: true, update, @@ -225,11 +221,8 @@ describe("createChannelProgressDraftCompositor", () => { it("re-arms the draft for a queued turn after the primary final settled", async () => { const update = vi.fn(); - const progress = createChannelProgressDraftCompositor({ + const progress = createTestProgressDraftCompositor({ entry: { streaming: { mode: "progress", progress: { label: "Shelling" } } }, - mode: "progress", - active: true, - seed: "test", update, }); @@ -249,11 +242,8 @@ describe("createChannelProgressDraftCompositor", () => { it("force-rearms an authoritative queued boundary without a prior final", async () => { const update = vi.fn(); - const progress = createChannelProgressDraftCompositor({ + const progress = createTestProgressDraftCompositor({ entry: { streaming: { mode: "progress", progress: { label: "Shelling" } } }, - mode: "progress", - active: true, - seed: "test", update, }); @@ -269,11 +259,8 @@ describe("createChannelProgressDraftCompositor", () => { vi.useFakeTimers(); try { const update = vi.fn(); - const progress = createChannelProgressDraftCompositor({ + const progress = createTestProgressDraftCompositor({ entry: { streaming: { mode: "progress", progress: { label: "Shelling" } } }, - mode: "progress", - active: true, - seed: "test", update, }); @@ -290,11 +277,8 @@ describe("createChannelProgressDraftCompositor", () => { it("does not resurrect progress after suppression", async () => { const update = vi.fn(); - const progress = createChannelProgressDraftCompositor({ + const progress = createTestProgressDraftCompositor({ entry: { streaming: { mode: "progress", progress: { label: "Shelling" } } }, - mode: "progress", - active: true, - seed: "test", update, }); @@ -306,11 +290,8 @@ describe("createChannelProgressDraftCompositor", () => { it("composes reasoning deltas with tool progress", async () => { const update = vi.fn(); - const progress = createChannelProgressDraftCompositor({ + const progress = createTestProgressDraftCompositor({ entry: { streaming: { mode: "progress", progress: { label: "Shelling" } } }, - mode: "progress", - active: true, - seed: "test", reasoningLinePrefix: "🧠 ", update, }); @@ -326,11 +307,8 @@ describe("createChannelProgressDraftCompositor", () => { it("labels window narration with a 💬 prefix", async () => { const update = vi.fn(); - const progress = createChannelProgressDraftCompositor({ + const progress = createTestProgressDraftCompositor({ entry: { streaming: { mode: "progress", progress: { label: "Shelling", commentary: true } } }, - mode: "progress", - active: true, - seed: "test", commentaryLinePrefix: "💬 ", update, }); @@ -489,13 +467,10 @@ describe("createChannelProgressDraftCompositor", () => { it("interleaves reasoning bursts with tool calls in arrival order", async () => { const update = vi.fn(); - const progress = createChannelProgressDraftCompositor({ + const progress = createTestProgressDraftCompositor({ entry: { streaming: { mode: "progress", progress: { label: "Shelling", maxLines: 8 } }, }, - mode: "progress", - active: true, - seed: "test", reasoningLinePrefix: "🧠 ", update, }); @@ -517,11 +492,8 @@ describe("createChannelProgressDraftCompositor", () => { it("preserves tagged reasoning content without leaking tags", async () => { const update = vi.fn(); - const progress = createChannelProgressDraftCompositor({ + const progress = createTestProgressDraftCompositor({ entry: { streaming: { mode: "progress", progress: { label: "Shelling" } } }, - mode: "progress", - active: true, - seed: "test", reasoningLinePrefix: "🧠 ", update, }); @@ -536,11 +508,8 @@ describe("createChannelProgressDraftCompositor", () => { it("waits for complete reasoning tags before showing tagged progress", async () => { const update = vi.fn(); - const progress = createChannelProgressDraftCompositor({ + const progress = createTestProgressDraftCompositor({ entry: { streaming: { mode: "progress", progress: { label: "Shelling" } } }, - mode: "progress", - active: true, - seed: "test", update, }); @@ -553,11 +522,8 @@ describe("createChannelProgressDraftCompositor", () => { it("preserves partial reasoning tag buffers across deltas", async () => { const update = vi.fn(); - const progress = createChannelProgressDraftCompositor({ + const progress = createTestProgressDraftCompositor({ entry: { streaming: { mode: "progress", progress: { label: "Shelling" } } }, - mode: "progress", - active: true, - seed: "test", reasoningLinePrefix: "🧠 ", update, }); @@ -573,11 +539,8 @@ describe("createChannelProgressDraftCompositor", () => { it("keeps literal reasoning tags inside code blocks", async () => { const update = vi.fn(); - const progress = createChannelProgressDraftCompositor({ + const progress = createTestProgressDraftCompositor({ entry: { streaming: { mode: "progress", progress: { label: "Shelling" } } }, - mode: "progress", - active: true, - seed: "test", reasoningLinePrefix: "🧠 ", update, }); @@ -595,11 +558,8 @@ describe("createChannelProgressDraftCompositor", () => { it("replaces repeated formatted reasoning snapshots", async () => { const update = vi.fn(); - const progress = createChannelProgressDraftCompositor({ + const progress = createTestProgressDraftCompositor({ entry: { streaming: { mode: "progress", progress: { label: "Shelling" } } }, - mode: "progress", - active: true, - seed: "test", reasoningLinePrefix: "🧠 ", update, }); @@ -652,13 +612,10 @@ describe("createChannelProgressDraftCompositor", () => { it("hands preambles to the commentary lane when it is enabled", async () => { const update = vi.fn(); - const progress = createChannelProgressDraftCompositor({ + const progress = createTestProgressDraftCompositor({ entry: { streaming: { mode: "progress", progress: { label: "Shelling", commentary: true } }, }, - mode: "progress", - active: true, - seed: "test", update, }); @@ -670,11 +627,8 @@ describe("createChannelProgressDraftCompositor", () => { it("holds a preamble headline until the gate starts and hides the implicit label", async () => { const update = vi.fn(); - const progress = createChannelProgressDraftCompositor({ + const progress = createTestProgressDraftCompositor({ entry: { streaming: { mode: "progress" } }, - mode: "progress", - active: true, - seed: "test", update, }); @@ -690,11 +644,8 @@ describe("createChannelProgressDraftCompositor", () => { it("publishes rolling tool-line changes beneath a stable preamble headline", async () => { const update = vi.fn(); - const progress = createChannelProgressDraftCompositor({ + const progress = createTestProgressDraftCompositor({ entry: { streaming: { mode: "progress", progress: { maxLines: 8 } } }, - mode: "progress", - active: true, - seed: "test", updateOnLineChange: true, update, }); @@ -711,11 +662,8 @@ describe("createChannelProgressDraftCompositor", () => { it("rejects control-only preambles without clobbering a valid headline", async () => { let nowMs = 0; const update = vi.fn(); - const progress = createChannelProgressDraftCompositor({ + const progress = createTestProgressDraftCompositor({ entry: { streaming: { mode: "progress" } }, - mode: "progress", - active: true, - seed: "test", now: () => nowMs, update, }); @@ -750,11 +698,8 @@ describe("createChannelProgressDraftCompositor", () => { it("retracts only the matching preamble headline", async () => { const update = vi.fn(); - const progress = createChannelProgressDraftCompositor({ + const progress = createTestProgressDraftCompositor({ entry: { streaming: { mode: "progress", progress: { label: "Shelling" } } }, - mode: "progress", - active: true, - seed: "test", update, }); @@ -775,11 +720,8 @@ describe("createChannelProgressDraftCompositor", () => { it("keeps a fresh preamble ahead of later narration", async () => { let nowMs = 0; const update = vi.fn(); - const progress = createChannelProgressDraftCompositor({ + const progress = createTestProgressDraftCompositor({ entry: { streaming: { mode: "progress", progress: { label: "Shelling" } } }, - mode: "progress", - active: true, - seed: "test", now: () => nowMs, update, }); @@ -798,11 +740,8 @@ describe("createChannelProgressDraftCompositor", () => { it("uses newer narration after the preamble becomes stale", async () => { let nowMs = 0; const update = vi.fn(); - const progress = createChannelProgressDraftCompositor({ + const progress = createTestProgressDraftCompositor({ entry: { streaming: { mode: "progress", progress: { label: "Shelling" } } }, - mode: "progress", - active: true, - seed: "test", now: () => nowMs, update, }); @@ -821,11 +760,8 @@ describe("createChannelProgressDraftCompositor", () => { it("uses a plan explanation after the preamble becomes stale", async () => { let nowMs = 0; const update = vi.fn(); - const progress = createChannelProgressDraftCompositor({ + const progress = createTestProgressDraftCompositor({ entry: { streaming: { mode: "progress", progress: { label: "Shelling" } } }, - mode: "progress", - active: true, - seed: "test", now: () => nowMs, update, }); @@ -846,11 +782,8 @@ describe("createChannelProgressDraftCompositor", () => { it("refreshes a new preamble item when its text matches the stale item", async () => { let nowMs = 0; const update = vi.fn(); - const progress = createChannelProgressDraftCompositor({ + const progress = createTestProgressDraftCompositor({ entry: { streaming: { mode: "progress", progress: { label: "Shelling" } } }, - mode: "progress", - active: true, - seed: "test", now: () => nowMs, update, }); @@ -871,11 +804,8 @@ describe("createChannelProgressDraftCompositor", () => { vi.useFakeTimers(); try { const update = vi.fn(); - const progress = createChannelProgressDraftCompositor({ + const progress = createTestProgressDraftCompositor({ entry: { streaming: { mode: "progress", progress: { label: "Shelling" } } }, - mode: "progress", - active: true, - seed: "test", update, }); @@ -901,11 +831,8 @@ describe("createChannelProgressDraftCompositor", () => { it("cancels a pending preamble-expiry refresh when the final starts", async () => { vi.useFakeTimers(); try { - const progress = createChannelProgressDraftCompositor({ + const progress = createTestProgressDraftCompositor({ entry: { streaming: { mode: "progress" } }, - mode: "progress", - active: true, - seed: "test", update: vi.fn(), }); @@ -924,11 +851,8 @@ describe("createChannelProgressDraftCompositor", () => { it("returns to the retained preamble when narration clears", async () => { let nowMs = 0; const update = vi.fn(); - const progress = createChannelProgressDraftCompositor({ + const progress = createTestProgressDraftCompositor({ entry: { streaming: { mode: "progress", progress: { label: "Shelling" } } }, - mode: "progress", - active: true, - seed: "test", now: () => nowMs, update, }); @@ -948,11 +872,8 @@ describe("createChannelProgressDraftCompositor", () => { it("clears both status sources on reset", async () => { let nowMs = 0; const update = vi.fn(); - const progress = createChannelProgressDraftCompositor({ + const progress = createTestProgressDraftCompositor({ entry: { streaming: { mode: "progress", progress: { label: "Shelling" } } }, - mode: "progress", - active: true, - seed: "test", now: () => nowMs, update, }); @@ -971,11 +892,8 @@ describe("createChannelProgressDraftCompositor", () => { vi.useFakeTimers(); try { const update = vi.fn(); - const progress = createChannelProgressDraftCompositor({ + const progress = createTestProgressDraftCompositor({ entry: { streaming: { mode: "progress" } }, - mode: "progress", - active: true, - seed: "test", update, }); @@ -1049,11 +967,8 @@ describe("createChannelProgressDraftCompositor", () => { it("ignores status updates once the final reply started and clears both per turn", async () => { const update = vi.fn(); - const progress = createChannelProgressDraftCompositor({ + const progress = createTestProgressDraftCompositor({ entry: { streaming: { mode: "progress", progress: { label: "Shelling" } } }, - mode: "progress", - active: true, - seed: "test", update, }); @@ -1079,11 +994,8 @@ describe("createChannelProgressDraftCompositor", () => { try { const error = new Error("send failed"); const update = vi.fn().mockRejectedValue(error); - const progress = createChannelProgressDraftCompositor({ + const progress = createTestProgressDraftCompositor({ entry: { streaming: { mode: "progress", progress: { label: "Shelling" } } }, - mode: "progress", - active: true, - seed: "test", update, }); diff --git a/src/channels/turn/kernel.test.ts b/src/channels/turn/kernel.test.ts index f09da39ce2e8..010f63af7736 100644 --- a/src/channels/turn/kernel.test.ts +++ b/src/channels/turn/kernel.test.ts @@ -141,6 +141,20 @@ function createDispatch( }) as DispatchReplyWithBufferedBlockDispatcher; } +function dispatchTestAssembledTurn( + overrides: Omit< + Parameters[0], + "cfg" | "agentId" | "storePath" + >, +) { + return dispatchAssembledChannelTurn({ + cfg, + agentId: "main", + storePath: "/tmp/sessions.json", + ...overrides, + }); +} + function requireFirstMockCall(mock: { mock: { calls: T[][] } }, label: string): T[] { const call = mock.mock.calls[0]; if (!call) { @@ -612,13 +626,10 @@ describe("channel turn kernel", () => { const recordInboundSession = createRecordInboundSession(); const dispatchReplyWithBufferedBlockDispatcher = createDispatch(); - const result = await dispatchAssembledChannelTurn({ - cfg, + const result = await dispatchTestAssembledTurn({ channel: "telegram", accountId: "acct", - agentId: "main", routeSessionKey: "agent:main:telegram:peer", - storePath: "/tmp/sessions.json", ctxPayload: createCtx({ To: "123", OriginatingTo: "123", @@ -677,13 +688,10 @@ describe("channel turn kernel", () => { }, ) as DispatchReplyWithBufferedBlockDispatcher; - await dispatchAssembledChannelTurn({ - cfg, + await dispatchTestAssembledTurn({ channel: "telegram", accountId: "acct", - agentId: "main", routeSessionKey: "agent:main:telegram:peer", - storePath: "/tmp/sessions.json", ctxPayload: createCtx({ To: "123", OriginatingTo: "123" }), recordInboundSession: createRecordInboundSession(), dispatchReplyWithBufferedBlockDispatcher, @@ -749,13 +757,10 @@ describe("channel turn kernel", () => { const onDelivered = vi.fn(); const dispatchReplyWithBufferedBlockDispatcher = createDispatch(); - await dispatchAssembledChannelTurn({ - cfg, + await dispatchTestAssembledTurn({ channel: "tlon", accountId: "acct", - agentId: "main", routeSessionKey: "agent:main:tlon:peer", - storePath: "/tmp/sessions.json", ctxPayload: createCtx({ To: "chat/~nec/general", OriginatingTo: "chat/~nec/general" }), recordInboundSession: createRecordInboundSession(), dispatchReplyWithBufferedBlockDispatcher, @@ -810,13 +815,10 @@ describe("channel turn kernel", () => { }, ) as DispatchReplyWithBufferedBlockDispatcher; - await dispatchAssembledChannelTurn({ - cfg, + await dispatchTestAssembledTurn({ channel: "telegram", accountId: "acct", - agentId: "main", routeSessionKey: "agent:main:telegram:peer", - storePath: "/tmp/sessions.json", ctxPayload: createCtx({ To: "123", OriginatingTo: "123" }), recordInboundSession: createRecordInboundSession(), dispatchReplyWithBufferedBlockDispatcher, @@ -843,13 +845,10 @@ describe("channel turn kernel", () => { const dispatchReplyWithBufferedBlockDispatcher = createDispatch(); await expect( - dispatchAssembledChannelTurn({ - cfg, + dispatchTestAssembledTurn({ channel: "telegram", accountId: "acct", - agentId: "main", routeSessionKey: "agent:main:telegram:peer", - storePath: "/tmp/sessions.json", ctxPayload: createCtx({ To: "123", OriginatingTo: "123" }), recordInboundSession: createRecordInboundSession(), dispatchReplyWithBufferedBlockDispatcher, @@ -879,13 +878,10 @@ describe("channel turn kernel", () => { const dispatchReplyWithBufferedBlockDispatcher = createDispatch(); await expect( - dispatchAssembledChannelTurn({ - cfg, + dispatchTestAssembledTurn({ channel: "telegram", accountId: "acct", - agentId: "main", routeSessionKey: "agent:main:telegram:peer", - storePath: "/tmp/sessions.json", ctxPayload: createCtx({ To: "123", OriginatingTo: "123" }), recordInboundSession: createRecordInboundSession(), dispatchReplyWithBufferedBlockDispatcher, @@ -905,13 +901,10 @@ describe("channel turn kernel", () => { const dispatchReplyWithBufferedBlockDispatcher = createDispatch(); await expect( - dispatchAssembledChannelTurn({ - cfg, + dispatchTestAssembledTurn({ channel: "telegram", accountId: "acct", - agentId: "main", routeSessionKey: "agent:main:telegram:peer", - storePath: "/tmp/sessions.json", ctxPayload: createCtx({ To: "123", OriginatingTo: "123" }), recordInboundSession: createRecordInboundSession(), dispatchReplyWithBufferedBlockDispatcher, @@ -948,12 +941,9 @@ describe("channel turn kernel", () => { }, ) as DispatchReplyWithBufferedBlockDispatcher; - await dispatchAssembledChannelTurn({ - cfg, + await dispatchTestAssembledTurn({ channel: "test", - agentId: "main", routeSessionKey: "agent:main:test:peer", - storePath: "/tmp/sessions.json", ctxPayload: createCtx(), recordInboundSession: createRecordInboundSession(), dispatchReplyWithBufferedBlockDispatcher, @@ -1002,13 +992,10 @@ describe("channel turn kernel", () => { return { queuedFinal: true, counts: { tool: 0, block: 0, final: 1 } }; }) as DispatchReplyWithBufferedBlockDispatcher; - await dispatchAssembledChannelTurn({ - cfg, + await dispatchTestAssembledTurn({ channel: "feishu", accountId: "acct", - agentId: "main", routeSessionKey: "agent:main:feishu:peer", - storePath: "/tmp/sessions.json", ctxPayload: createCtx({ Surface: "feishu", Provider: "feishu", OriginatingTo: "oc_chat" }), recordInboundSession: createRecordInboundSession(), dispatchReplyWithBufferedBlockDispatcher, @@ -1046,12 +1033,9 @@ describe("channel turn kernel", () => { return { queuedFinal: true, counts: { tool: 0, block: 0, final: 1 } }; }) as DispatchReplyWithBufferedBlockDispatcher; - await dispatchAssembledChannelTurn({ - cfg, + await dispatchTestAssembledTurn({ channel: "feishu", - agentId: "main", routeSessionKey: "agent:main:feishu:peer", - storePath: "/tmp/sessions.json", ctxPayload: createCtx({ Surface: "feishu", Provider: "feishu" }), recordInboundSession: createRecordInboundSession(), dispatchReplyWithBufferedBlockDispatcher, @@ -1075,12 +1059,9 @@ describe("channel turn kernel", () => { const onError = vi.fn(); await expect( - dispatchAssembledChannelTurn({ - cfg, + dispatchTestAssembledTurn({ channel: "feishu", - agentId: "main", routeSessionKey: "agent:main:feishu:peer", - storePath: "/tmp/sessions.json", ctxPayload: createCtx({ Surface: "feishu", Provider: "feishu" }), recordInboundSession: createRecordInboundSession(), dispatchReplyWithBufferedBlockDispatcher: createDispatch(), @@ -1137,12 +1118,9 @@ describe("channel turn kernel", () => { }) as DispatchReplyWithBufferedBlockDispatcher; await expect( - dispatchAssembledChannelTurn({ - cfg, + dispatchTestAssembledTurn({ channel: "feishu", - agentId: "main", routeSessionKey: "agent:main:feishu:peer", - storePath: "/tmp/sessions.json", ctxPayload: createCtx({ Surface: "feishu", Provider: "feishu" }), recordInboundSession: createRecordInboundSession(), dispatchReplyWithBufferedBlockDispatcher, @@ -1190,12 +1168,9 @@ describe("channel turn kernel", () => { }) as DispatchReplyWithBufferedBlockDispatcher; await expect( - dispatchAssembledChannelTurn({ - cfg, + dispatchTestAssembledTurn({ channel: "feishu", - agentId: "main", routeSessionKey: "agent:main:feishu:peer", - storePath: "/tmp/sessions.json", ctxPayload: createCtx({ Surface: "feishu", Provider: "feishu" }), recordInboundSession: createRecordInboundSession(), dispatchReplyWithBufferedBlockDispatcher, @@ -1246,12 +1221,9 @@ describe("channel turn kernel", () => { }) as DispatchReplyWithBufferedBlockDispatcher; await expect( - dispatchAssembledChannelTurn({ - cfg, + dispatchTestAssembledTurn({ channel: "feishu", - agentId: "main", routeSessionKey: "agent:main:feishu:peer", - storePath: "/tmp/sessions.json", ctxPayload: createCtx({ Surface: "feishu", Provider: "feishu" }), recordInboundSession: createRecordInboundSession(), dispatchReplyWithBufferedBlockDispatcher, @@ -1284,12 +1256,9 @@ describe("channel turn kernel", () => { return { queuedFinal: false, counts: { tool: 0, block: 0, final: 0 } }; }) as DispatchReplyWithBufferedBlockDispatcher; - await dispatchAssembledChannelTurn({ - cfg, + await dispatchTestAssembledTurn({ channel: "feishu", - agentId: "main", routeSessionKey: "agent:main:feishu:peer", - storePath: "/tmp/sessions.json", ctxPayload: createCtx({ Surface: "feishu", Provider: "feishu" }), recordInboundSession: createRecordInboundSession(), dispatchReplyWithBufferedBlockDispatcher, @@ -1310,13 +1279,10 @@ describe("channel turn kernel", () => { const deliver = vi.fn(async () => ({ messageIds: ["local-1"], visibleReplySent: true })); const dispatchReplyWithBufferedBlockDispatcher = createDispatch(); - await dispatchAssembledChannelTurn({ - cfg, + await dispatchTestAssembledTurn({ channel: "telegram", accountId: "acct", - agentId: "main", routeSessionKey: "agent:main:telegram:peer", - storePath: "/tmp/sessions.json", ctxPayload: createCtx({ To: "123", OriginatingTo: "123" }), recordInboundSession: createRecordInboundSession(), dispatchReplyWithBufferedBlockDispatcher, @@ -1332,12 +1298,9 @@ describe("channel turn kernel", () => { const deliver = vi.fn(async () => ({ messageIds: ["local-1"], visibleReplySent: true })); const dispatchReplyWithBufferedBlockDispatcher = createDispatch(); - await dispatchAssembledChannelTurn({ - cfg, + await dispatchTestAssembledTurn({ channel: "test", - agentId: "main", routeSessionKey: "agent:main:test:peer", - storePath: "/tmp/sessions.json", ctxPayload: createCtx(), recordInboundSession: createRecordInboundSession(), dispatchReplyWithBufferedBlockDispatcher, @@ -1377,12 +1340,9 @@ describe("channel turn kernel", () => { }, ) as DispatchReplyWithBufferedBlockDispatcher; - await dispatchAssembledChannelTurn({ - cfg, + await dispatchTestAssembledTurn({ channel: "test", - agentId: "main", routeSessionKey: "agent:main:test:peer", - storePath: "/tmp/sessions.json", ctxPayload: createCtx(), recordInboundSession: createRecordInboundSession(), dispatchReplyWithBufferedBlockDispatcher, @@ -1402,12 +1362,9 @@ describe("channel turn kernel", () => { const recordInboundSession = createRecordInboundSession(events); const dispatchReplyWithBufferedBlockDispatcher = createDispatch(events); - const result = await dispatchAssembledChannelTurn({ - cfg, + const result = await dispatchTestAssembledTurn({ channel: "test", - agentId: "main", routeSessionKey: "agent:main:test:peer", - storePath: "/tmp/sessions.json", ctxPayload: createCtx(), recordInboundSession, dispatchReplyWithBufferedBlockDispatcher, @@ -1435,12 +1392,9 @@ describe("channel turn kernel", () => { const commandSessionKey = "agent:main:command:telegram:42"; const targetSessionKey = "agent:main:telegram:group:42:topic:7"; - const result = await dispatchAssembledChannelTurn({ - cfg, + const result = await dispatchTestAssembledTurn({ channel: "telegram", - agentId: "main", routeSessionKey: commandSessionKey, - storePath: "/tmp/sessions.json", ctxPayload: createCtx({ AgentId: "main", SessionKey: commandSessionKey, @@ -1486,12 +1440,9 @@ describe("channel turn kernel", () => { const dispatchReplyWithBufferedBlockDispatcher = createDispatch(); await expect( - dispatchAssembledChannelTurn({ - cfg, + dispatchTestAssembledTurn({ channel: "telegram", - agentId: "main", routeSessionKey: "agent:main:command:telegram:42", - storePath: "/tmp/sessions.json", ctxPayload: createCtx(), recordInboundSession, dispatchReplyWithBufferedBlockDispatcher, @@ -1505,12 +1456,9 @@ describe("channel turn kernel", () => { it("rejects surrounding whitespace in an explicit record session", async () => { await expect( - dispatchAssembledChannelTurn({ - cfg, + dispatchTestAssembledTurn({ channel: "telegram", - agentId: "main", routeSessionKey: "agent:main:command:telegram:42", - storePath: "/tmp/sessions.json", ctxPayload: createCtx(), recordInboundSession: createRecordInboundSession(), dispatchReplyWithBufferedBlockDispatcher: createDispatch(), @@ -1865,13 +1813,10 @@ describe("channel turn kernel", () => { }); // Unbinding removes Discord's thread route, not the core-owned outbound identity. - const result = await dispatchAssembledChannelTurn({ - cfg, + const result = await dispatchTestAssembledTurn({ channel: "discord", accountId: "default", - agentId: "main", routeSessionKey: "agent:main:discord:channel:thread-1", - storePath: "/tmp/sessions.json", ctxPayload: createCtx({ Provider: "discord", Surface: "discord", @@ -2086,12 +2031,9 @@ describe("channel turn kernel", () => { }, ) as DispatchReplyWithBufferedBlockDispatcher; - await dispatchAssembledChannelTurn({ - cfg, + await dispatchTestAssembledTurn({ channel: "test", - agentId: "main", routeSessionKey: "agent:main:test:peer", - storePath: "/tmp/sessions.json", ctxPayload: createCtx(), recordInboundSession: createRecordInboundSession(events), dispatchReplyWithBufferedBlockDispatcher,