import { beforeEach, describe, expect, it, vi } from "vitest" ;
import { deliverAgentCommandResult } from "../agents/command/delivery.js" ;
import type { ReplyPayload } from "../auto-reply/types.js" ;
import type { CliDeps } from "../cli/deps.js" ;
import type { OpenClawConfig } from "../config/config.js" ;
import type { SessionEntry } from "../config/sessions.js" ;
import type { RuntimeEnv } from "../runtime.js" ;
const mocks = vi.hoisted(() => ({
deliverOutboundPayloads: vi.fn(async () => []),
getChannelPlugin: vi.fn(() => ({})),
resolveOutboundTarget: vi.fn(() => ({ ok: true as const , to: "+15551234567" })),
}));
vi.mock("../channels/plugins/index.js" , () => ({
getChannelPlugin: mocks.getChannelPlugin,
getLoadedChannelPlugin: mocks.getChannelPlugin,
normalizeChannelId: (value: string) => value,
}));
vi.mock("../infra/outbound/deliver.js" , () => ({
deliverOutboundPayloads: mocks.deliverOutboundPayloads,
}));
vi.mock("../infra/outbound/targets.js" , async () => {
const actual = await vi.importActual<typeof import ("../infra/outbound/targets.js" )>(
"../infra/outbound/targets.js" ,
);
return {
...actual,
resolveOutboundTarget: mocks.resolveOutboundTarget,
};
});
describe("deliverAgentCommandResult" , () => {
function createRuntime(): RuntimeEnv {
return {
log: vi.fn(),
error: vi.fn(),
} as unknown as RuntimeEnv;
}
function createResult(text = "hi" ) {
return {
payloads: [{ text }],
meta: { durationMs: 1 },
};
}
async function runDelivery(params: {
opts: Record<string, unknown>;
outboundSession?: { key?: string; agentId?: string };
sessionEntry?: SessionEntry;
runtime?: RuntimeEnv;
resultText?: string;
payloads?: ReplyPayload[];
}) {
const cfg = {} as OpenClawConfig;
const deps = {} as CliDeps;
const runtime = params.runtime ?? createRuntime();
const result = params.payloads
? {
payloads: params.payloads,
meta: { durationMs: 1 },
}
: createResult(params.resultText);
await deliverAgentCommandResult({
cfg,
deps,
runtime,
opts: params.opts as never,
outboundSession: params.outboundSession,
sessionEntry: params.sessionEntry,
result,
payloads: result.payloads,
});
return { runtime };
}
beforeEach(() => {
mocks.deliverOutboundPayloads.mockClear();
mocks.resolveOutboundTarget.mockClear();
});
it("prefers explicit accountId for outbound delivery" , async () => {
await runDelivery({
opts: {
message: "hello" ,
deliver: true ,
channel: "whatsapp" ,
accountId: "kev" ,
to: "+15551234567" ,
},
sessionEntry: {
lastAccountId: "default" ,
} as SessionEntry,
});
expect(mocks.deliverOutboundPayloads).toHaveBeenCalledWith(
expect.objectContaining({ accountId: "kev" }),
);
});
it("falls back to session accountId for implicit delivery" , async () => {
await runDelivery({
opts: {
message: "hello" ,
deliver: true ,
channel: "whatsapp" ,
},
sessionEntry: {
lastAccountId: "legacy" ,
lastChannel: "whatsapp" ,
} as SessionEntry,
});
expect(mocks.deliverOutboundPayloads).toHaveBeenCalledWith(
expect.objectContaining({ accountId: "legacy" }),
);
});
it("does not infer accountId for explicit delivery targets" , async () => {
await runDelivery({
opts: {
message: "hello" ,
deliver: true ,
channel: "whatsapp" ,
to: "+15551234567" ,
deliveryTargetMode: "explicit" ,
},
sessionEntry: {
lastAccountId: "legacy" ,
} as SessionEntry,
});
expect(mocks.resolveOutboundTarget).toHaveBeenCalledWith(
expect.objectContaining({ accountId: undefined, mode: "explicit" }),
);
expect(mocks.deliverOutboundPayloads).toHaveBeenCalledWith(
expect.objectContaining({ accountId: undefined }),
);
});
it("skips session accountId when channel differs" , async () => {
await runDelivery({
opts: {
message: "hello" ,
deliver: true ,
channel: "whatsapp" ,
},
sessionEntry: {
lastAccountId: "legacy" ,
lastChannel: "telegram" ,
} as SessionEntry,
});
expect(mocks.resolveOutboundTarget).toHaveBeenCalledWith(
expect.objectContaining({ accountId: undefined, channel: "whatsapp" }),
);
});
it("uses session last channel when none is provided" , async () => {
await runDelivery({
opts: {
message: "hello" ,
deliver: true ,
},
sessionEntry: {
lastChannel: "telegram" ,
lastTo: "123" ,
} as SessionEntry,
});
expect(mocks.resolveOutboundTarget).toHaveBeenCalledWith(
expect.objectContaining({ channel: "telegram" , to: "123" }),
);
});
it("uses reply overrides for delivery routing" , async () => {
await runDelivery({
opts: {
message: "hello" ,
deliver: true ,
to: "+15551234567" ,
replyTo: "#reports" ,
replyChannel: "slack" ,
replyAccountId: "ops" ,
},
sessionEntry: {
lastChannel: "telegram" ,
lastTo: "123" ,
lastAccountId: "legacy" ,
} as SessionEntry,
});
expect(mocks.resolveOutboundTarget).toHaveBeenCalledWith(
expect.objectContaining({ channel: "slack" , to: "#reports" , accountId: "ops" }),
);
});
it("uses runContext turn source over stale session last route" , async () => {
await runDelivery({
opts: {
message: "hello" ,
deliver: true ,
runContext: {
messageChannel: "whatsapp" ,
currentChannelId: "+15559876543" ,
accountId: "work" ,
},
},
sessionEntry: {
lastChannel: "slack" ,
lastTo: "U_WRONG" ,
lastAccountId: "wrong" ,
} as SessionEntry,
});
expect(mocks.resolveOutboundTarget).toHaveBeenCalledWith(
expect.objectContaining({ channel: "whatsapp" , to: "+15559876543" , accountId: "work" }),
);
});
it("does not reuse session lastTo when runContext source omits currentChannelId" , async () => {
await runDelivery({
opts: {
message: "hello" ,
deliver: true ,
runContext: {
messageChannel: "whatsapp" ,
},
},
sessionEntry: {
lastChannel: "slack" ,
lastTo: "U_WRONG" ,
} as SessionEntry,
});
expect(mocks.resolveOutboundTarget).toHaveBeenCalledWith(
expect.objectContaining({ channel: "whatsapp" , to: undefined }),
);
});
it("uses caller-provided outbound session context when opts.sessionKey is absent" , async () => {
await runDelivery({
opts: {
message: "hello" ,
deliver: true ,
channel: "whatsapp" ,
to: "+15551234567" ,
},
outboundSession: {
key: "agent:exec:hook:gmail:thread-1" ,
agentId: "exec" ,
},
});
expect(mocks.deliverOutboundPayloads).toHaveBeenCalledWith(
expect.objectContaining({
session: expect.objectContaining({
key: "agent:exec:hook:gmail:thread-1" ,
agentId: "exec" ,
}),
}),
);
});
it("prefixes nested agent outputs with context" , async () => {
const runtime = createRuntime();
await runDelivery({
runtime,
resultText: "ANNOUNCE_SKIP" ,
opts: {
message: "hello" ,
deliver: false ,
lane: "nested" ,
sessionKey: "agent:main:main" ,
runId: "run-announce" ,
messageChannel: "webchat" ,
},
sessionEntry: undefined,
});
expect(runtime.log).toHaveBeenCalledTimes(1 );
const line = String((runtime.log as ReturnType<typeof vi.fn>).mock.calls[0 ]?.[0 ]);
expect(line).toContain("[agent:nested]" );
expect(line).toContain("session=agent:main:main" );
expect(line).toContain("run=run-announce" );
expect(line).toContain("channel=webchat" );
expect(line).toContain("ANNOUNCE_SKIP" );
});
it("prefixes per-session nested lanes with the same nested log context (#67502)" , async () => {
const runtime = createRuntime();
await runDelivery({
runtime,
resultText: "ANNOUNCE_SKIP" ,
opts: {
message: "hello" ,
deliver: false ,
lane: "nested:agent:ebao-next:quietchat:channel:1" ,
sessionKey: "agent:ebao-next:quietchat:channel:1" ,
runId: "run-announce" ,
messageChannel: "webchat" ,
},
sessionEntry: undefined,
});
expect(runtime.log).toHaveBeenCalledTimes(1 );
const line = String((runtime.log as ReturnType<typeof vi.fn>).mock.calls[0 ]?.[0 ]);
expect(line).toContain("[agent:nested]" );
expect(line).toContain("session=agent:ebao-next:quietchat:channel:1" );
expect(line).toContain("ANNOUNCE_SKIP" );
});
it("preserves audioAsVoice in JSON output envelopes" , async () => {
const runtime = createRuntime();
await runDelivery({
runtime,
payloads: [{ text: "voice caption" , mediaUrl: "file:///tmp/clip.mp3", audioAsVoice: true }],
opts: {
message: "hello" ,
deliver: false ,
json: true ,
},
});
expect(runtime.log).toHaveBeenCalledTimes(1 );
expect(
JSON.parse(String((runtime.log as ReturnType<typeof vi.fn>).mock.calls[0 ]?.[0 ])),
).toEqual({
payloads: [
{
text: "voice caption" ,
mediaUrl: "file:///tmp/clip.mp3",
mediaUrls: ["file:///tmp/clip.mp3"],
audioAsVoice: true ,
},
],
meta: { durationMs: 1 },
});
});
});
Messung V0.5 in Prozent C=100 H=100 G=100
¤ Dauer der Verarbeitung: 0.16 Sekunden
(vorverarbeitet am 2026-06-09)
¤
*© Formatika GbR, Deutschland