From 2e3ef1b9e180bff08a77845fe936b3bdddfd2b83 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 17 Apr 2026 07:50:19 +0100 Subject: [PATCH] fix: pass message routing context to send actions --- src/commands/message.default-agent.test.ts | 171 ------------------ src/commands/message.test.ts | 62 ++++++- .../local/auth-choice.test.ts | 5 +- src/infra/outbound/message-action-runner.ts | 9 +- src/infra/outbound/outbound-send-service.ts | 7 + 5 files changed, 76 insertions(+), 178 deletions(-) delete mode 100644 src/commands/message.default-agent.test.ts diff --git a/src/commands/message.default-agent.test.ts b/src/commands/message.default-agent.test.ts deleted file mode 100644 index 11bd4b78278..00000000000 --- a/src/commands/message.default-agent.test.ts +++ /dev/null @@ -1,171 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; -import type { CliDeps } from "../cli/outbound-send-deps.js"; -import type { RuntimeEnv } from "../runtime.js"; -import { messageCommand } from "./message.js"; - -let testConfig: Record = {}; -const resolveCommandConfigWithSecrets = vi.hoisted(() => - vi.fn(async ({ config }: { config: unknown }) => ({ - resolvedConfig: config, - effectiveConfig: config, - diagnostics: [] as string[], - })), -); -const runMessageAction = vi.hoisted(() => - vi.fn(async () => ({ - kind: "send" as const, - channel: "telegram" as const, - action: "send" as const, - to: "123456", - handledBy: "core" as const, - payload: { ok: true }, - dryRun: false, - })), -); - -vi.mock("../config/config.js", () => ({ - loadConfig: () => testConfig, -})); - -vi.mock("../cli/command-config-resolution.js", () => ({ - resolveCommandConfigWithSecrets, -})); - -vi.mock("../infra/outbound/message-action-runner.js", () => ({ - runMessageAction, -})); - -describe("messageCommand agent routing", () => { - beforeEach(() => { - testConfig = {}; - resolveCommandConfigWithSecrets.mockClear(); - runMessageAction.mockClear(); - }); - - it("passes resolved command config and scoped secret targets to the outbound runner", async () => { - const rawConfig = { - channels: { - telegram: { - token: { $secret: "vault://telegram/token" }, - }, - }, - }; - const resolvedConfig = { - channels: { - telegram: { - token: "12345:resolved-token", - }, - }, - }; - testConfig = rawConfig; - resolveCommandConfigWithSecrets.mockResolvedValueOnce({ - resolvedConfig, - effectiveConfig: resolvedConfig, - diagnostics: [], - }); - - const runtime: RuntimeEnv = { - log: vi.fn(), - error: vi.fn(), - exit: vi.fn(), - }; - await messageCommand( - { - action: "send", - channel: "telegram", - target: "123456", - message: "hi", - json: true, - }, - {} as CliDeps, - runtime, - ); - - expect(resolveCommandConfigWithSecrets).toHaveBeenCalledWith( - expect.objectContaining({ - config: rawConfig, - commandName: "message", - }), - ); - const call = resolveCommandConfigWithSecrets.mock.calls[0]?.[0] as { - targetIds?: Set; - }; - expect(call.targetIds).toBeInstanceOf(Set); - expect([...(call.targetIds ?? [])].every((id) => id.startsWith("channels.telegram."))).toBe( - true, - ); - expect(runMessageAction).toHaveBeenCalledWith( - expect.objectContaining({ - cfg: resolvedConfig, - }), - ); - }); - - it("passes the resolved default agent id to the outbound runner", async () => { - testConfig = { - agents: { - list: [{ id: "alpha" }, { id: "ops", default: true }], - }, - }; - - const runtime: RuntimeEnv = { - log: vi.fn(), - error: vi.fn(), - exit: vi.fn(), - }; - await messageCommand( - { - action: "send", - channel: "telegram", - target: "123456", - message: "hi", - json: true, - }, - {} as CliDeps, - runtime, - ); - - expect(runMessageAction).toHaveBeenCalledWith( - expect.objectContaining({ - agentId: "ops", - }), - ); - }); - - it.each([ - { - name: "defaults senderIsOwner to true for local message runs", - opts: {}, - expected: true, - }, - { - name: "honors explicit senderIsOwner override", - opts: { senderIsOwner: false }, - expected: false, - }, - ])("$name", async ({ opts, expected }) => { - const runtime: RuntimeEnv = { - log: vi.fn(), - error: vi.fn(), - exit: vi.fn(), - }; - await messageCommand( - { - action: "send", - channel: "telegram", - target: "123456", - message: "hi", - json: true, - ...opts, - }, - {} as CliDeps, - runtime, - ); - - expect(runMessageAction).toHaveBeenCalledWith( - expect.objectContaining({ - senderIsOwner: expected, - }), - ); - }); -}); diff --git a/src/commands/message.test.ts b/src/commands/message.test.ts index 27561583324..3e18edabb15 100644 --- a/src/commands/message.test.ts +++ b/src/commands/message.test.ts @@ -167,9 +167,22 @@ const createTelegramSendPluginRegistration = () => ({ label: "Telegram", actions: { describeMessageTool: () => ({ actions: ["send"] }), - handleAction: (async ({ action, params, cfg, accountId }: ChannelActionParams) => { + handleAction: (async ({ + action, + params, + cfg, + accountId, + agentId, + senderIsOwner, + }: ChannelActionParams) => { return await handleTelegramAction( - { action, to: params.to, accountId: accountId ?? undefined }, + { + action, + to: params.to, + accountId: accountId ?? undefined, + agentId, + senderIsOwner, + }, cfg, ); }) as unknown as NonNullable["handleAction"], @@ -185,9 +198,22 @@ const createTelegramPollPluginRegistration = () => ({ label: "Telegram", actions: { describeMessageTool: () => ({ actions: ["poll"] }), - handleAction: (async ({ action, params, cfg, accountId }: ChannelActionParams) => { + handleAction: (async ({ + action, + params, + cfg, + accountId, + agentId, + senderIsOwner, + }: ChannelActionParams) => { return await handleTelegramAction( - { action, to: params.to, accountId: accountId ?? undefined }, + { + action, + to: params.to, + accountId: accountId ?? undefined, + agentId, + senderIsOwner, + }, cfg, ); }) as unknown as NonNullable["handleAction"], @@ -294,6 +320,19 @@ describe("messageCommand", () => { }), ); expect(sendText.mock.calls[0]?.[0]?.cfg).not.toBe(rawConfig); + expect(resolveCommandConfigWithSecrets).toHaveBeenCalledWith( + expect.objectContaining({ + config: rawConfig, + commandName: "message", + }), + ); + const call = resolveCommandConfigWithSecrets.mock.calls[0]?.[0] as { + targetIds?: Set; + }; + expect(call.targetIds).toBeInstanceOf(Set); + expect([...(call.targetIds ?? [])].every((id) => id.startsWith("channels.telegram."))).toBe( + true, + ); }); it("keeps local-fallback resolved cfg in outbound adapter sends", async () => { @@ -330,6 +369,11 @@ describe("messageCommand", () => { it("defaults channel when only one configured", async () => { process.env.TELEGRAM_BOT_TOKEN = "token-abc"; + testConfig = { + agents: { + list: [{ id: "alpha" }, { id: "ops", default: true }], + }, + }; setActivePluginRegistry( createTestRegistry([ { @@ -346,7 +390,13 @@ describe("messageCommand", () => { deps, runtime, ); - expect(handleTelegramAction).toHaveBeenCalled(); + expect(handleTelegramAction).toHaveBeenCalledWith( + expect.objectContaining({ + agentId: "ops", + senderIsOwner: true, + }), + expect.any(Object), + ); }); it("defaults channel from the auto-enabled config snapshot when only one channel becomes configured", async () => { @@ -500,6 +550,7 @@ describe("messageCommand", () => { pollQuestion: "Ship it?", pollOption: ["Yes", "No"], pollDurationSeconds: 120, + senderIsOwner: false, }, deps, runtime, @@ -508,6 +559,7 @@ describe("messageCommand", () => { expect.objectContaining({ action: "poll", to: "123456789", + senderIsOwner: false, }), expect.any(Object), ); diff --git a/src/commands/onboard-non-interactive/local/auth-choice.test.ts b/src/commands/onboard-non-interactive/local/auth-choice.test.ts index 659cfae36ec..ddf96f2b5fb 100644 --- a/src/commands/onboard-non-interactive/local/auth-choice.test.ts +++ b/src/commands/onboard-non-interactive/local/auth-choice.test.ts @@ -1,5 +1,6 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../../../config/config.js"; +import { resolveAgentModelPrimaryValue } from "../../../config/model-input.js"; import { applyNonInteractiveAuthChoice } from "./auth-choice.js"; const applyNonInteractivePluginProviderChoice = vi.hoisted(() => vi.fn(async () => undefined)); @@ -99,7 +100,9 @@ describe("applyNonInteractiveAuthChoice", () => { provider: "default", id: "CUSTOM_API_KEY", }); - expect(result?.agents?.defaults?.model?.primary).toBe("custom-models-custom-local/local-large"); + expect(resolveAgentModelPrimaryValue(result?.agents?.defaults?.model)).toBe( + "custom-models-custom-local/local-large", + ); expect(resolveNonInteractiveApiKey).toHaveBeenCalledWith( expect.objectContaining({ provider: "custom-models-custom-local", diff --git a/src/infra/outbound/message-action-runner.ts b/src/infra/outbound/message-action-runner.ts index 5f7737a0b5b..0e613bed9dc 100644 --- a/src/infra/outbound/message-action-runner.ts +++ b/src/infra/outbound/message-action-runner.ts @@ -598,6 +598,8 @@ async function handleSendAction(ctx: ResolvedActionContext): Promise { - const { cfg, params, channel, accountId, dryRun, gateway, input, abortSignal } = ctx; + const { cfg, params, channel, accountId, dryRun, gateway, input, agentId, abortSignal } = ctx; throwIfAborted(abortSignal); const action: ChannelMessageActionName = "poll"; const to = readStringParam(params, "to", { required: true }); @@ -672,6 +674,11 @@ async function handlePollAction(ctx: ResolvedActionContext): Promise