Skip to content

Commit 844655c

Browse files
committed
tests: add boundary coverage for media delivery
1 parent 6142fc1 commit 844655c

File tree

10 files changed

+713
-1
lines changed

10 files changed

+713
-1
lines changed
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { beforeEach, describe, expect, it, vi } from "vitest";
2+
import { imessageOutbound } from "./outbound-adapter.js";
3+
4+
describe("imessageOutbound", () => {
5+
const cfg = {
6+
channels: {
7+
imessage: {
8+
mediaMaxMb: 3,
9+
},
10+
},
11+
};
12+
13+
const sendIMessage = vi.fn();
14+
15+
beforeEach(() => {
16+
sendIMessage.mockReset();
17+
});
18+
19+
it("forwards replyToId on direct text sends", async () => {
20+
sendIMessage.mockResolvedValueOnce({ messageId: "m-text" });
21+
22+
const result = await imessageOutbound.sendText!({
23+
cfg,
24+
to: "chat_id:12",
25+
text: "hello",
26+
accountId: "default",
27+
replyToId: "reply-1",
28+
deps: { sendIMessage },
29+
});
30+
31+
expect(sendIMessage).toHaveBeenCalledWith(
32+
"chat_id:12",
33+
"hello",
34+
expect.objectContaining({
35+
accountId: "default",
36+
replyToId: "reply-1",
37+
maxBytes: 3 * 1024 * 1024,
38+
}),
39+
);
40+
expect(result).toEqual({ channel: "imessage", messageId: "m-text" });
41+
});
42+
43+
it("forwards mediaLocalRoots on direct media sends", async () => {
44+
sendIMessage.mockResolvedValueOnce({ messageId: "m-media-local" });
45+
46+
const result = await imessageOutbound.sendMedia!({
47+
cfg,
48+
to: "chat_id:88",
49+
text: "caption",
50+
mediaUrl: "/tmp/workspace/pic.png",
51+
mediaLocalRoots: ["/tmp/workspace"],
52+
accountId: "acct-1",
53+
replyToId: "reply-2",
54+
deps: { sendIMessage },
55+
});
56+
57+
expect(sendIMessage).toHaveBeenCalledWith(
58+
"chat_id:88",
59+
"caption",
60+
expect.objectContaining({
61+
mediaUrl: "/tmp/workspace/pic.png",
62+
mediaLocalRoots: ["/tmp/workspace"],
63+
accountId: "acct-1",
64+
replyToId: "reply-2",
65+
maxBytes: 3 * 1024 * 1024,
66+
}),
67+
);
68+
expect(result).toEqual({ channel: "imessage", messageId: "m-media-local" });
69+
});
70+
});
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { beforeEach, describe, expect, it, vi } from "vitest";
2+
3+
const sendMessageSignalMock = vi.fn();
4+
5+
vi.mock("./send.js", () => ({
6+
sendMessageSignal: (...args: unknown[]) => sendMessageSignalMock(...args),
7+
}));
8+
9+
import { signalOutbound } from "./outbound-adapter.js";
10+
11+
describe("signalOutbound", () => {
12+
beforeEach(() => {
13+
sendMessageSignalMock.mockReset();
14+
});
15+
16+
it("formats media captions and forwards mediaLocalRoots", async () => {
17+
sendMessageSignalMock.mockResolvedValueOnce({ messageId: "sig-media" });
18+
19+
const result = await signalOutbound.sendFormattedMedia!({
20+
cfg: {} as never,
21+
to: "signal:+15551234567",
22+
text: "**bold** caption",
23+
mediaUrl: "/tmp/workspace/photo.png",
24+
mediaLocalRoots: ["/tmp/workspace"],
25+
accountId: "default",
26+
});
27+
28+
expect(sendMessageSignalMock).toHaveBeenCalledWith(
29+
"signal:+15551234567",
30+
"bold caption",
31+
expect.objectContaining({
32+
mediaUrl: "/tmp/workspace/photo.png",
33+
mediaLocalRoots: ["/tmp/workspace"],
34+
accountId: "default",
35+
textMode: "plain",
36+
textStyles: [{ start: 0, length: 4, style: "BOLD" }],
37+
}),
38+
);
39+
expect(result).toEqual({ channel: "signal", messageId: "sig-media" });
40+
});
41+
42+
it("formats markdown text into plain Signal chunks with styles", async () => {
43+
sendMessageSignalMock.mockResolvedValue({ messageId: "sig-text" });
44+
45+
const result = await signalOutbound.sendFormattedText!({
46+
cfg: {} as never,
47+
to: "signal:+15557654321",
48+
text: "hi _there_ **boss**",
49+
accountId: "default",
50+
});
51+
52+
expect(sendMessageSignalMock).toHaveBeenCalledTimes(1);
53+
expect(sendMessageSignalMock).toHaveBeenCalledWith(
54+
"signal:+15557654321",
55+
"hi there boss",
56+
expect.objectContaining({
57+
accountId: "default",
58+
textMode: "plain",
59+
textStyles: [
60+
{ start: 3, length: 5, style: "ITALIC" },
61+
{ start: 9, length: 4, style: "BOLD" },
62+
],
63+
}),
64+
);
65+
expect(result).toEqual([{ channel: "signal", messageId: "sig-text" }]);
66+
});
67+
});
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import { beforeEach, describe, expect, it, vi } from "vitest";
2+
3+
const sendMessageSlackMock = vi.fn();
4+
const hasHooksMock = vi.fn();
5+
const runMessageSendingMock = vi.fn();
6+
7+
vi.mock("./send.js", () => ({
8+
sendMessageSlack: (...args: unknown[]) => sendMessageSlackMock(...args),
9+
}));
10+
11+
vi.mock("openclaw/plugin-sdk/plugin-runtime", () => ({
12+
getGlobalHookRunner: () => ({
13+
hasHooks: (...args: unknown[]) => hasHooksMock(...args),
14+
runMessageSending: (...args: unknown[]) => runMessageSendingMock(...args),
15+
}),
16+
}));
17+
18+
import { slackOutbound } from "./outbound-adapter.js";
19+
20+
describe("slackOutbound", () => {
21+
const cfg = {
22+
channels: {
23+
slack: {
24+
botToken: "xoxb-test",
25+
appToken: "xapp-test",
26+
},
27+
},
28+
};
29+
30+
beforeEach(() => {
31+
sendMessageSlackMock.mockReset();
32+
hasHooksMock.mockReset();
33+
runMessageSendingMock.mockReset();
34+
hasHooksMock.mockReturnValue(false);
35+
});
36+
37+
it("sends payload media first, then finalizes with blocks", async () => {
38+
sendMessageSlackMock
39+
.mockResolvedValueOnce({ messageId: "m-media-1" })
40+
.mockResolvedValueOnce({ messageId: "m-media-2" })
41+
.mockResolvedValueOnce({ messageId: "m-final" });
42+
43+
const result = await slackOutbound.sendPayload!({
44+
cfg,
45+
to: "C123",
46+
text: "",
47+
payload: {
48+
text: "final text",
49+
mediaUrls: ["https://example.com/1.png", "https://example.com/2.png"],
50+
channelData: {
51+
slack: {
52+
blocks: [
53+
{
54+
type: "section",
55+
text: { type: "plain_text", text: "Block body" },
56+
},
57+
],
58+
},
59+
},
60+
},
61+
mediaLocalRoots: ["/tmp/workspace"],
62+
accountId: "default",
63+
});
64+
65+
expect(sendMessageSlackMock).toHaveBeenCalledTimes(3);
66+
expect(sendMessageSlackMock).toHaveBeenNthCalledWith(
67+
1,
68+
"C123",
69+
"",
70+
expect.objectContaining({
71+
cfg,
72+
mediaUrl: "https://example.com/1.png",
73+
mediaLocalRoots: ["/tmp/workspace"],
74+
}),
75+
);
76+
expect(sendMessageSlackMock).toHaveBeenNthCalledWith(
77+
2,
78+
"C123",
79+
"",
80+
expect.objectContaining({
81+
cfg,
82+
mediaUrl: "https://example.com/2.png",
83+
mediaLocalRoots: ["/tmp/workspace"],
84+
}),
85+
);
86+
expect(sendMessageSlackMock).toHaveBeenNthCalledWith(
87+
3,
88+
"C123",
89+
"final text",
90+
expect.objectContaining({
91+
cfg,
92+
blocks: [
93+
{
94+
type: "section",
95+
text: { type: "plain_text", text: "Block body" },
96+
},
97+
],
98+
}),
99+
);
100+
expect(result).toEqual({ channel: "slack", messageId: "m-final" });
101+
});
102+
103+
it("cancels sendMedia when message_sending hooks block it", async () => {
104+
hasHooksMock.mockReturnValue(true);
105+
runMessageSendingMock.mockResolvedValue({ cancel: true });
106+
107+
const result = await slackOutbound.sendMedia!({
108+
cfg,
109+
to: "C123",
110+
text: "caption",
111+
mediaUrl: "https://example.com/image.png",
112+
accountId: "default",
113+
replyToId: "1712000000.000001",
114+
});
115+
116+
expect(runMessageSendingMock).toHaveBeenCalledWith(
117+
{
118+
to: "C123",
119+
content: "caption",
120+
metadata: {
121+
threadTs: "1712000000.000001",
122+
channelId: "C123",
123+
mediaUrl: "https://example.com/image.png",
124+
},
125+
},
126+
{ channelId: "slack", accountId: "default" },
127+
);
128+
expect(sendMessageSlackMock).not.toHaveBeenCalled();
129+
expect(result).toMatchObject({
130+
channel: "slack",
131+
messageId: "cancelled-by-hook",
132+
meta: { cancelled: true },
133+
});
134+
});
135+
});
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { beforeEach, describe, expect, it, vi } from "vitest";
2+
import { telegramOutbound } from "./outbound-adapter.js";
3+
4+
describe("telegramOutbound", () => {
5+
const sendTelegram = vi.fn();
6+
7+
beforeEach(() => {
8+
sendTelegram.mockReset();
9+
});
10+
11+
it("forwards mediaLocalRoots in direct media sends", async () => {
12+
sendTelegram.mockResolvedValueOnce({ messageId: "tg-media" });
13+
14+
const result = await telegramOutbound.sendMedia!({
15+
cfg: {} as never,
16+
to: "12345",
17+
text: "hello",
18+
mediaUrl: "/tmp/image.png",
19+
mediaLocalRoots: ["/tmp/agent-root"],
20+
accountId: "ops",
21+
replyToId: "900",
22+
threadId: "12",
23+
deps: { sendTelegram },
24+
});
25+
26+
expect(sendTelegram).toHaveBeenCalledWith(
27+
"12345",
28+
"hello",
29+
expect.objectContaining({
30+
mediaUrl: "/tmp/image.png",
31+
mediaLocalRoots: ["/tmp/agent-root"],
32+
accountId: "ops",
33+
replyToMessageId: 900,
34+
messageThreadId: 12,
35+
textMode: "html",
36+
}),
37+
);
38+
expect(result).toEqual({ channel: "telegram", messageId: "tg-media" });
39+
});
40+
41+
it("sends payload media in sequence and keeps buttons on the first message only", async () => {
42+
sendTelegram
43+
.mockResolvedValueOnce({ messageId: "tg-1", chatId: "12345" })
44+
.mockResolvedValueOnce({ messageId: "tg-2", chatId: "12345" });
45+
46+
const result = await telegramOutbound.sendPayload!({
47+
cfg: {} as never,
48+
to: "12345",
49+
text: "",
50+
payload: {
51+
text: "Approval required",
52+
mediaUrls: ["https://example.com/1.jpg", "https://example.com/2.jpg"],
53+
channelData: {
54+
telegram: {
55+
quoteText: "quoted",
56+
buttons: [[{ text: "Allow Once", callback_data: "/approve abc allow-once" }]],
57+
},
58+
},
59+
},
60+
mediaLocalRoots: ["/tmp/media"],
61+
accountId: "ops",
62+
deps: { sendTelegram },
63+
});
64+
65+
expect(sendTelegram).toHaveBeenCalledTimes(2);
66+
expect(sendTelegram).toHaveBeenNthCalledWith(
67+
1,
68+
"12345",
69+
"Approval required",
70+
expect.objectContaining({
71+
mediaUrl: "https://example.com/1.jpg",
72+
mediaLocalRoots: ["/tmp/media"],
73+
quoteText: "quoted",
74+
buttons: [[{ text: "Allow Once", callback_data: "/approve abc allow-once" }]],
75+
}),
76+
);
77+
expect(sendTelegram).toHaveBeenNthCalledWith(
78+
2,
79+
"12345",
80+
"",
81+
expect.objectContaining({
82+
mediaUrl: "https://example.com/2.jpg",
83+
mediaLocalRoots: ["/tmp/media"],
84+
quoteText: "quoted",
85+
}),
86+
);
87+
expect((sendTelegram.mock.calls[1]?.[2] as Record<string, unknown>)?.buttons).toBeUndefined();
88+
expect(result).toEqual({ channel: "telegram", messageId: "tg-2", chatId: "12345" });
89+
});
90+
});

0 commit comments

Comments
 (0)