From 10062e8111ea35207c6c71c9c01d2cca8dbd9125 Mon Sep 17 00:00:00 2001 From: Tak Hoffman <781889+Takhoffman@users.noreply.github.com> Date: Fri, 3 Apr 2026 16:31:13 -0500 Subject: [PATCH] fix: honor plugin binding default account --- .../reply/dispatch-from-config.test.ts | 65 +++++++++++++++++++ src/auto-reply/reply/dispatch-from-config.ts | 7 +- 2 files changed, 71 insertions(+), 1 deletion(-) diff --git a/src/auto-reply/reply/dispatch-from-config.test.ts b/src/auto-reply/reply/dispatch-from-config.test.ts index fb10f9589c1..4f8cf1ce4d7 100644 --- a/src/auto-reply/reply/dispatch-from-config.test.ts +++ b/src/auto-reply/reply/dispatch-from-config.test.ts @@ -1400,6 +1400,71 @@ describe("dispatchReplyFromConfig", () => { expect(noticePayload?.text).toContain("acpx session id: acpx-123"); }); + it("honors the configured default account when resolving plugin-owned binding fallbacks", async () => { + setNoAbort(); + sessionBindingMocks.resolveByConversation.mockImplementation( + (ref: { + channel: string; + accountId: string; + conversationId: string; + parentConversationId?: string; + }) => + ref.channel === "discord" && ref.accountId === "work" && ref.conversationId === "thread-1" + ? ({ + bindingId: "plugin:work:thread-1", + targetSessionKey: "plugin-binding:missing-plugin", + targetKind: "session", + conversation: { + channel: "discord", + accountId: "work", + conversationId: "thread-1", + }, + status: "active", + boundAt: Date.now(), + metadata: { + pluginBindingOwner: "plugin", + pluginId: "missing-plugin", + pluginRoot: "/plugins/missing-plugin", + pluginName: "Missing Plugin", + }, + } satisfies SessionBindingRecord) + : null, + ); + + const cfg = { + channels: { + discord: { + defaultAccount: "work", + }, + }, + } as OpenClawConfig; + const dispatcher = createDispatcher(); + const replyResolver = vi.fn(async () => undefined); + const ctx = buildTestCtx({ + Provider: "discord", + Surface: "discord", + To: "discord:thread-1", + SessionKey: "main", + BodyForAgent: "fallback", + }); + + await dispatchReplyFromConfig({ ctx, cfg, dispatcher, replyResolver }); + + expect(sessionBindingMocks.resolveByConversation).toHaveBeenCalledWith( + expect.objectContaining({ + channel: "discord", + accountId: "work", + conversationId: "thread-1", + }), + ); + expect(dispatcher.sendToolResult).toHaveBeenCalledWith( + expect.objectContaining({ + text: expect.stringContaining("not currently loaded"), + }), + ); + expect(replyResolver).toHaveBeenCalled(); + }); + it("honors send-policy deny before ACP runtime dispatch", async () => { setNoAbort(); const runtime = createAcpRuntime([ diff --git a/src/auto-reply/reply/dispatch-from-config.ts b/src/auto-reply/reply/dispatch-from-config.ts index f92a2f2e63c..e4b58286ff9 100644 --- a/src/auto-reply/reply/dispatch-from-config.ts +++ b/src/auto-reply/reply/dispatch-from-config.ts @@ -342,7 +342,12 @@ export async function dispatchReplyFromConfig(params: { inboundClaimContext.conversationId && inboundClaimContext.channelId ? resolveConversationBindingRecord({ channel: inboundClaimContext.channelId, - accountId: inboundClaimContext.accountId ?? "default", + accountId: + inboundClaimContext.accountId ?? + ((cfg.channels as Record | undefined)?.[ + inboundClaimContext.channelId + ]?.defaultAccount as string | undefined) ?? + "default", conversationId: inboundClaimContext.conversationId, parentConversationId: inboundClaimContext.parentConversationId, })