Files
openclaw/extensions/telegram/src/outbound-adapter.test.ts
Peter Steinberger f9b1ccc4fa refactor(plugin-sdk): discriminate delivery and exec results (#124979)
* refactor(plugin-sdk): discriminate delivery and exec results

* test(plugin-sdk): update delivery target expectations

* fix(outbound): preserve canonical delivery targets

* fix(irc): normalize message delivery target
2026-08-16 22:55:41 -07:00

928 lines
30 KiB
TypeScript

// Telegram tests cover outbound adapter plugin behavior.
import { expectDefined } from "@openclaw/normalization-core";
import { verifyDurableFinalCapabilityProofs } from "openclaw/plugin-sdk/channel-outbound";
import { beforeEach, describe, expect, it, vi } from "vitest";
const sendMessageTelegramMock = vi.fn();
const pinMessageTelegramMock = vi.fn();
const reactMessageTelegramMock = vi.fn();
const sendPollTelegramMock = vi.fn();
const sendLocationTelegramMock = vi.fn();
vi.mock("./send.js", () => ({
pinMessageTelegram: (...args: unknown[]) => pinMessageTelegramMock(...args),
reactMessageTelegram: (...args: unknown[]) => reactMessageTelegramMock(...args),
sendPollTelegram: (...args: unknown[]) => sendPollTelegramMock(...args),
sendLocationTelegram: (...args: unknown[]) => sendLocationTelegramMock(...args),
sendMessageTelegram: (...args: unknown[]) => sendMessageTelegramMock(...args),
}));
import { telegramOutbound } from "./outbound-adapter.js";
import { resolveTelegramPromptContextDeliverySignature } from "./prompt-context-projection.js";
type MockWithCalls = {
mock: { calls: unknown[][] };
};
function callOptionsAt(
mock: MockWithCalls,
index: number,
expectedTo: string,
expectedText: string,
): Record<string, unknown> {
const call = mock.mock.calls[index];
expect(call?.[0]).toBe(expectedTo);
expect(call?.[1]).toBe(expectedText);
const options = call?.[2];
if (
options === undefined ||
options === null ||
typeof options !== "object" ||
Array.isArray(options)
) {
throw new Error(`expected call ${index} to include options`);
}
return options as Record<string, unknown>;
}
function lastCallOptions(
mock: MockWithCalls,
expectedTo: string,
expectedText: string,
): Record<string, unknown> {
return callOptionsAt(mock, mock.mock.calls.length - 1, expectedTo, expectedText);
}
function callOptionsFromEnd(
mock: MockWithCalls,
offsetFromEnd: number,
expectedTo: string,
expectedText: string,
): Record<string, unknown> {
return callOptionsAt(mock, mock.mock.calls.length - offsetFromEnd, expectedTo, expectedText);
}
describe("telegramOutbound", () => {
beforeEach(() => {
pinMessageTelegramMock.mockReset();
reactMessageTelegramMock.mockReset();
sendPollTelegramMock.mockReset();
sendMessageTelegramMock.mockReset();
sendLocationTelegramMock.mockReset();
});
it("forwards workspace-scoped media access in direct media sends", async () => {
sendMessageTelegramMock.mockResolvedValueOnce({ messageId: "tg-media" });
const mediaReadFile = vi.fn(async (_filePath: string) => Buffer.from("chart"));
const workspaceDir = "/tmp/agent-root";
const mediaAccess = { localRoots: [workspaceDir], readFile: mediaReadFile, workspaceDir };
const result = await telegramOutbound.sendMedia!({
cfg: {} as never,
to: "12345",
text: "hello",
mediaUrl: "chart.png",
mediaAccess,
mediaLocalRoots: mediaAccess.localRoots,
mediaReadFile,
accountId: "ops",
replyToId: "900",
threadId: "12",
deps: { sendTelegram: sendMessageTelegramMock },
});
expect(sendMessageTelegramMock).toHaveBeenCalledWith("12345", "hello", {
cfg: {},
verbose: false,
messageThreadId: 12,
replyToMessageId: 900,
accountId: "ops",
silent: undefined,
gatewayClientScopes: undefined,
mediaUrl: "chart.png",
mediaAccess,
mediaLocalRoots: ["/tmp/agent-root"],
mediaReadFile,
forceDocument: false,
});
expect(lastCallOptions(sendMessageTelegramMock, "12345", "hello").mediaAccess).toBe(
mediaAccess,
);
expect(result).toEqual({ channel: "telegram", messageId: "tg-media" });
});
it("sends payload media in sequence and keeps buttons on the first message only", async () => {
sendMessageTelegramMock
.mockResolvedValueOnce({ messageId: "tg-1", chatId: "12345" })
.mockResolvedValueOnce({ messageId: "tg-2", chatId: "12345" });
const mediaAccess = { localRoots: ["/tmp/media"], workspaceDir: "/tmp/media" };
const result = await telegramOutbound.sendPayload!({
cfg: {} as never,
to: "12345",
text: "",
payload: {
text: "Approval required",
mediaUrls: ["chart.png", "chart-2.png"],
channelData: {
telegram: {
quoteText: "quoted",
buttons: [[{ text: "Allow Once", callback_data: "/approve abc allow-once" }]],
promptContextSource: {
transcriptMessageId: "assistant-media",
deliverySignature: resolveTelegramPromptContextDeliverySignature({
text: "Approval required",
mediaUrls: ["chart.png", "chart-2.png"],
}),
},
},
},
},
mediaAccess,
mediaLocalRoots: mediaAccess.localRoots,
accountId: "ops",
deps: { sendTelegram: sendMessageTelegramMock },
});
expect(sendMessageTelegramMock).toHaveBeenCalledTimes(2);
const firstOptions = callOptionsAt(sendMessageTelegramMock, 0, "12345", "Approval required");
expect(firstOptions.mediaUrl).toBe("chart.png");
expect(firstOptions.mediaAccess).toBe(mediaAccess);
expect(firstOptions.mediaLocalRoots).toEqual(["/tmp/media"]);
expect(firstOptions.quoteText).toBe("quoted");
expect(firstOptions.buttons).toEqual([
[{ text: "Allow Once", callback_data: "/approve abc allow-once" }],
]);
expect(firstOptions.promptContextProjectionPlan).toMatchObject({
cursor: {
source: {
transcriptMessageId: "assistant-media",
},
nextPartIndex: 0,
complete: true,
},
finalPart: false,
});
const secondOptions = callOptionsAt(sendMessageTelegramMock, 1, "12345", "");
expect(secondOptions.mediaUrl).toBe("chart-2.png");
expect(secondOptions.mediaAccess).toBe(mediaAccess);
expect(secondOptions.mediaLocalRoots).toEqual(["/tmp/media"]);
expect(secondOptions.quoteText).toBe("quoted");
expect(secondOptions.buttons).toBeUndefined();
expect(secondOptions.promptContextProjectionPlan).toMatchObject({
cursor: {
source: {
transcriptMessageId: "assistant-media",
},
nextPartIndex: 0,
complete: true,
},
finalPart: true,
});
expect((firstOptions.promptContextProjectionPlan as { cursor: unknown }).cursor).toBe(
(secondOptions.promptContextProjectionPlan as { cursor: unknown }).cursor,
);
expect(result).toEqual({
channel: "telegram",
messageId: "tg-2",
target: { kind: "chat", id: "12345" },
});
});
it.each([
["trailing empty", ["https://example.com/only.jpg", ""]],
["leading empty", ["", "https://example.com/only.jpg"]],
["whitespace", [" ", " https://example.com/only.jpg "]],
])("normalizes $0 media entries before first/final decisions", async (_name, mediaUrls) => {
sendMessageTelegramMock.mockResolvedValueOnce({ messageId: "tg-only", chatId: "12345" });
const buttons = [[{ text: "Open", callback_data: "open" }]];
await telegramOutbound.sendPayload!({
cfg: {} as never,
to: "12345",
text: "caption",
payload: {
text: "caption",
mediaUrls,
channelData: {
telegram: {
buttons,
promptContextSource: {
transcriptMessageId: "assistant-media",
deliverySignature: resolveTelegramPromptContextDeliverySignature({
text: "caption",
mediaUrls,
}),
},
},
},
},
deps: { sendTelegram: sendMessageTelegramMock },
});
const options = callOptionsAt(sendMessageTelegramMock, 0, "12345", "caption");
expect(sendMessageTelegramMock).toHaveBeenCalledTimes(1);
expect(options.mediaUrl).toBe("https://example.com/only.jpg");
expect(options.buttons).toEqual(buttons);
expect(options.promptContextProjectionPlan).toMatchObject({ finalPart: true });
});
it("uses interactive button labels as fallback text for button-only payloads", async () => {
sendMessageTelegramMock.mockResolvedValueOnce({ messageId: "tg-buttons", chatId: "12345" });
const result = await telegramOutbound.sendPayload!({
cfg: {} as never,
to: "12345",
text: "",
payload: {
interactive: {
blocks: [{ type: "buttons", buttons: [{ label: "Retry", value: "cmd:retry" }] }],
},
},
deps: { sendTelegram: sendMessageTelegramMock },
});
const options = callOptionsAt(sendMessageTelegramMock, 0, "12345", "- Retry");
expect(options.buttons).toEqual([[{ text: "Retry", callback_data: "cmd:retry" }]]);
expect(result).toEqual({
channel: "telegram",
messageId: "tg-buttons",
target: { kind: "chat", id: "12345" },
});
});
it("forwards prompt-context sources on durable payload sends", async () => {
sendMessageTelegramMock.mockResolvedValueOnce({ messageId: "tg-final", chatId: "12345" });
const result = await telegramOutbound.sendPayload!({
cfg: {} as never,
to: "12345",
text: "",
payload: {
text: "Final answer",
channelData: {
telegram: {
promptContextSource: {
transcriptMessageId: "assistant-final",
deliverySignature: resolveTelegramPromptContextDeliverySignature({
text: "Final answer",
}),
},
},
},
},
deps: { sendTelegram: sendMessageTelegramMock },
});
const options = callOptionsAt(sendMessageTelegramMock, 0, "12345", "Final answer");
expect(options.promptContextProjectionPlan).toMatchObject({
cursor: {
source: {
transcriptMessageId: "assistant-final",
},
nextPartIndex: 0,
complete: true,
},
finalPart: true,
});
expect(result).toEqual({
channel: "telegram",
messageId: "tg-final",
target: { kind: "chat", id: "12345" },
});
});
it("detaches stale prompt-context provenance after a durable hook rewrite", async () => {
sendMessageTelegramMock.mockResolvedValueOnce({ messageId: "tg-final", chatId: "12345" });
await telegramOutbound.sendPayload!({
cfg: {} as never,
to: "12345",
text: "",
payload: {
text: "Hook-rewritten answer",
channelData: {
telegram: {
promptContextSource: {
transcriptMessageId: "assistant-final",
deliverySignature: resolveTelegramPromptContextDeliverySignature({
text: "Original answer",
}),
},
},
},
},
deps: { sendTelegram: sendMessageTelegramMock },
});
const options = callOptionsAt(sendMessageTelegramMock, 0, "12345", "Hook-rewritten answer");
expect(options.promptContextProjectionPlan).toBeUndefined();
});
it("applies reaction-only payloads without sending empty Telegram text", async () => {
reactMessageTelegramMock.mockResolvedValueOnce({ ok: true });
const result = await telegramOutbound.sendPayload!({
cfg: {} as never,
to: "12345",
text: "",
replyToId: "777",
payload: {
channelData: {
telegram: {
reaction: { emoji: "🔥" },
},
},
},
deps: { sendTelegram: sendMessageTelegramMock },
});
expect(reactMessageTelegramMock).toHaveBeenCalledWith("12345", 777, "🔥", {
cfg: {},
verbose: false,
accountId: undefined,
gatewayClientScopes: undefined,
});
expect(sendMessageTelegramMock).not.toHaveBeenCalled();
expect(result).toEqual({
channel: "telegram",
messageId: "777",
target: { kind: "chat", id: "12345" },
});
});
it("applies reaction payloads before sending visible text", async () => {
reactMessageTelegramMock.mockResolvedValueOnce({ ok: true });
sendMessageTelegramMock.mockResolvedValueOnce({ messageId: "tg-text", chatId: "12345" });
await telegramOutbound.sendPayload!({
cfg: {} as never,
to: "12345",
text: "",
accountId: "ops",
replyToId: "777",
payload: {
text: "Done",
channelData: {
telegram: {
reaction: { emoji: "✅" },
},
},
},
deps: { sendTelegram: sendMessageTelegramMock },
});
expect(reactMessageTelegramMock).toHaveBeenCalledWith(
"12345",
777,
"✅",
expect.objectContaining({ accountId: "ops" }),
);
expect(sendMessageTelegramMock).toHaveBeenCalledWith(
"12345",
"Done",
expect.objectContaining({ accountId: "ops", replyToMessageId: 777 }),
);
expect(
expectDefined(
reactMessageTelegramMock.mock.invocationCallOrder[0],
"Telegram reaction invocation",
),
).toBeLessThan(
expectDefined(
sendMessageTelegramMock.mock.invocationCallOrder[0],
"Telegram send invocation",
),
);
});
it("rejects text plus reaction payloads without a reply target", async () => {
await expect(
telegramOutbound.sendPayload!({
cfg: {} as never,
to: "12345",
text: "",
payload: {
text: "Done",
channelData: { telegram: { reaction: { emoji: "🔥" } } },
},
deps: { sendTelegram: sendMessageTelegramMock },
}),
).rejects.toThrow("Telegram reaction requires a reply target");
expect(reactMessageTelegramMock).not.toHaveBeenCalled();
expect(sendMessageTelegramMock).not.toHaveBeenCalled();
});
it("rejects text plus reaction payloads when Telegram refuses the emoji", async () => {
reactMessageTelegramMock.mockResolvedValueOnce({
ok: false,
warning: "Reaction unavailable: not-supported",
});
await expect(
telegramOutbound.sendPayload!({
cfg: {} as never,
to: "12345",
text: "",
replyToId: "777",
payload: {
text: "Done",
channelData: { telegram: { reaction: { emoji: "not-supported" } } },
},
deps: { sendTelegram: sendMessageTelegramMock },
}),
).rejects.toThrow("Reaction unavailable: not-supported");
expect(sendMessageTelegramMock).not.toHaveBeenCalled();
});
it("rejects reaction-only payloads without a reply target", async () => {
await expect(
telegramOutbound.sendPayload!({
cfg: {} as never,
to: "12345",
text: "",
payload: {
channelData: { telegram: { reaction: { emoji: "🔥" } } },
},
deps: { sendTelegram: sendMessageTelegramMock },
}),
).rejects.toThrow("Telegram reaction requires a reply target");
expect(reactMessageTelegramMock).not.toHaveBeenCalled();
expect(sendMessageTelegramMock).not.toHaveBeenCalled();
});
it("rejects reaction-only payloads when Telegram refuses the emoji", async () => {
reactMessageTelegramMock.mockResolvedValueOnce({
ok: false,
warning: "Reaction unavailable: not-supported",
});
await expect(
telegramOutbound.sendPayload!({
cfg: {} as never,
to: "12345",
text: "",
replyToId: "777",
payload: {
channelData: { telegram: { reaction: { emoji: "not-supported" } } },
},
deps: { sendTelegram: sendMessageTelegramMock },
}),
).rejects.toThrow("Reaction unavailable: not-supported");
expect(sendMessageTelegramMock).not.toHaveBeenCalled();
});
it("forwards silent delivery options to Telegram sends", async () => {
sendMessageTelegramMock.mockResolvedValueOnce({ messageId: "tg-silent", chatId: "12345" });
const result = await telegramOutbound.sendPayload!({
cfg: {} as never,
to: "12345",
text: "quiet",
payload: { text: "quiet" },
silent: true,
deps: { sendTelegram: sendMessageTelegramMock },
});
const options = callOptionsAt(sendMessageTelegramMock, 0, "12345", "quiet");
expect(options.silent).toBe(true);
expect(result).toEqual({
channel: "telegram",
messageId: "tg-silent",
target: { kind: "chat", id: "12345" },
});
});
it("does not plain-text sanitize Telegram HTML before durable delivery", async () => {
sendMessageTelegramMock.mockResolvedValueOnce({ messageId: "tg-html", chatId: "12345" });
await telegramOutbound.sendText!({
cfg: {} as never,
to: "12345",
text: "<b>Morning</b> <code>oauth2</code>",
deps: { sendTelegram: sendMessageTelegramMock },
});
const options = callOptionsAt(
sendMessageTelegramMock,
0,
"12345",
"<b>Morning</b> <code>oauth2</code>",
);
expect(options.textMode).toBeUndefined();
});
it("normalizes legacy durable group retry targets before Telegram sends", async () => {
sendMessageTelegramMock.mockResolvedValueOnce({
messageId: "tg-group-retry",
chatId: "-1001234567890",
});
await telegramOutbound.sendText!({
cfg: {} as never,
to: "group:-1001234567890",
text: "retry reminder",
deps: { sendTelegram: sendMessageTelegramMock },
});
lastCallOptions(sendMessageTelegramMock, "-1001234567890", "retry reminder");
});
it("keeps numeric durable retry targets unchanged", async () => {
sendMessageTelegramMock.mockResolvedValueOnce({
messageId: "tg-direct-retry",
chatId: "123456789",
});
await telegramOutbound.sendText!({
cfg: {} as never,
to: "123456789",
text: "retry direct",
deps: { sendTelegram: sendMessageTelegramMock },
});
lastCallOptions(sendMessageTelegramMock, "123456789", "retry direct");
});
it("normalizes legacy durable group retry targets with topic suffixes", async () => {
sendMessageTelegramMock.mockResolvedValueOnce({
messageId: "tg-topic-retry",
chatId: "-1001234567890",
});
await telegramOutbound.sendPayload!({
cfg: {} as never,
to: "group:-1001234567890:topic:77",
text: "",
payload: { text: "topic retry" },
deps: { sendTelegram: sendMessageTelegramMock },
});
lastCallOptions(sendMessageTelegramMock, "-1001234567890:topic:77", "topic retry");
});
it("does not make non-numeric legacy group targets look valid", async () => {
sendMessageTelegramMock.mockResolvedValueOnce({
messageId: "tg-invalid-retry",
chatId: "group:not-a-number",
});
await telegramOutbound.sendText!({
cfg: {} as never,
to: "group:not-a-number",
text: "bad retry target",
deps: { sendTelegram: sendMessageTelegramMock },
});
lastCallOptions(sendMessageTelegramMock, "group:not-a-number", "bad retry target");
});
it("normalizes legacy durable group retry topic targets before Telegram polls", async () => {
sendPollTelegramMock.mockResolvedValueOnce({
messageId: "tg-poll-retry",
chatId: "-1001234567890",
});
await telegramOutbound.sendPoll?.({
cfg: {} as never,
to: "group:-1001234567890:topic:77",
poll: { question: "Retry?", options: ["Yes", "No"] },
accountId: "ops",
});
expect(sendPollTelegramMock).toHaveBeenCalledWith(
"-1001234567890:topic:77",
{ question: "Retry?", options: ["Yes", "No"] },
{
cfg: {},
accountId: "ops",
messageThreadId: undefined,
silent: undefined,
isAnonymous: undefined,
gatewayClientScopes: undefined,
},
);
});
it("forwards audioAsVoice payload media to Telegram voice sends", async () => {
sendMessageTelegramMock.mockResolvedValueOnce({ messageId: "tg-voice", chatId: "12345" });
const result = await telegramOutbound.sendPayload!({
cfg: {} as never,
to: "12345",
text: "",
payload: {
text: "voice caption",
mediaUrl: "file:///tmp/note.ogg",
audioAsVoice: true,
},
deps: { sendTelegram: sendMessageTelegramMock },
});
const options = callOptionsAt(sendMessageTelegramMock, 0, "12345", "voice caption");
expect(options.mediaUrl).toBe("file:///tmp/note.ogg");
expect(options.asVoice).toBe(true);
expect(result).toEqual({
channel: "telegram",
messageId: "tg-voice",
target: { kind: "chat", id: "12345" },
});
});
it("forwards videoAsNote payload media to Telegram video-note sends", async () => {
sendMessageTelegramMock.mockResolvedValueOnce({ messageId: "tg-video-note", chatId: "12345" });
const result = await telegramOutbound.sendPayload!({
cfg: {} as never,
to: "12345",
text: "",
payload: {
mediaUrl: "file:///tmp/note.mp4",
videoAsNote: true,
},
deps: { sendTelegram: sendMessageTelegramMock },
});
const options = callOptionsAt(sendMessageTelegramMock, 0, "12345", "");
expect(options.mediaUrl).toBe("file:///tmp/note.mp4");
expect(options.asVideoNote).toBe(true);
expect(result).toEqual({
channel: "telegram",
messageId: "tg-video-note",
target: { kind: "chat", id: "12345" },
});
});
it.each([
{ name: "no attachment", mediaUrls: [] },
{
name: "multiple attachments",
mediaUrls: ["file:///tmp/one.mp4", "file:///tmp/two.mp4"],
},
])("rejects video-note payloads with $name", async ({ mediaUrls }) => {
await expect(
telegramOutbound.sendPayload!({
cfg: {} as never,
to: "12345",
text: "",
payload: { mediaUrls, videoAsNote: true },
deps: { sendTelegram: sendMessageTelegramMock },
}),
).rejects.toThrow("Telegram video notes require exactly one media attachment.");
expect(sendMessageTelegramMock).not.toHaveBeenCalled();
});
it("maps portable locations to Telegram native sends", async () => {
sendLocationTelegramMock.mockResolvedValueOnce({
messageId: "tg-location",
chatId: "12345",
});
const location = {
latitude: 48.858844,
longitude: 2.294351,
name: "Eiffel Tower",
address: "Champ de Mars",
};
const promptContextSource = {
transcriptMessageId: "assistant-location",
deliverySignature: resolveTelegramPromptContextDeliverySignature({ location }),
};
const result = await telegramOutbound.sendPayload!({
cfg: {} as never,
to: "12345",
text: "",
payload: {
location,
channelData: {
telegram: { quoteText: "quoted location", promptContextSource },
},
},
accountId: "ops",
replyToId: "41",
});
expect(sendLocationTelegramMock).toHaveBeenCalledWith(
"12345",
location,
expect.objectContaining({
cfg: {},
accountId: "ops",
buttons: undefined,
quoteText: "quoted location",
replyToMessageId: 41,
promptContextProjectionPlan: expect.objectContaining({
finalPart: true,
cursor: expect.objectContaining({
source: { transcriptMessageId: "assistant-location" },
}),
}),
}),
);
expect(sendMessageTelegramMock).not.toHaveBeenCalled();
expect(result).toEqual({
channel: "telegram",
messageId: "tg-location",
target: { kind: "chat", id: "12345" },
});
});
it("sends cross-context location markers before the native location", async () => {
sendMessageTelegramMock.mockResolvedValueOnce({
messageId: "tg-marker",
chatId: "12345",
});
sendLocationTelegramMock.mockResolvedValueOnce({
messageId: "tg-location",
chatId: "12345",
});
const location = { latitude: 1, longitude: 2 };
const result = await telegramOutbound.sendPayload!({
cfg: {} as never,
to: "12345",
text: "[from telegram:origin] ",
payload: {
text: "[from telegram:origin] ",
location,
channelData: { telegram: { quoteText: "quoted location" } },
},
replyToId: "41",
});
expect(sendMessageTelegramMock).toHaveBeenCalledWith(
"12345",
"[from telegram:origin] ",
expect.objectContaining({
replyToMessageId: undefined,
replyToIdSource: undefined,
replyToMode: undefined,
}),
);
expect(sendLocationTelegramMock).toHaveBeenCalledWith(
"12345",
location,
expect.objectContaining({
quoteText: "quoted location",
replyToMessageId: 41,
}),
);
expect(result).toEqual({
channel: "telegram",
messageId: "tg-location",
target: { kind: "chat", id: "12345" },
});
});
it("backs declared durable final capabilities with delivery proofs", async () => {
const proveText = async () => {
sendMessageTelegramMock.mockResolvedValueOnce({ messageId: "tg-text", chatId: "12345" });
await telegramOutbound.sendText!({
cfg: {} as never,
to: "12345",
text: "hello",
formatting: { parseMode: "HTML", tableMode: "bullets" },
deps: { sendTelegram: sendMessageTelegramMock },
});
const options = lastCallOptions(sendMessageTelegramMock, "12345", "hello");
expect(options.textMode).toBe("html");
expect(options.tableMode).toBe("bullets");
};
const proveMedia = async () => {
sendMessageTelegramMock.mockResolvedValueOnce({ messageId: "tg-media", chatId: "12345" });
await telegramOutbound.sendMedia!({
cfg: {} as never,
to: "12345",
text: "caption",
mediaUrl: "https://example.com/a.png",
deps: { sendTelegram: sendMessageTelegramMock },
});
const options = lastCallOptions(sendMessageTelegramMock, "12345", "caption");
expect(options.mediaUrl).toBe("https://example.com/a.png");
};
const provePayload = async () => {
sendMessageTelegramMock.mockResolvedValueOnce({ messageId: "tg-payload", chatId: "12345" });
await telegramOutbound.sendPayload!({
cfg: {} as never,
to: "12345",
text: "",
payload: { text: "payload" },
deps: { sendTelegram: sendMessageTelegramMock },
});
lastCallOptions(sendMessageTelegramMock, "12345", "payload");
};
const proveReplyThreadSilent = async () => {
sendMessageTelegramMock.mockResolvedValueOnce({ messageId: "tg-thread", chatId: "12345" });
await telegramOutbound.sendText!({
cfg: {} as never,
to: "12345",
text: "threaded",
replyToId: "900",
threadId: "12",
silent: true,
deps: { sendTelegram: sendMessageTelegramMock },
});
const options = lastCallOptions(sendMessageTelegramMock, "12345", "threaded");
expect(options.replyToMessageId).toBe(900);
expect(options.messageThreadId).toBe(12);
expect(options.silent).toBe(true);
};
const proveBatch = async () => {
sendMessageTelegramMock
.mockResolvedValueOnce({ messageId: "tg-batch-1", chatId: "12345" })
.mockResolvedValueOnce({ messageId: "tg-batch-2", chatId: "12345" });
await telegramOutbound.sendPayload!({
cfg: {} as never,
to: "12345",
text: "",
payload: {
text: "batch",
mediaUrls: ["https://example.com/a.png", "https://example.com/b.png"],
},
deps: { sendTelegram: sendMessageTelegramMock },
});
const firstOptions = callOptionsFromEnd(sendMessageTelegramMock, 2, "12345", "batch");
expect(firstOptions.mediaUrl).toBe("https://example.com/a.png");
const secondOptions = callOptionsFromEnd(sendMessageTelegramMock, 1, "12345", "");
expect(secondOptions.mediaUrl).toBe("https://example.com/b.png");
};
await verifyDurableFinalCapabilityProofs({
adapterName: "telegramOutbound",
capabilities: telegramOutbound.deliveryCapabilities?.durableFinal,
proofs: {
text: proveText,
media: proveMedia,
payload: provePayload,
silent: proveReplyThreadSilent,
replyTo: proveReplyThreadSilent,
thread: proveReplyThreadSilent,
messageSendingHooks: () => {
expect(telegramOutbound.sendText).toBeTypeOf("function");
},
batch: proveBatch,
},
});
});
it("passes delivery pin notify requests to Telegram pinning", async () => {
pinMessageTelegramMock.mockResolvedValueOnce({ ok: true, messageId: "tg-1", chatId: "12345" });
await telegramOutbound.pinDeliveredMessage?.({
cfg: {} as never,
target: { channel: "telegram", to: "12345", accountId: "ops" },
messageId: "tg-1",
pin: { enabled: true, notify: true },
gatewayClientScopes: ["operator.write"],
});
const options = callOptionsAt(pinMessageTelegramMock, 0, "12345", "tg-1");
expect(options.accountId).toBe("ops");
expect(options.notify).toBe(true);
expect(options.verbose).toBe(false);
expect(options.gatewayClientScopes).toEqual(["operator.write"]);
});
it("normalizes legacy durable group retry targets before Telegram pinning", async () => {
pinMessageTelegramMock.mockResolvedValueOnce({
ok: true,
messageId: "tg-group-retry",
chatId: "-1001234567890",
});
await telegramOutbound.pinDeliveredMessage?.({
cfg: {} as never,
target: { channel: "telegram", to: "group:-1001234567890", accountId: "ops" },
messageId: "tg-group-retry",
pin: { enabled: true, notify: false },
});
const options = callOptionsAt(pinMessageTelegramMock, 0, "-1001234567890", "tg-group-retry");
expect(options.accountId).toBe("ops");
expect(options.notify).toBe(false);
});
it("normalizes legacy durable group retry topic targets before Telegram pinning", async () => {
pinMessageTelegramMock.mockResolvedValueOnce({
ok: true,
messageId: "tg-topic-retry",
chatId: "-1001234567890",
});
await telegramOutbound.pinDeliveredMessage?.({
cfg: {} as never,
target: { channel: "telegram", to: "group:-1001234567890:topic:77", accountId: "ops" },
messageId: "tg-topic-retry",
pin: { enabled: true, notify: false },
});
const options = callOptionsAt(pinMessageTelegramMock, 0, "-1001234567890", "tg-topic-retry");
expect(options.accountId).toBe("ops");
expect(options.notify).toBe(false);
});
});