218 lines
5.8 KiB
TypeScript
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");
|
|
});
|
|
});
|