Download src/agents/command/delivery.test.ts from SaylorTwift/openclaw: direct link, hf CLI and curl.
- Browser
- Download file 59.3 kB
-
https://huggingface.co/SaylorTwift/openclaw/resolve/main/src/agents/command/delivery.test.ts
- Command line
-
hf download hf://SaylorTwift/openclaw/src/agents/command/delivery.test.ts
-
curl -L -o delivery.test.ts https://huggingface.co/SaylorTwift/openclaw/resolve/main/src/agents/command/delivery.test.ts
59.3 kB
| // Covers agent-command reply normalization and outbound delivery status. | |
| import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; | |
| import type { ReplyPayload } from "../../auto-reply/reply-payload.js"; | |
| import type { | |
| ChannelOutboundAdapter, | |
| ChannelThreadingAdapter, | |
| } from "../../channels/plugins/types.public.js"; | |
| import type { CliDeps } from "../../cli/outbound-send-deps.js"; | |
| import type { OpenClawConfig } from "../../config/config.js"; | |
| import { setActivePluginRegistry } from "../../plugins/runtime.js"; | |
| import { createOutboundTestPlugin, createTestRegistry } from "../../test-utils/channel-plugins.js"; | |
| import { normalizeSessionDeliveryState } from "../../utils/delivery-context.shared.js"; | |
| import { buildRestartRecoveryTerminalDeliveryEvidence } from "../agent-command-restart-recovery.js"; | |
| import { hasVisibleAgentPayload } from "../embedded-agent-runner/message-visibility.js"; | |
| import { createAgentRunRestartAbortError } from "../run-termination.js"; | |
| import { deliverAgentCommandResult } from "./delivery.js"; | |
| import type { AgentCommandOpts } from "./types.js"; | |
| const deliverOutboundPayloadsMock = vi.hoisted(() => | |
| vi.fn(async (..._args: unknown[]) => [] as unknown[]), | |
| ); | |
| vi.mock("../../infra/outbound/deliver.js", () => ({ | |
| deliverOutboundPayloads: deliverOutboundPayloadsMock, | |
| deliverOutboundPayloadsInternal: deliverOutboundPayloadsMock, | |
| })); | |
| const createReplyMediaPathNormalizerMock = vi.hoisted(() => | |
| vi.fn( | |
| (..._args: unknown[]) => | |
| (payload: ReplyPayload) => | |
| Promise.resolve(payload), | |
| ), | |
| ); | |
| vi.mock("../../auto-reply/reply/reply-media-paths.runtime.js", () => ({ | |
| createReplyMediaPathNormalizer: createReplyMediaPathNormalizerMock, | |
| })); | |
| type DeliverParams = Parameters<typeof deliverAgentCommandResult>[0]; | |
| type RunResult = DeliverParams["result"]; | |
| type TextPayloadLike = { text?: unknown }; | |
| type ResolveReplyTransportParams = Parameters< | |
| NonNullable<ChannelThreadingAdapter["resolveReplyTransport"]> | |
| >[0]; | |
| type MediaNormalizerOptions = { | |
| sessionKey?: unknown; | |
| agentId?: unknown; | |
| workspaceDir?: unknown; | |
| messageProvider?: unknown; | |
| }; | |
| type ReplyPayloadSendingHookArgs = { | |
| kind?: unknown; | |
| channel?: unknown; | |
| sessionKey?: unknown; | |
| runId?: unknown; | |
| context?: Record<string, unknown>; | |
| }; | |
| const slackOutboundForTest: ChannelOutboundAdapter = { | |
| deliveryMode: "direct", | |
| sendText: async ({ to, text }) => ({ | |
| channel: "slack", | |
| messageId: `${to}:${text}`, | |
| }), | |
| }; | |
| // Two registries let tests switch between no-channel and Slack-capable delivery | |
| // without loading the full plugin runtime. | |
| const emptyRegistry = createTestRegistry([]); | |
| const slackPluginForTest = createOutboundTestPlugin({ | |
| id: "slack", | |
| outbound: slackOutboundForTest, | |
| }); | |
| const slackRegistry = createTestRegistry([ | |
| { | |
| pluginId: "slack", | |
| source: "test", | |
| plugin: { | |
| ...slackPluginForTest, | |
| threading: { | |
| resolveReplyTransport: ({ threadId }: ResolveReplyTransportParams) => ({ | |
| replyToId: threadId == null ? undefined : String(threadId), | |
| threadId: null, | |
| }), | |
| }, | |
| }, | |
| }, | |
| ]); | |
| function createResult(overrides: Partial<RunResult> = {}): RunResult { | |
| return { | |
| meta: { | |
| durationMs: 1, | |
| ...overrides.meta, | |
| }, | |
| ...(overrides.payloads ? { payloads: overrides.payloads } : {}), | |
| } as RunResult; | |
| } | |
| type MessagingToolSentTarget = NonNullable<RunResult["messagingToolSentTargets"]>[number]; | |
| type DeliveryFixture = Omit<Partial<DeliverParams>, "opts" | "payloads" | "result"> & { | |
| payloads: DeliverParams["payloads"]; | |
| opts?: Partial<AgentCommandOpts>; | |
| omitReplyTarget?: boolean; | |
| result?: Partial<RunResult>; | |
| sentTarget?: Partial<MessagingToolSentTarget>; | |
| workspace?: boolean; | |
| }; | |
| function deliverAgentCommandResultForTest({ | |
| opts, | |
| omitReplyTarget, | |
| result, | |
| sentTarget, | |
| workspace, | |
| ...params | |
| }: DeliveryFixture) { | |
| return deliverAgentCommandResult({ | |
| cfg: (workspace | |
| ? { agents: { list: [{ id: "tester", workspace: "/tmp/agent-workspace" }] } } | |
| : {}) as OpenClawConfig, | |
| deps: {} as CliDeps, | |
| runtime: { log: vi.fn(), error: vi.fn() } as never, | |
| opts: { | |
| message: "completion handoff", | |
| deliver: true, | |
| ...(omitReplyTarget ? {} : { replyChannel: "slack", replyTo: "channel:C123" }), | |
| ...opts, | |
| } as AgentCommandOpts, | |
| outboundSession: undefined, | |
| sessionEntry: undefined, | |
| result: { | |
| ...createResult(), | |
| ...result, | |
| ...(sentTarget | |
| ? { | |
| messagingToolSentTargets: [ | |
| { tool: "message", provider: "slack", to: "channel:C123", ...sentTarget }, | |
| ], | |
| } | |
| : {}), | |
| } as RunResult, | |
| ...params, | |
| } as DeliverParams); | |
| } | |
| function expectTextPayload(payload: TextPayloadLike | undefined, text: string): void { | |
| expect(payload?.text).toBe(text); | |
| } | |
| function requirePayload(payloads: readonly ReplyPayload[], index: number): ReplyPayload { | |
| const payload = payloads.at(index); | |
| if (!payload) { | |
| throw new Error(`expected payload at index ${index}`); | |
| } | |
| return payload; | |
| } | |
| function lastMockArg(mock: { mock: { calls: Array<Array<unknown>> } }, label: string): unknown { | |
| const calls = mock.mock.calls; | |
| const call = calls[calls.length - 1]; | |
| if (!call) { | |
| throw new Error(`expected ${label}`); | |
| } | |
| return call[0]; | |
| } | |
| function latestNormalizerOptions(): MediaNormalizerOptions { | |
| const options = lastMockArg(createReplyMediaPathNormalizerMock, "media normalizer options"); | |
| if (!options || typeof options !== "object") { | |
| throw new Error("expected media normalizer options"); | |
| } | |
| return options as MediaNormalizerOptions; | |
| } | |
| function latestOutboundDeliveryArgs(): { | |
| channel?: string; | |
| to?: string; | |
| accountId?: string; | |
| replyToId?: string | null; | |
| threadId?: string | number | null; | |
| payloads: ReplyPayload[]; | |
| bestEffort?: boolean; | |
| queuePolicy?: string; | |
| replyPayloadSendingHook?: ReplyPayloadSendingHookArgs; | |
| } { | |
| const args = lastMockArg(deliverOutboundPayloadsMock, "outbound delivery arguments"); | |
| if (!args || typeof args !== "object") { | |
| throw new Error("expected outbound delivery arguments"); | |
| } | |
| return args as { | |
| channel?: string; | |
| to?: string; | |
| accountId?: string; | |
| replyToId?: string | null; | |
| threadId?: string | number | null; | |
| payloads: ReplyPayload[]; | |
| bestEffort?: boolean; | |
| queuePolicy?: string; | |
| replyPayloadSendingHook?: ReplyPayloadSendingHookArgs; | |
| }; | |
| } | |
| type DeliveryStatusLike = { | |
| requested?: unknown; | |
| attempted?: unknown; | |
| status?: unknown; | |
| succeeded?: unknown; | |
| reason?: unknown; | |
| error?: unknown; | |
| errorMessage?: unknown; | |
| resultCount?: unknown; | |
| sentBeforeError?: unknown; | |
| payloadOutcomes?: Array<Record<string, unknown>>; | |
| }; | |
| function deliveryStatus(delivered: { deliveryStatus?: unknown }): DeliveryStatusLike { | |
| return (delivered.deliveryStatus ?? {}) as DeliveryStatusLike; | |
| } | |
| function expectDeliveryStatusFields( | |
| delivered: { deliveryStatus?: unknown }, | |
| expected: Record<string, unknown>, | |
| ) { | |
| const status = deliveryStatus(delivered); | |
| for (const [key, value] of Object.entries(expected)) { | |
| expect(status[key as keyof DeliveryStatusLike], key).toEqual(value); | |
| } | |
| return status; | |
| } | |
| function expectRuntimeErrorIncludes( | |
| runtime: { error: { mock: { calls: Array<Array<unknown>> } } }, | |
| text: string, | |
| ) { | |
| const errorOutput = runtime.error.mock.calls.map(([message]) => String(message)).join("\n"); | |
| expect(errorOutput).toContain(text); | |
| } | |
| function latestJsonOutput(runtime: { writeJson: { mock: { calls: Array<Array<unknown>> } } }) { | |
| const output = lastMockArg(runtime.writeJson, "JSON output"); | |
| if (!output || typeof output !== "object") { | |
| throw new Error("expected JSON output"); | |
| } | |
| return output as { | |
| payloads: unknown[]; | |
| meta?: unknown; | |
| deliveryStatus?: DeliveryStatusLike; | |
| }; | |
| } | |
| async function deliverMediaReplyForTest( | |
| outboundSession: DeliverParams["outboundSession"], | |
| optsOverrides: Partial<AgentCommandOpts> = {}, | |
| ) { | |
| // Media replies go through the same normalizer seam as production so relative | |
| // paths are interpreted with agent/session context before delivery. | |
| return await deliverAgentCommandResultForTest({ | |
| workspace: true, | |
| opts: { | |
| message: "go", | |
| replyTo: "#general", | |
| ...optsOverrides, | |
| }, | |
| outboundSession, | |
| payloads: [{ text: "here you go", mediaUrls: ["./out/photo.png"] }], | |
| }); | |
| } | |
| describe("deliverAgentCommandResult payload normalization", () => { | |
| beforeEach(() => { | |
| setActivePluginRegistry(slackRegistry); | |
| deliverOutboundPayloadsMock.mockReset(); | |
| deliverOutboundPayloadsMock.mockResolvedValue([]); | |
| createReplyMediaPathNormalizerMock.mockReset(); | |
| createReplyMediaPathNormalizerMock.mockImplementation( | |
| (..._args: unknown[]) => | |
| (payload: ReplyPayload) => | |
| Promise.resolve(payload), | |
| ); | |
| }); | |
| afterEach(() => { | |
| setActivePluginRegistry(emptyRegistry); | |
| }); | |
| it.each([ | |
| { | |
| name: "only a tool failure", | |
| payloads: [{ text: "Yield failed", isError: true }], | |
| visible: false, | |
| }, | |
| { | |
| name: "a final reply after a tool failure", | |
| payloads: [ | |
| { text: "Yield failed", isError: true }, | |
| { text: "Both child results are ready." }, | |
| ], | |
| visible: true, | |
| }, | |
| ])("preserves completion visibility for $name", async ({ payloads, visible }) => { | |
| const delivered = await deliverAgentCommandResultForTest({ | |
| payloads, | |
| opts: { deliver: false }, | |
| omitReplyTarget: true, | |
| }); | |
| expect( | |
| hasVisibleAgentPayload(delivered, { | |
| includeErrorPayloads: false, | |
| includeReasoningPayloads: false, | |
| requireTerminalContent: true, | |
| }), | |
| ).toBe(visible); | |
| }); | |
| it("rechecks delivery ownership after asynchronous payload preparation", async () => { | |
| let deliveryCurrent = true; | |
| createReplyMediaPathNormalizerMock.mockImplementationOnce( | |
| (..._args: unknown[]) => | |
| async (payload: ReplyPayload): Promise<ReplyPayload> => { | |
| deliveryCurrent = false; | |
| return payload; | |
| }, | |
| ); | |
| await expect( | |
| deliverAgentCommandResult({ | |
| cfg: { | |
| agents: { | |
| list: [{ id: "tester", workspace: "/tmp/agent-workspace" }], | |
| }, | |
| } as OpenClawConfig, | |
| deps: {} as CliDeps, | |
| runtime: { log: vi.fn(), error: vi.fn() } as never, | |
| opts: { | |
| message: "go", | |
| deliver: true, | |
| replyChannel: "slack", | |
| replyTo: "#general", | |
| } as AgentCommandOpts, | |
| outboundSession: undefined, | |
| sessionEntry: undefined, | |
| payloads: [{ text: "result", mediaUrls: ["./out/photo.png"] }], | |
| result: createResult(), | |
| assertDeliveryCurrent: () => { | |
| if (!deliveryCurrent) { | |
| throw new Error("stale lifecycle"); | |
| } | |
| }, | |
| }), | |
| ).rejects.toThrow("stale lifecycle"); | |
| expect(deliverOutboundPayloadsMock).not.toHaveBeenCalled(); | |
| }); | |
| it("forwards the run abort signal into durable delivery", async () => { | |
| const controller = new AbortController(); | |
| controller.abort(createAgentRunRestartAbortError()); | |
| await deliverMediaReplyForTest(undefined, { | |
| abortSignal: controller.signal, | |
| }); | |
| const deliverySignal = ( | |
| deliverOutboundPayloadsMock.mock.calls[0]?.[0] as { abortSignal?: AbortSignal } | undefined | |
| )?.abortSignal; | |
| expect(deliverySignal).toBeInstanceOf(AbortSignal); | |
| expect(deliverySignal?.aborted).toBe(true); | |
| expect(deliverySignal?.reason).toBe(controller.signal.reason); | |
| }); | |
| it("does not cancel final delivery for an ordinary run timeout", async () => { | |
| const controller = new AbortController(); | |
| const timeoutError = new Error("run timed out"); | |
| timeoutError.name = "TimeoutError"; | |
| controller.abort(timeoutError); | |
| await deliverMediaReplyForTest(undefined, { | |
| abortSignal: controller.signal, | |
| }); | |
| const deliverySignal = ( | |
| deliverOutboundPayloadsMock.mock.calls[0]?.[0] as { abortSignal?: AbortSignal } | undefined | |
| )?.abortSignal; | |
| expect(deliverySignal).toBeInstanceOf(AbortSignal); | |
| expect(deliverySignal?.aborted).toBe(false); | |
| }); | |
| it("cancels durable delivery when restart arrives before the durable intent", async () => { | |
| const controller = new AbortController(); | |
| let deliverySignal: AbortSignal | undefined; | |
| deliverOutboundPayloadsMock.mockImplementationOnce(async (params: unknown) => { | |
| deliverySignal = (params as { abortSignal?: AbortSignal }).abortSignal; | |
| controller.abort(createAgentRunRestartAbortError()); | |
| expect(deliverySignal?.aborted).toBe(true); | |
| throw deliverySignal?.reason; | |
| }); | |
| await expect( | |
| deliverMediaReplyForTest(undefined, { | |
| abortSignal: controller.signal, | |
| }), | |
| ).rejects.toThrow("agent run aborted for restart"); | |
| expect(deliverySignal?.reason).toBe(controller.signal.reason); | |
| }); | |
| it("finishes durable delivery when restart arrives after the durable intent", async () => { | |
| const controller = new AbortController(); | |
| let deliverySignal: AbortSignal | undefined; | |
| deliverOutboundPayloadsMock.mockImplementationOnce(async (params: unknown) => { | |
| const request = params as { | |
| abortSignal?: AbortSignal; | |
| onDeliveryIntent?: (intent: { | |
| id: string; | |
| channel: string; | |
| to: string; | |
| queuePolicy: "required"; | |
| }) => void; | |
| }; | |
| deliverySignal = request.abortSignal; | |
| request.onDeliveryIntent?.({ | |
| id: "intent-after-restart", | |
| channel: "discord", | |
| to: "channel:123", | |
| queuePolicy: "required", | |
| }); | |
| controller.abort(createAgentRunRestartAbortError()); | |
| expect(deliverySignal?.aborted).toBe(false); | |
| return [{ channel: "discord", messageId: "sent-after-restart" }]; | |
| }); | |
| const result = await deliverMediaReplyForTest(undefined, { | |
| abortSignal: controller.signal, | |
| }); | |
| expect(result.deliverySucceeded).toBe(true); | |
| expect(deliverySignal?.aborted).toBe(false); | |
| }); | |
| it("passes final reply hook metadata through durable delivery", async () => { | |
| deliverOutboundPayloadsMock.mockResolvedValue([{ channel: "slack", messageId: "msg-1" }]); | |
| await deliverAgentCommandResultForTest({ | |
| workspace: true, | |
| opts: { | |
| message: "go", | |
| replyTo: "#general", | |
| replyAccountId: "workspace-1", | |
| threadId: "thread-1", | |
| runId: "run-1", | |
| }, | |
| outboundSession: { | |
| key: "agent:tester:slack:direct:alice", | |
| agentId: "tester", | |
| } as never, | |
| sessionEntry: { | |
| sessionId: "session-1", | |
| updatedAt: 1, | |
| }, | |
| payloads: [{ text: "final answer" }], | |
| }); | |
| expect(latestOutboundDeliveryArgs().replyPayloadSendingHook).toEqual({ | |
| kind: "final", | |
| channel: "slack", | |
| sessionKey: "agent:tester:slack:direct:alice", | |
| runId: "run-1", | |
| context: { | |
| channelId: "slack", | |
| accountId: "workspace-1", | |
| conversationId: "#general", | |
| sessionKey: "agent:tester:slack:direct:alice", | |
| runId: "run-1", | |
| }, | |
| }); | |
| }); | |
| it.each([ | |
| { source: "outbound agent", outboundSession: { agentId: "worker" }, name: "Worker" }, | |
| { source: "session key", sessionKey: "agent:worker:slack:channel:c123", name: "Worker" }, | |
| { source: "sole agent", name: "Default" }, | |
| ])("carries the $source identity through durable final delivery", async (testCase) => { | |
| await deliverAgentCommandResultForTest({ | |
| cfg: { | |
| agents: { | |
| entries: { | |
| main: { identity: { name: " Default ", emoji: " :robot_face: " } }, | |
| ...(testCase.name === "Worker" | |
| ? { worker: { identity: { name: " Worker ", emoji: " :robot_face: " } } } | |
| : {}), | |
| }, | |
| }, | |
| }, | |
| outboundSession: testCase.outboundSession, | |
| opts: { sessionKey: testCase.sessionKey }, | |
| payloads: [{ text: "final answer" }], | |
| }); | |
| expect(latestOutboundDeliveryArgs()).toMatchObject({ | |
| identity: { name: testCase.name, emoji: ":robot_face:" }, | |
| }); | |
| }); | |
| it("keeps Gateway reset status notices through the durable delivery handoff", async () => { | |
| deliverOutboundPayloadsMock.mockResolvedValue([{ channel: "slack", messageId: "msg-1" }]); | |
| await deliverAgentCommandResultForTest({ | |
| payloads: [{ text: "✅ New session started.", isStatusNotice: true }], | |
| }); | |
| expect(latestOutboundDeliveryArgs().payloads).toEqual([ | |
| expect.objectContaining({ | |
| text: "✅ New session started.", | |
| isStatusNotice: true, | |
| }), | |
| ]); | |
| }); | |
| it("renders response prefix templates with the selected runtime model", async () => { | |
| const delivered = await deliverAgentCommandResult({ | |
| cfg: { | |
| channels: { slack: { responsePrefix: "[{modelFull}]" } }, | |
| } as OpenClawConfig, | |
| deps: {} as CliDeps, | |
| runtime: { log: vi.fn() } as never, | |
| opts: { message: "test", channel: "slack" } as AgentCommandOpts, | |
| outboundSession: undefined, | |
| sessionEntry: undefined, | |
| payloads: [{ text: "Ready." }], | |
| result: createResult({ | |
| meta: { | |
| durationMs: 1, | |
| agentMeta: { | |
| sessionId: "session-1", | |
| provider: "openai", | |
| model: "gpt-5.4", | |
| }, | |
| }, | |
| }), | |
| }); | |
| expect(delivered.payloads).toHaveLength(1); | |
| expectTextPayload(delivered.payloads[0], "[openai/gpt-5.4] Ready."); | |
| }); | |
| it("normalizes reply-media paths before outbound delivery", async () => { | |
| const normalizerFn = vi.fn(async (payload: ReplyPayload): Promise<ReplyPayload> => ({ | |
| ...payload, | |
| mediaUrl: "/tmp/agent-workspace/out/photo.png", | |
| mediaUrls: ["/tmp/agent-workspace/out/photo.png"], | |
| })); | |
| createReplyMediaPathNormalizerMock.mockReturnValue(normalizerFn); | |
| deliverOutboundPayloadsMock.mockResolvedValue([]); | |
| await deliverMediaReplyForTest({ | |
| key: "agent:tester:slack:direct:alice", | |
| agentId: "tester", | |
| } as never); | |
| const normalizerOptions = latestNormalizerOptions(); | |
| expect(normalizerOptions.sessionKey).toBe("agent:tester:slack:direct:alice"); | |
| expect(normalizerOptions.agentId).toBe("tester"); | |
| expect(normalizerOptions.workspaceDir).toBe("/tmp/agent-workspace"); | |
| expect(normalizerOptions.messageProvider).toBe("slack"); | |
| const normalizedInput = normalizerFn.mock.calls[0]?.[0]; | |
| expect(normalizedInput?.mediaUrls).toStrictEqual(["./out/photo.png"]); | |
| expect(deliverOutboundPayloadsMock).toHaveBeenCalledTimes(1); | |
| const deliverArgs = latestOutboundDeliveryArgs(); | |
| expect(requirePayload(deliverArgs.payloads, 0).mediaUrls).toStrictEqual([ | |
| "/tmp/agent-workspace/out/photo.png", | |
| ]); | |
| }); | |
| it.each([ | |
| { name: "empty", payloads: [], expectedPayloads: [] }, | |
| { | |
| name: "text and media", | |
| payloads: [{ text: "hello", mediaUrl: "https://example.invalid/photo.png" }], | |
| expectedPayloads: [ | |
| { | |
| text: "hello", | |
| mediaUrl: "https://example.invalid/photo.png", | |
| mediaUrls: ["https://example.invalid/photo.png"], | |
| }, | |
| ], | |
| }, | |
| ])( | |
| "emits canonical $name JSON and isolates the writer's payload array", | |
| async ({ payloads, expectedPayloads }) => { | |
| const result = createResult(); | |
| let serialized: string | undefined; | |
| let emittedPayloads: unknown[] | undefined; | |
| const runtime = { | |
| log: vi.fn(), | |
| error: vi.fn(), | |
| writeStdout: vi.fn(), | |
| writeJson: vi.fn((value: { payloads: unknown[]; meta?: unknown }) => { | |
| expect(Object.keys(value)).toEqual(["payloads", "meta"]); | |
| expect(value.meta).toBe(result.meta); | |
| serialized = JSON.stringify(value); | |
| emittedPayloads = value.payloads; | |
| value.payloads.splice(0); | |
| }), | |
| }; | |
| const delivered = await deliverAgentCommandResultForTest({ | |
| runtime: runtime as never, | |
| opts: { deliver: false, json: true }, | |
| payloads, | |
| result, | |
| }); | |
| expect(runtime.writeJson).toHaveBeenCalledOnce(); | |
| expect(runtime.log).not.toHaveBeenCalled(); | |
| expect(deliverOutboundPayloadsMock).not.toHaveBeenCalled(); | |
| expect(serialized).toBe( | |
| JSON.stringify({ payloads: expectedPayloads, meta: { durationMs: 1 } }), | |
| ); | |
| expect(emittedPayloads).not.toBe(delivered.payloads); | |
| expect(delivered.payloads).toEqual(expectedPayloads); | |
| expect(delivered.meta).toBe(result.meta); | |
| expect(delivered.deliveryStatus).toBeUndefined(); | |
| }, | |
| ); | |
| it("reports successful requested delivery", async () => { | |
| deliverOutboundPayloadsMock.mockResolvedValue([]); | |
| const delivered = await deliverMediaReplyForTest({ | |
| key: "agent:tester:slack:direct:alice", | |
| agentId: "tester", | |
| } as never); | |
| expect(delivered.deliverySucceeded).toBe(true); | |
| expectDeliveryStatusFields(delivered, { | |
| requested: true, | |
| attempted: true, | |
| status: "suppressed", | |
| succeeded: true, | |
| reason: "no_visible_result", | |
| }); | |
| }); | |
| it("refreshes stale implicit session routing before final delivery", async () => { | |
| deliverOutboundPayloadsMock.mockResolvedValue([{ channel: "slack", messageId: "msg-1" }]); | |
| const runtime = { log: vi.fn(), error: vi.fn() }; | |
| const resolveFreshSessionEntryForDelivery = vi.fn(async () => ({ | |
| sessionId: "session-1", | |
| updatedAt: 2, | |
| delivery: normalizeSessionDeliveryState({ | |
| context: { | |
| channel: "slack", | |
| to: "#fresh", | |
| accountId: "workspace-1", | |
| }, | |
| }), | |
| })); | |
| const delivered = await deliverAgentCommandResultForTest({ | |
| workspace: true, | |
| runtime: runtime as never, | |
| omitReplyTarget: true, | |
| opts: { | |
| message: "go", | |
| bestEffortDeliver: true, | |
| sessionKey: "agent:tester:main", | |
| }, | |
| outboundSession: { | |
| key: "agent:tester:main", | |
| agentId: "tester", | |
| } as never, | |
| sessionEntry: { | |
| sessionId: "session-1", | |
| updatedAt: 1, | |
| }, | |
| expectedSessionIdForFreshDelivery: "session-1", | |
| resolveFreshSessionEntryForDelivery, | |
| payloads: [{ text: "final answer" }], | |
| }); | |
| expect(resolveFreshSessionEntryForDelivery).toHaveBeenCalledTimes(1); | |
| expect(deliverOutboundPayloadsMock).toHaveBeenCalledTimes(1); | |
| const deliverArgs = latestOutboundDeliveryArgs(); | |
| expect(deliverArgs.channel).toBe("slack"); | |
| expect(deliverArgs.to).toBe("#fresh"); | |
| expect(deliverArgs.accountId).toBe("workspace-1"); | |
| expect(delivered.deliverySucceeded).toBe(true); | |
| expectDeliveryStatusFields(delivered, { | |
| requested: true, | |
| attempted: true, | |
| status: "sent", | |
| succeeded: true, | |
| resultCount: 1, | |
| }); | |
| }); | |
| it("does not refresh final delivery routing from a different logical session", async () => { | |
| deliverOutboundPayloadsMock.mockResolvedValue([{ channel: "slack", messageId: "msg-1" }]); | |
| const runtime = { log: vi.fn(), error: vi.fn() }; | |
| const resolveFreshSessionEntryForDelivery = vi.fn(async () => ({ | |
| sessionId: "session-2", | |
| updatedAt: 2, | |
| delivery: normalizeSessionDeliveryState({ | |
| context: { | |
| channel: "slack", | |
| to: "#fresh", | |
| accountId: "workspace-1", | |
| }, | |
| }), | |
| })); | |
| const delivered = await deliverAgentCommandResultForTest({ | |
| workspace: true, | |
| runtime: runtime as never, | |
| omitReplyTarget: true, | |
| opts: { | |
| message: "go", | |
| bestEffortDeliver: true, | |
| sessionKey: "agent:tester:main", | |
| }, | |
| outboundSession: { | |
| key: "agent:tester:main", | |
| agentId: "tester", | |
| } as never, | |
| sessionEntry: { | |
| sessionId: "session-1", | |
| updatedAt: 1, | |
| }, | |
| expectedSessionIdForFreshDelivery: "session-1", | |
| resolveFreshSessionEntryForDelivery, | |
| payloads: [{ text: "final answer" }], | |
| }); | |
| expect(resolveFreshSessionEntryForDelivery).toHaveBeenCalledTimes(1); | |
| expect(deliverOutboundPayloadsMock).not.toHaveBeenCalled(); | |
| expect(delivered.deliverySucceeded).toBe(false); | |
| expectDeliveryStatusFields(delivered, { | |
| requested: true, | |
| attempted: false, | |
| status: "failed", | |
| succeeded: false, | |
| reason: "channel_resolved_to_internal", | |
| }); | |
| }); | |
| it("does not report success when best-effort delivery records an error", async () => { | |
| deliverOutboundPayloadsMock.mockImplementationOnce(async (params: unknown) => { | |
| ( | |
| params as { | |
| onError?: (err: unknown, payload: ReplyPayload) => void; | |
| onPayloadDeliveryOutcome?: (outcome: { | |
| index: number; | |
| payload: ReplyPayload; | |
| status: "failed"; | |
| error: Error; | |
| stage: "send"; | |
| }) => void; | |
| } | |
| ).onError?.(new Error("send failed"), { text: "here you go" }); | |
| ( | |
| params as { | |
| onPayloadDeliveryOutcome?: (outcome: { | |
| index: number; | |
| payload: ReplyPayload; | |
| status: "failed"; | |
| error: Error; | |
| stage: "send"; | |
| }) => void; | |
| } | |
| ).onPayloadDeliveryOutcome?.({ | |
| index: 0, | |
| payload: { text: "here you go" }, | |
| status: "failed", | |
| error: new Error("send failed"), | |
| stage: "send", | |
| }); | |
| return []; | |
| }); | |
| const runtime = { log: vi.fn(), error: vi.fn() }; | |
| const delivered = await deliverAgentCommandResultForTest({ | |
| workspace: true, | |
| runtime: runtime as never, | |
| opts: { | |
| message: "go", | |
| bestEffortDeliver: true, | |
| replyTo: "#general", | |
| }, | |
| outboundSession: { | |
| key: "agent:tester:slack:direct:alice", | |
| agentId: "tester", | |
| } as never, | |
| payloads: [{ text: "here you go" }], | |
| }); | |
| expect(delivered.deliverySucceeded).toBe(false); | |
| expectDeliveryStatusFields(delivered, { | |
| requested: true, | |
| attempted: true, | |
| status: "failed", | |
| succeeded: false, | |
| error: true, | |
| }); | |
| expectRuntimeErrorIncludes(runtime, "send failed"); | |
| const deliverArgs = latestOutboundDeliveryArgs(); | |
| expect(deliverArgs.bestEffort).toBe(true); | |
| expect(deliverArgs.queuePolicy).toBe("best_effort"); | |
| }); | |
| it("threads agentId into the normalizer when sessionKey is unresolved", async () => { | |
| createReplyMediaPathNormalizerMock.mockReturnValue(async (payload: ReplyPayload) => payload); | |
| deliverOutboundPayloadsMock.mockResolvedValue([]); | |
| await deliverMediaReplyForTest({ agentId: "tester" } as never); | |
| const normalizerOptions = latestNormalizerOptions(); | |
| expect(normalizerOptions.agentId).toBe("tester"); | |
| expect(normalizerOptions.sessionKey).toBeUndefined(); | |
| expect(normalizerOptions.workspaceDir).toBe("/tmp/agent-workspace"); | |
| }); | |
| it("keeps LINE directive-only replies intact for local preview when delivery is disabled", async () => { | |
| const runtime = { | |
| log: vi.fn(), | |
| }; | |
| const delivered = await deliverAgentCommandResult({ | |
| cfg: {} as OpenClawConfig, | |
| deps: {} as CliDeps, | |
| runtime: runtime as never, | |
| opts: { | |
| message: "test", | |
| channel: "line", | |
| } as AgentCommandOpts, | |
| outboundSession: undefined, | |
| sessionEntry: undefined, | |
| payloads: [ | |
| { | |
| text: "[[buttons: Release menu | Choose an action | Retry:retry, Ignore:ignore]]", | |
| }, | |
| ], | |
| result: createResult(), | |
| }); | |
| expect(runtime.log).toHaveBeenCalledTimes(1); | |
| expect(runtime.log).toHaveBeenCalledWith( | |
| "[[buttons: Release menu | Choose an action | Retry:retry, Ignore:ignore]]", | |
| ); | |
| expect(delivered.payloads).toHaveLength(1); | |
| expectTextPayload( | |
| delivered.payloads[0], | |
| "[[buttons: Release menu | Choose an action | Retry:retry, Ignore:ignore]]", | |
| ); | |
| }); | |
| it("preserves settled continuation through empty command output normalization", async () => { | |
| const delivered = await deliverAgentCommandResultForTest({ | |
| omitReplyTarget: true, | |
| opts: { deliver: false }, | |
| payloads: [], | |
| result: { requesterContinuationSettled: true }, | |
| }); | |
| expect(delivered.requesterContinuationSettled).toBe(true); | |
| }); | |
| it("preserves committed message-tool delivery evidence when automatic delivery is disabled", async () => { | |
| const runtime = { log: vi.fn(), error: vi.fn() }; | |
| const delivered = await deliverAgentCommandResultForTest({ | |
| runtime: runtime as never, | |
| omitReplyTarget: true, | |
| opts: { deliver: false }, | |
| payloads: [], | |
| result: { | |
| didSendViaMessagingTool: true, | |
| messagingToolSentTexts: ["The image is ready."], | |
| messagingToolSentMediaUrls: ["/tmp/generated-image.png"], | |
| }, | |
| sentTarget: { | |
| provider: "telegram", | |
| to: "telegram:-100123", | |
| threadId: "22", | |
| text: "The image is ready.", | |
| mediaUrls: ["/tmp/generated-image.png"], | |
| }, | |
| }); | |
| expect(delivered.didSendViaMessagingTool).toBe(true); | |
| expect(delivered.messagingToolSentTexts).toEqual(["The image is ready."]); | |
| expect(delivered.messagingToolSentMediaUrls).toEqual(["/tmp/generated-image.png"]); | |
| expect(delivered.messagingToolSentTargets).toEqual([ | |
| { | |
| tool: "message", | |
| provider: "telegram", | |
| to: "telegram:-100123", | |
| threadId: "22", | |
| text: "The image is ready.", | |
| mediaUrls: ["/tmp/generated-image.png"], | |
| }, | |
| ]); | |
| expect(deliverOutboundPayloadsMock).not.toHaveBeenCalled(); | |
| }); | |
| it.each([ | |
| { | |
| name: "deterministic approval prompt", | |
| result: { didSendDeterministicApprovalPrompt: true }, | |
| field: "didSendDeterministicApprovalPrompt", | |
| expected: true, | |
| }, | |
| { | |
| name: "accepted session spawn", | |
| result: { | |
| acceptedSessionSpawns: [ | |
| { runId: "child-run", childSessionKey: "agent:main:subagent:child" }, | |
| ], | |
| }, | |
| field: "acceptedSessionSpawns", | |
| expected: [{ runId: "child-run", childSessionKey: "agent:main:subagent:child" }], | |
| }, | |
| { | |
| name: "successful cron add", | |
| result: { successfulCronAdds: 1 }, | |
| field: "successfulCronAdds", | |
| expected: 1, | |
| }, | |
| ])("preserves $name as restart-unsafe delivery evidence", async ({ result, field, expected }) => { | |
| const onDeliveryResult = vi.fn(); | |
| const delivered = await deliverAgentCommandResultForTest({ | |
| omitReplyTarget: true, | |
| opts: { deliver: false }, | |
| payloads: [], | |
| result, | |
| onDeliveryResult, | |
| }); | |
| expect(delivered).toHaveProperty(field, expected); | |
| expect(onDeliveryResult).toHaveBeenCalledOnce(); | |
| expect(onDeliveryResult).toHaveBeenCalledWith(delivered); | |
| expect(buildRestartRecoveryTerminalDeliveryEvidence(delivered)).toEqual({ | |
| captured: true, | |
| restartUnsafeSideEffectsDetected: true, | |
| }); | |
| }); | |
| it("does not automatically redeliver text and media already sent to the same target", async () => { | |
| const delivered = await deliverAgentCommandResultForTest({ | |
| opts: { threadId: "171.222" }, | |
| payloads: [{ text: "The image is ready.", mediaUrls: ["/tmp/generated-image.png"] }], | |
| result: { | |
| didSendViaMessagingTool: true, | |
| messagingToolSentTexts: ["The image is ready."], | |
| messagingToolSentMediaUrls: ["/tmp/generated-image.png"], | |
| }, | |
| sentTarget: { | |
| threadId: "171.222", | |
| text: "The image is ready.", | |
| mediaUrls: ["/tmp/generated-image.png"], | |
| }, | |
| }); | |
| expect(delivered.payloads).toEqual([]); | |
| expect(delivered.deliverySucceeded).toBe(true); | |
| expectDeliveryStatusFields(delivered, { | |
| requested: true, | |
| attempted: false, | |
| status: "suppressed", | |
| succeeded: true, | |
| reason: "no_visible_payload", | |
| }); | |
| expect(delivered.messagingToolSentMediaUrls).toEqual(["/tmp/generated-image.png"]); | |
| expect(deliverOutboundPayloadsMock).not.toHaveBeenCalled(); | |
| }); | |
| it("preserves duplicate media needed for a delivery operation", async () => { | |
| deliverOutboundPayloadsMock.mockResolvedValue([{ channel: "slack", messageId: "msg-1" }]); | |
| const delivery = { pin: { enabled: true, required: true } }; | |
| const delivered = await deliverAgentCommandResultForTest({ | |
| payloads: [{ mediaUrls: ["/tmp/generated-image.png"], delivery }] as never, | |
| sentTarget: { mediaUrls: ["/tmp/generated-image.png"] }, | |
| }); | |
| expect(delivered.deliverySucceeded).toBe(true); | |
| expect(latestOutboundDeliveryArgs().payloads).toEqual([ | |
| expect.objectContaining({ | |
| mediaUrls: ["/tmp/generated-image.png"], | |
| delivery, | |
| }), | |
| ]); | |
| }); | |
| it("drops audioAsVoice when its media was already delivered", async () => { | |
| const delivered = await deliverAgentCommandResultForTest({ | |
| payloads: [{ mediaUrls: ["/tmp/voice.ogg"], audioAsVoice: true }], | |
| sentTarget: { mediaUrls: ["/tmp/voice.ogg"] }, | |
| }); | |
| expect(delivered.payloads).toEqual([]); | |
| expect(deliverOutboundPayloadsMock).not.toHaveBeenCalled(); | |
| }); | |
| it("dedupes delivered file media before normalization can add a failure warning", async () => { | |
| createReplyMediaPathNormalizerMock.mockImplementationOnce( | |
| (..._args: unknown[]) => | |
| async (payload: ReplyPayload): Promise<ReplyPayload> => ({ | |
| ...payload, | |
| text: `${payload.text ?? ""}\n⚠️ Media failed.`, | |
| mediaUrl: undefined, | |
| mediaUrls: undefined, | |
| }), | |
| ); | |
| const delivered = await deliverAgentCommandResultForTest({ | |
| payloads: [{ text: "The image is ready.", mediaUrls: ["file:///tmp/generated-image.png"] }], | |
| sentTarget: { | |
| text: "The image is ready.", | |
| mediaUrls: ["file:///tmp/generated-image.png"], | |
| }, | |
| }); | |
| expect(delivered.payloads).toEqual([]); | |
| expect(createReplyMediaPathNormalizerMock).not.toHaveBeenCalled(); | |
| expect(deliverOutboundPayloadsMock).not.toHaveBeenCalled(); | |
| }); | |
| it("dedupes media encoded in a final MEDIA directive", async () => { | |
| const delivered = await deliverAgentCommandResultForTest({ | |
| payloads: [{ text: "MEDIA:/tmp/generated-image.png" }], | |
| sentTarget: { mediaUrls: ["/tmp/generated-image.png"] }, | |
| }); | |
| expect(delivered.payloads).toEqual([]); | |
| expect(deliverOutboundPayloadsMock).not.toHaveBeenCalled(); | |
| }); | |
| it("keeps unsent media when only the matching text was already delivered", async () => { | |
| deliverOutboundPayloadsMock.mockResolvedValue([{ channel: "slack", messageId: "msg-1" }]); | |
| const delivered = await deliverAgentCommandResultForTest({ | |
| opts: { threadId: "171.222" }, | |
| payloads: [{ text: "The image is ready.", mediaUrls: ["/tmp/generated-image.png"] }], | |
| result: { | |
| didSendViaMessagingTool: true, | |
| messagingToolSentTexts: ["The image is ready."], | |
| }, | |
| sentTarget: { threadId: "171.222", text: "The image is ready." }, | |
| }); | |
| expect(delivered.deliverySucceeded).toBe(true); | |
| expect(latestOutboundDeliveryArgs().payloads).toEqual([ | |
| expect.objectContaining({ | |
| text: "", | |
| mediaUrls: ["/tmp/generated-image.png"], | |
| }), | |
| ]); | |
| }); | |
| it("dedupes sent text after applying the delivery response prefix", async () => { | |
| const delivered = await deliverAgentCommandResultForTest({ | |
| cfg: { | |
| channels: { slack: { responsePrefix: "Bot:" } }, | |
| } as OpenClawConfig, | |
| payloads: [{ text: "Ready" }], | |
| sentTarget: { text: "Ready" }, | |
| }); | |
| expect(delivered.payloads).toEqual([]); | |
| expect(deliverOutboundPayloadsMock).not.toHaveBeenCalled(); | |
| }); | |
| it("does not add unresolved dynamic prefixes to message-tool evidence", async () => { | |
| deliverOutboundPayloadsMock.mockResolvedValue([{ channel: "slack", messageId: "msg-1" }]); | |
| const delivered = await deliverAgentCommandResultForTest({ | |
| cfg: { | |
| channels: { slack: { responsePrefix: "[{modelFull}]" } }, | |
| } as OpenClawConfig, | |
| payloads: [{ text: "Ready" }], | |
| result: createResult({ | |
| meta: { | |
| durationMs: 1, | |
| agentMeta: { provider: "openai", model: "gpt-5.4" }, | |
| } as RunResult["meta"], | |
| }), | |
| sentTarget: { text: "Ready" }, | |
| }); | |
| expect(delivered.deliverySucceeded).toBe(true); | |
| expect(latestOutboundDeliveryArgs().payloads).toEqual([ | |
| expect.objectContaining({ text: "[openai/gpt-5.4] Ready" }), | |
| ]); | |
| }); | |
| it("dedupes exact short text on a confirmed matching route", async () => { | |
| const delivered = await deliverAgentCommandResultForTest({ | |
| payloads: [{ text: "Ready" }], | |
| sentTarget: { text: "Ready" }, | |
| }); | |
| expect(delivered.payloads).toEqual([]); | |
| expect(deliverOutboundPayloadsMock).not.toHaveBeenCalled(); | |
| }); | |
| it("dedupes visible text after parsing a final reply directive", async () => { | |
| const delivered = await deliverAgentCommandResultForTest({ | |
| payloads: [{ text: "[[reply_to_current]] Ready" }], | |
| sentTarget: { text: "Ready" }, | |
| }); | |
| expect(delivered.payloads).toEqual([]); | |
| expect(deliverOutboundPayloadsMock).not.toHaveBeenCalled(); | |
| }); | |
| it("does not apply ambiguous global evidence across message-tool targets", async () => { | |
| deliverOutboundPayloadsMock.mockResolvedValue([{ channel: "slack", messageId: "msg-1" }]); | |
| const delivered = await deliverAgentCommandResultForTest({ | |
| payloads: [{ text: "Ready", mediaUrls: ["/tmp/generated-image.png"] }], | |
| result: { | |
| messagingToolSentTexts: ["Ready"], | |
| messagingToolSentMediaUrls: ["/tmp/generated-image.png"], | |
| messagingToolSentTargets: [ | |
| { tool: "message", provider: "slack", to: "channel:C123" }, | |
| { tool: "message", provider: "slack", to: "channel:C999" }, | |
| ], | |
| }, | |
| }); | |
| expect(delivered.deliverySucceeded).toBe(true); | |
| expect(latestOutboundDeliveryArgs().payloads).toEqual([ | |
| expect.objectContaining({ | |
| text: "Ready", | |
| mediaUrls: ["/tmp/generated-image.png"], | |
| }), | |
| ]); | |
| }); | |
| it("preserves final text that extends a message-tool send", async () => { | |
| deliverOutboundPayloadsMock.mockResolvedValue([{ channel: "slack", messageId: "msg-1" }]); | |
| const delivered = await deliverAgentCommandResultForTest({ | |
| payloads: [{ text: "The image is ready. Dimensions are 1024x1024." }], | |
| sentTarget: { text: "The image is ready." }, | |
| }); | |
| expect(delivered.deliverySucceeded).toBe(true); | |
| expect(latestOutboundDeliveryArgs().payloads).toEqual([ | |
| expect.objectContaining({ | |
| text: "The image is ready. Dimensions are 1024x1024.", | |
| }), | |
| ]); | |
| }); | |
| it.each([ | |
| { name: "emoji", sentText: "❌ Failed", finalText: "✅ Failed" }, | |
| { name: "letter case", sentText: "US", finalText: "us" }, | |
| { | |
| name: "significant whitespace", | |
| sentText: "const x = 1;\n return x;", | |
| finalText: "const x = 1;\n return x;", | |
| }, | |
| ])("preserves $name differences in exact replies", async ({ sentText, finalText }) => { | |
| deliverOutboundPayloadsMock.mockResolvedValue([{ channel: "slack", messageId: "msg-1" }]); | |
| const delivered = await deliverAgentCommandResultForTest({ | |
| payloads: [{ text: finalText }], | |
| sentTarget: { text: sentText }, | |
| }); | |
| expect(delivered.deliverySucceeded).toBe(true); | |
| expect(latestOutboundDeliveryArgs().payloads).toEqual([ | |
| expect.objectContaining({ text: finalText }), | |
| ]); | |
| }); | |
| it("matches dedupe against the command thread instead of payload reply metadata", async () => { | |
| deliverOutboundPayloadsMock.mockResolvedValue([{ channel: "slack", messageId: "msg-1" }]); | |
| const delivered = await deliverAgentCommandResultForTest({ | |
| opts: { threadId: "thread-a" }, | |
| payloads: [{ text: "The image is ready.", replyToId: "thread-b" }], | |
| sentTarget: { threadId: "thread-b", text: "The image is ready." }, | |
| }); | |
| expect(delivered.deliverySucceeded).toBe(true); | |
| expect(deliverOutboundPayloadsMock).toHaveBeenCalledTimes(1); | |
| expect(latestOutboundDeliveryArgs().replyToId).toBe("thread-a"); | |
| }); | |
| it("keeps presentation content when only the matching text was already delivered", async () => { | |
| deliverOutboundPayloadsMock.mockResolvedValue([{ channel: "slack", messageId: "msg-1" }]); | |
| const presentation = { | |
| blocks: [{ type: "buttons" as const, buttons: [{ label: "Open", value: "open" }] }], | |
| }; | |
| const delivered = await deliverAgentCommandResultForTest({ | |
| payloads: [{ text: "The image is ready.", presentation }] as never, | |
| sentTarget: { text: "The image is ready." }, | |
| }); | |
| expect(delivered.deliverySucceeded).toBe(true); | |
| expect(latestOutboundDeliveryArgs().payloads).toEqual([ | |
| expect.objectContaining({ | |
| text: "", | |
| presentation, | |
| }), | |
| ]); | |
| }); | |
| it("keeps location content when only the matching text was already delivered", async () => { | |
| deliverOutboundPayloadsMock.mockResolvedValue([{ channel: "slack", messageId: "msg-1" }]); | |
| const location = { latitude: 48.858844, longitude: 2.294351 }; | |
| const delivered = await deliverAgentCommandResultForTest({ | |
| payloads: [{ text: "The image is ready.", location }] as never, | |
| sentTarget: { text: "The image is ready." }, | |
| }); | |
| expect(delivered.deliverySucceeded).toBe(true); | |
| expect(latestOutboundDeliveryArgs().payloads).toEqual([ | |
| expect.objectContaining({ | |
| text: "", | |
| location, | |
| }), | |
| ]); | |
| }); | |
| it("keeps BTW content when the base text was already delivered", async () => { | |
| deliverOutboundPayloadsMock.mockResolvedValue([{ channel: "slack", messageId: "msg-1" }]); | |
| const delivered = await deliverAgentCommandResultForTest({ | |
| payloads: [{ text: "The image is ready.", btw: { question: "What changed?" } }] as never, | |
| sentTarget: { text: "The image is ready." }, | |
| }); | |
| expect(delivered.deliverySucceeded).toBe(true); | |
| expect(latestOutboundDeliveryArgs().payloads).toEqual([ | |
| expect.objectContaining({ | |
| text: "BTW\nQuestion: What changed?\n\nThe image is ready.", | |
| }), | |
| ]); | |
| }); | |
| it("keeps delivery operations when the base text was already delivered", async () => { | |
| deliverOutboundPayloadsMock.mockResolvedValue([{ channel: "slack", messageId: "msg-1" }]); | |
| const delivery = { pin: { enabled: true, required: true } }; | |
| const delivered = await deliverAgentCommandResultForTest({ | |
| payloads: [{ text: "Ready", delivery }] as never, | |
| sentTarget: { text: "Ready" }, | |
| }); | |
| expect(delivered.deliverySucceeded).toBe(true); | |
| expect(latestOutboundDeliveryArgs().payloads).toEqual([ | |
| expect.objectContaining({ text: "Ready", delivery }), | |
| ]); | |
| }); | |
| it.each([{ delivery: { pin: false } }, { delivery: { pin: { enabled: false } } }])( | |
| "dedupes text for disabled delivery metadata: $delivery", | |
| async ({ delivery }) => { | |
| const delivered = await deliverAgentCommandResultForTest({ | |
| payloads: [{ text: "Ready", delivery }] as never, | |
| sentTarget: { text: "Ready" }, | |
| }); | |
| expect(delivered.payloads).toEqual([]); | |
| expect(deliverOutboundPayloadsMock).not.toHaveBeenCalled(); | |
| }, | |
| ); | |
| it("does not dedupe an explicit send from a different account against the default account", async () => { | |
| deliverOutboundPayloadsMock.mockResolvedValue([{ channel: "slack", messageId: "msg-1" }]); | |
| const delivered = await deliverAgentCommandResultForTest({ | |
| payloads: [{ text: "Ready" }], | |
| sentTarget: { accountId: "work", text: "Ready" }, | |
| }); | |
| expect(delivered.deliverySucceeded).toBe(true); | |
| expect(deliverOutboundPayloadsMock).toHaveBeenCalledTimes(1); | |
| expect(latestOutboundDeliveryArgs().accountId).toBe("default"); | |
| }); | |
| it("does not dedupe accountless default-account evidence against an explicit account", async () => { | |
| deliverOutboundPayloadsMock.mockResolvedValue([{ channel: "slack", messageId: "msg-1" }]); | |
| const delivered = await deliverAgentCommandResultForTest({ | |
| opts: { replyAccountId: "work" }, | |
| payloads: [{ text: "Ready" }], | |
| sentTarget: { text: "Ready" }, | |
| }); | |
| expect(delivered.deliverySucceeded).toBe(true); | |
| expect(deliverOutboundPayloadsMock).toHaveBeenCalledTimes(1); | |
| expect(latestOutboundDeliveryArgs().accountId).toBe("work"); | |
| }); | |
| it("dedupes accountless evidence against the non-default run account", async () => { | |
| deliverOutboundPayloadsMock.mockResolvedValue([{ channel: "slack", messageId: "msg-1" }]); | |
| const delivered = await deliverAgentCommandResultForTest({ | |
| opts: { | |
| replyAccountId: "work", | |
| runContext: { | |
| messageChannel: "slack", | |
| currentChannelId: "channel:C123", | |
| accountId: "work", | |
| }, | |
| }, | |
| payloads: [{ text: "Ready" }], | |
| sentTarget: { text: "Ready" }, | |
| }); | |
| expect(delivered.deliverySucceeded).toBe(true); | |
| expect(deliverOutboundPayloadsMock).not.toHaveBeenCalled(); | |
| }); | |
| it("does not dedupe accountless source evidence against an explicit cross-account delivery", async () => { | |
| deliverOutboundPayloadsMock.mockResolvedValue([{ channel: "slack", messageId: "msg-1" }]); | |
| const delivered = await deliverAgentCommandResultForTest({ | |
| opts: { | |
| replyAccountId: "other", | |
| runContext: { | |
| messageChannel: "slack", | |
| currentChannelId: "channel:C123", | |
| accountId: "work", | |
| }, | |
| }, | |
| payloads: [{ text: "Ready" }], | |
| sentTarget: { text: "Ready" }, | |
| }); | |
| expect(delivered.deliverySucceeded).toBe(true); | |
| expect(deliverOutboundPayloadsMock).toHaveBeenCalledTimes(1); | |
| expect(latestOutboundDeliveryArgs().accountId).toBe("other"); | |
| }); | |
| it("does not dedupe targetless cross-session messaging evidence", async () => { | |
| deliverOutboundPayloadsMock.mockResolvedValue([{ channel: "slack", messageId: "msg-1" }]); | |
| const delivered = await deliverAgentCommandResultForTest({ | |
| payloads: [{ text: "The image is ready." }], | |
| result: { | |
| didSendViaMessagingTool: true, | |
| messagingToolSentTexts: ["The image is ready."], | |
| }, | |
| }); | |
| expect(delivered.deliverySucceeded).toBe(true); | |
| expect(deliverOutboundPayloadsMock).toHaveBeenCalledTimes(1); | |
| expect(latestOutboundDeliveryArgs().payloads).toEqual([ | |
| expect.objectContaining({ text: "The image is ready." }), | |
| ]); | |
| }); | |
| it("keeps automatic delivery when message-tool media went to another target", async () => { | |
| deliverOutboundPayloadsMock.mockResolvedValue([{ channel: "slack", messageId: "msg-1" }]); | |
| const delivered = await deliverAgentCommandResultForTest({ | |
| payloads: [{ text: "The image is ready.", mediaUrls: ["/tmp/generated-image.png"] }], | |
| result: { | |
| messagingToolSentTexts: ["The image is ready."], | |
| messagingToolSentMediaUrls: ["/tmp/generated-image.png"], | |
| }, | |
| sentTarget: { | |
| to: "channel:OTHER", | |
| text: "The image is ready.", | |
| mediaUrls: ["/tmp/generated-image.png"], | |
| }, | |
| }); | |
| expect(delivered.deliverySucceeded).toBe(true); | |
| expect(deliverOutboundPayloadsMock).toHaveBeenCalledTimes(1); | |
| expect(latestOutboundDeliveryArgs().payloads).toEqual([ | |
| expect.objectContaining({ | |
| text: "The image is ready.", | |
| mediaUrls: ["/tmp/generated-image.png"], | |
| }), | |
| ]); | |
| }); | |
| it("adds sent deliveryStatus to JSON output after delivery completes", async () => { | |
| deliverOutboundPayloadsMock.mockResolvedValue([{ channel: "slack", messageId: "msg-1" }]); | |
| const runtime = { | |
| log: vi.fn(), | |
| error: vi.fn(), | |
| writeStdout: vi.fn(), | |
| writeJson: vi.fn(), | |
| }; | |
| const delivered = await deliverAgentCommandResultForTest({ | |
| workspace: true, | |
| runtime: runtime as never, | |
| opts: { | |
| message: "go", | |
| json: true, | |
| replyTo: "#general", | |
| }, | |
| outboundSession: { | |
| key: "agent:tester:slack:direct:alice", | |
| agentId: "tester", | |
| } as never, | |
| payloads: [{ text: "here you go" }], | |
| }); | |
| expect(runtime.writeJson).toHaveBeenCalledTimes(1); | |
| const json = latestJsonOutput(runtime); | |
| expect(Object.keys(json)).toEqual(["payloads", "meta", "deliveryStatus"]); | |
| expect(json).toMatchObject({ | |
| payloads: [{ text: "here you go", mediaUrl: null }], | |
| meta: { durationMs: 1 }, | |
| }); | |
| expect(json.meta).toBe(delivered.meta); | |
| expect(json.deliveryStatus).toEqual({ | |
| requested: true, | |
| attempted: true, | |
| status: "sent", | |
| succeeded: true, | |
| resultCount: 1, | |
| }); | |
| expect(delivered.deliverySucceeded).toBe(true); | |
| expect(delivered.deliveryStatus?.status).toBe("sent"); | |
| }); | |
| it("surfaces hook cancellation as a suppressed terminal deliveryStatus", async () => { | |
| deliverOutboundPayloadsMock.mockImplementationOnce(async (params: unknown) => { | |
| ( | |
| params as { | |
| onPayloadDeliveryOutcome?: (outcome: { | |
| index: number; | |
| status: "suppressed"; | |
| reason: "cancelled_by_message_sending_hook"; | |
| hookEffect: { cancelReason: string }; | |
| }) => void; | |
| } | |
| ).onPayloadDeliveryOutcome?.({ | |
| index: 0, | |
| status: "suppressed", | |
| reason: "cancelled_by_message_sending_hook", | |
| hookEffect: { cancelReason: "owned-by-other-agent" }, | |
| }); | |
| return []; | |
| }); | |
| const delivered = await deliverMediaReplyForTest({ | |
| key: "agent:tester:slack:direct:alice", | |
| agentId: "tester", | |
| } as never); | |
| expect(delivered.deliverySucceeded).toBe(true); | |
| const status = expectDeliveryStatusFields(delivered, { | |
| requested: true, | |
| attempted: true, | |
| status: "suppressed", | |
| succeeded: true, | |
| reason: "cancelled_by_message_sending_hook", | |
| }); | |
| expect(status.payloadOutcomes).toEqual([ | |
| { | |
| index: 0, | |
| status: "suppressed", | |
| reason: "cancelled_by_message_sending_hook", | |
| hookEffect: { cancelReason: "owned-by-other-agent" }, | |
| }, | |
| ]); | |
| }); | |
| it("surfaces durable partial failures without clearing delivery retry state", async () => { | |
| deliverOutboundPayloadsMock.mockImplementationOnce(async (params: unknown) => { | |
| ( | |
| params as { | |
| onPayloadDeliveryOutcome?: (outcome: { | |
| index: number; | |
| status: "failed"; | |
| error: Error; | |
| sentBeforeError: true; | |
| stage: "platform_send"; | |
| }) => void; | |
| } | |
| ).onPayloadDeliveryOutcome?.({ | |
| index: 1, | |
| status: "failed", | |
| error: new Error("second chunk failed"), | |
| sentBeforeError: true, | |
| stage: "platform_send", | |
| }); | |
| return [{ channel: "slack", messageId: "msg-1" }]; | |
| }); | |
| const delivered = await deliverMediaReplyForTest( | |
| { | |
| key: "agent:tester:slack:direct:alice", | |
| agentId: "tester", | |
| } as never, | |
| { bestEffortDeliver: true }, | |
| ); | |
| expect(delivered.deliverySucceeded).toBe(false); | |
| const status = expectDeliveryStatusFields(delivered, { | |
| requested: true, | |
| attempted: true, | |
| status: "partial_failed", | |
| succeeded: "partial", | |
| error: true, | |
| resultCount: 1, | |
| sentBeforeError: true, | |
| }); | |
| expect(String(status.errorMessage)).toContain("second chunk failed"); | |
| expect(status.payloadOutcomes).toHaveLength(1); | |
| const outcome = status.payloadOutcomes?.[0]; | |
| expect(outcome?.index).toBe(1); | |
| expect(outcome?.status).toBe("failed"); | |
| expect(String(outcome?.error)).toContain("second chunk failed"); | |
| expect(outcome?.sentBeforeError).toBe(true); | |
| expect(outcome?.stage).toBe("platform_send"); | |
| }); | |
| it("marks no-payload deliveryStatus as terminal delivery success", async () => { | |
| const delivered = await deliverAgentCommandResultForTest({ | |
| opts: { message: "go", replyTo: "#general" }, | |
| payloads: [], | |
| }); | |
| expect(delivered.deliverySucceeded).toBe(true); | |
| expectDeliveryStatusFields(delivered, { | |
| requested: true, | |
| attempted: false, | |
| status: "suppressed", | |
| succeeded: true, | |
| reason: "no_visible_payload", | |
| }); | |
| expect(deliverOutboundPayloadsMock).not.toHaveBeenCalled(); | |
| }); | |
| it("surfaces no-visible-payload deliveryStatus after payload normalization suppresses output", async () => { | |
| const delivered = await deliverAgentCommandResultForTest({ | |
| opts: { message: "go", replyTo: "#general" }, | |
| payloads: [{ text: "NO_REPLY" }], | |
| }); | |
| expect(delivered.payloads).toEqual([]); | |
| expect(delivered.deliverySucceeded).toBe(true); | |
| expectDeliveryStatusFields(delivered, { | |
| requested: true, | |
| attempted: false, | |
| status: "suppressed", | |
| succeeded: true, | |
| reason: "no_visible_payload", | |
| }); | |
| expect(deliverOutboundPayloadsMock).not.toHaveBeenCalled(); | |
| }); | |
| it("records channel transform suppression without calling outbound delivery", async () => { | |
| const transformReplyPayload = vi.fn(() => null); | |
| setActivePluginRegistry( | |
| createTestRegistry([ | |
| { | |
| pluginId: "slack", | |
| source: "test", | |
| plugin: { ...slackPluginForTest, messaging: { transformReplyPayload } }, | |
| }, | |
| ]), | |
| ); | |
| const delivered = await deliverAgentCommandResultForTest({ | |
| payloads: [{ text: "private reply" }], | |
| }); | |
| expect(delivered.payloads).toEqual([]); | |
| expect(delivered.deliverySucceeded).toBe(true); | |
| expectDeliveryStatusFields(delivered, { | |
| requested: true, | |
| attempted: false, | |
| status: "suppressed", | |
| succeeded: true, | |
| reason: "channel_transform", | |
| }); | |
| expect(deliverOutboundPayloadsMock).not.toHaveBeenCalled(); | |
| }); | |
| it("lets a later accepted payload enter durable delivery after an earlier transform veto", async () => { | |
| const transformReplyPayload = vi.fn(({ payload }: { payload: ReplyPayload }) => | |
| payload.text === "private reply" ? null : payload, | |
| ); | |
| setActivePluginRegistry( | |
| createTestRegistry([ | |
| { | |
| pluginId: "slack", | |
| source: "test", | |
| plugin: { ...slackPluginForTest, messaging: { transformReplyPayload } }, | |
| }, | |
| ]), | |
| ); | |
| deliverOutboundPayloadsMock.mockResolvedValue([{ channel: "slack", messageId: "msg-1" }]); | |
| const delivered = await deliverAgentCommandResultForTest({ | |
| payloads: [{ text: "private reply" }, { text: "public reply" }], | |
| }); | |
| expect(delivered.deliverySucceeded).toBe(true); | |
| expect(latestOutboundDeliveryArgs().payloads).toEqual([ | |
| expect.objectContaining({ text: "public reply" }), | |
| ]); | |
| }); | |
| it("preserves preflight deliveryStatus when best-effort delivery has no payloads", async () => { | |
| const runtime = { log: vi.fn(), error: vi.fn() }; | |
| const delivered = await deliverAgentCommandResultForTest({ | |
| runtime: runtime as never, | |
| opts: { | |
| message: "go", | |
| bestEffortDeliver: true, | |
| replyChannel: "not-installed", | |
| replyTo: "#general", | |
| }, | |
| payloads: [], | |
| }); | |
| expect(delivered.deliverySucceeded).toBeUndefined(); | |
| expectDeliveryStatusFields(delivered, { | |
| requested: true, | |
| attempted: false, | |
| status: "failed", | |
| succeeded: false, | |
| error: true, | |
| reason: "unknown_channel", | |
| }); | |
| expectRuntimeErrorIncludes(runtime, "Unknown channel"); | |
| expect(deliverOutboundPayloadsMock).not.toHaveBeenCalled(); | |
| }); | |
| it("emits JSON deliveryStatus before strict delivery failures rethrow", async () => { | |
| deliverOutboundPayloadsMock.mockRejectedValueOnce(new Error("Slack API timeout")); | |
| const events: string[] = []; | |
| const onDeliveryResult = vi.fn(() => events.push("captured")); | |
| const runtime = { | |
| log: vi.fn(), | |
| error: vi.fn(), | |
| writeStdout: vi.fn(), | |
| writeJson: vi.fn(() => events.push("json")), | |
| }; | |
| await expect( | |
| deliverAgentCommandResult({ | |
| cfg: { | |
| agents: { | |
| list: [{ id: "tester", workspace: "/tmp/agent-workspace" }], | |
| }, | |
| } as OpenClawConfig, | |
| deps: {} as CliDeps, | |
| runtime: runtime as never, | |
| opts: { | |
| message: "go", | |
| deliver: true, | |
| json: true, | |
| bestEffortDeliver: false, | |
| replyChannel: "slack", | |
| replyTo: "#general", | |
| } as AgentCommandOpts, | |
| outboundSession: { | |
| key: "agent:tester:slack:direct:alice", | |
| agentId: "tester", | |
| } as never, | |
| sessionEntry: undefined, | |
| payloads: [{ text: "here you go" }], | |
| result: createResult(), | |
| onDeliveryResult, | |
| }), | |
| ).rejects.toThrow("Slack API timeout"); | |
| expect(runtime.writeJson).toHaveBeenCalledTimes(1); | |
| const json = latestJsonOutput(runtime); | |
| expect(Object.keys(json)).toEqual(["payloads", "meta", "deliveryStatus"]); | |
| expect(json).toMatchObject({ | |
| payloads: [{ text: "here you go", mediaUrl: null }], | |
| meta: { durationMs: 1 }, | |
| }); | |
| expect(events).toEqual(["json", "captured"]); | |
| expect(json.deliveryStatus?.requested).toBe(true); | |
| expect(json.deliveryStatus?.attempted).toBe(true); | |
| expect(json.deliveryStatus?.status).toBe("failed"); | |
| expect(json.deliveryStatus?.succeeded).toBe(false); | |
| expect(json.deliveryStatus?.error).toBe(true); | |
| expect(String(json.deliveryStatus?.errorMessage)).toContain("Slack API timeout"); | |
| expect(onDeliveryResult).toHaveBeenCalledWith( | |
| expect.objectContaining({ | |
| deliveryStatus: expect.objectContaining({ status: "failed" }), | |
| }), | |
| ); | |
| }); | |
| it("emits JSON deliveryStatus before strict preflight failures rethrow", async () => { | |
| const runtime = { | |
| log: vi.fn(), | |
| error: vi.fn(), | |
| writeStdout: vi.fn(), | |
| writeJson: vi.fn(), | |
| }; | |
| deliverOutboundPayloadsMock.mockClear(); | |
| await expect( | |
| deliverAgentCommandResult({ | |
| cfg: { | |
| agents: { | |
| list: [{ id: "tester", workspace: "/tmp/agent-workspace" }], | |
| }, | |
| } as OpenClawConfig, | |
| deps: {} as CliDeps, | |
| runtime: runtime as never, | |
| opts: { | |
| message: "go", | |
| deliver: true, | |
| json: true, | |
| bestEffortDeliver: false, | |
| replyChannel: "not-installed", | |
| replyTo: "#general", | |
| } as AgentCommandOpts, | |
| outboundSession: { | |
| key: "agent:tester:not-installed:direct:alice", | |
| agentId: "tester", | |
| } as never, | |
| sessionEntry: undefined, | |
| payloads: [{ text: "here you go", mediaUrls: ["./out/photo.png"] }], | |
| result: createResult(), | |
| }), | |
| ).rejects.toThrow('Unknown channel "not-installed"'); | |
| expect(deliverOutboundPayloadsMock).not.toHaveBeenCalled(); | |
| expect(createReplyMediaPathNormalizerMock).not.toHaveBeenCalled(); | |
| expect(runtime.writeJson).toHaveBeenCalledTimes(1); | |
| const json = latestJsonOutput(runtime); | |
| expect(Object.keys(json)).toEqual(["payloads", "meta", "deliveryStatus"]); | |
| expect(json).toMatchObject({ | |
| payloads: [{ text: "here you go", mediaUrl: null, mediaUrls: ["./out/photo.png"] }], | |
| meta: { durationMs: 1 }, | |
| }); | |
| expect(json.deliveryStatus).toEqual({ | |
| requested: true, | |
| attempted: false, | |
| status: "failed", | |
| succeeded: false, | |
| error: true, | |
| reason: "unknown_channel", | |
| }); | |
| }); | |
| }); | |
| /* oxlint-disable max-lines -- TODO: split this grandfathered oversized file. */ | |