Files
openclaw/src/commands/agent.delivery.e2e.test.ts
2026-02-17 15:50:07 +09:00

218 lines
5.8 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from "vitest";
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,
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>;
sessionEntry?: SessionEntry;
runtime?: RuntimeEnv;
resultText?: string;
}) {
const cfg = {} as OpenClawConfig;
const deps = {} as CliDeps;
const runtime = params.runtime ?? createRuntime();
const result = createResult(params.resultText);
const { deliverAgentCommandResult } = await import("./agent/delivery.js");
await deliverAgentCommandResult({
cfg,
deps,
runtime,
opts: params.opts as never,
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("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");
});
});