mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 05:16:23 -06:00
fix(telegram): surface media failures in the agent-facing body (#130849)
This commit is contained in:
committed by
GitHub
parent
c52cf19bc6
commit
bdd8f33f5d
@@ -830,6 +830,7 @@ curl "https://api.telegram.org/bot<bot_token>/getUpdates"
|
||||
<Accordion title="Limits and CLI targets">
|
||||
- `channels.telegram.textChunkLimit` default 4000; `streaming.chunkMode="newline"` prefers paragraph boundaries (blank lines) before length splitting.
|
||||
- `channels.telegram.mediaMaxMb` (default 100) caps inbound and outbound media size.
|
||||
- When an inbound attachment cannot be downloaded and the message proceeds to the agent, its body includes a `[media unavailable: ...]` notice. Oversize notices include the effective size limit; partial albums include the failed and total attachment counts. This also applies to admitted channel posts, even when their separate chat warning is suppressed.
|
||||
- group context history uses `channels.telegram.historyLimit` or `messages.groupChat.historyLimit` (default 50); `0` disables.
|
||||
- reply/quote/forward supplemental context normalizes into one selected conversation context window when the gateway has observed the parent messages; the observed-message cache lives in OpenClaw SQLite plugin state, and `openclaw doctor --fix` imports legacy sidecars. Telegram only includes one shallow `reply_to_message` per update, so chains older than the cache are limited to that payload.
|
||||
- Telegram allowlists primarily gate who can trigger the agent, not a full supplemental-context redaction boundary.
|
||||
|
||||
@@ -354,10 +354,6 @@ export function createTelegramInboundMedia({
|
||||
}
|
||||
// Classic polling cannot replay a failed album; retain its existing partial-delivery path.
|
||||
runtime.log?.(warn(`media group: skipping photo that failed to fetch: ${String(error)}`));
|
||||
allMedia.push({ kind: nativeKind, sourceMessageId });
|
||||
selection.set(sourceMessageId, "exclude");
|
||||
skippedCount++;
|
||||
continue;
|
||||
}
|
||||
if (media) {
|
||||
await recordMessageResolvedMedia({ msg, media, botUserId: ctx.me?.id });
|
||||
@@ -372,7 +368,11 @@ export function createTelegramInboundMedia({
|
||||
materializedCount++;
|
||||
selection.set(sourceMessageId, "include");
|
||||
} else {
|
||||
allMedia.push({ kind: nativeKind, sourceMessageId });
|
||||
allMedia.push({
|
||||
kind: nativeKind,
|
||||
sourceMessageId,
|
||||
unavailable: { reason: "download-failed" },
|
||||
});
|
||||
selection.set(sourceMessageId, "exclude");
|
||||
skippedCount++;
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ import type {
|
||||
import type {
|
||||
TelegramAmbientTranscriptWatermark,
|
||||
TelegramChannelIngressResolver,
|
||||
TelegramMediaRef,
|
||||
} from "./bot-message-context.types.js";
|
||||
import {
|
||||
isTelegramSpooledReplayUpdate,
|
||||
@@ -240,6 +241,7 @@ export function createTelegramInboundProcessing({
|
||||
const nativeMedia = resolveTelegramPrimaryMedia(msg);
|
||||
const mediaRuntime = resolveMediaRuntime();
|
||||
let media: Awaited<ReturnType<typeof resolveMedia>> = null;
|
||||
let unavailable: TelegramMediaRef["unavailable"];
|
||||
try {
|
||||
media = await resolveMedia({
|
||||
ctx,
|
||||
@@ -268,11 +270,12 @@ export function createTelegramInboundProcessing({
|
||||
return { kind: "ignored" };
|
||||
}
|
||||
if (isMediaSizeLimitError(mediaErr)) {
|
||||
const limitMb =
|
||||
mediaErr instanceof TelegramBotApiFileTooLargeError
|
||||
? Math.min(mediaErr.limitMb, Math.round(mediaMaxBytes / (1024 * 1024)))
|
||||
: Math.round(mediaMaxBytes / (1024 * 1024));
|
||||
unavailable = { reason: "oversize", limitMb };
|
||||
if (sendOversizeWarning && mediaDisposition !== "silent-ingest") {
|
||||
const limitMb =
|
||||
mediaErr instanceof TelegramBotApiFileTooLargeError
|
||||
? Math.min(mediaErr.limitMb, Math.round(mediaMaxBytes / (1024 * 1024)))
|
||||
: Math.round(mediaMaxBytes / (1024 * 1024));
|
||||
await withTelegramApiErrorLogging({
|
||||
operation: "sendMessage",
|
||||
runtime,
|
||||
@@ -295,6 +298,7 @@ export function createTelegramInboundProcessing({
|
||||
releaseDispatchDedupeClaims(dispatchDedupeClaims, mediaErr);
|
||||
return { kind: "ignored" };
|
||||
}
|
||||
unavailable = { reason: "download-failed" };
|
||||
if (mediaDisposition !== "silent-ingest") {
|
||||
await withTelegramApiErrorLogging({
|
||||
operation: "sendMessage",
|
||||
@@ -322,7 +326,7 @@ export function createTelegramInboundProcessing({
|
||||
kind: media.kind,
|
||||
stickerMetadata: media.stickerMetadata,
|
||||
}
|
||||
: { kind: nativeMedia.kind },
|
||||
: { kind: nativeMedia.kind, unavailable },
|
||||
]
|
||||
: [];
|
||||
const conversationKey = buildTelegramInboundDebounceConversationKey({
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
type BuiltChannelInboundEventContext,
|
||||
formatMediaPlaceholderText,
|
||||
formatInboundEnvelope,
|
||||
formatInboundMediaUnavailableText,
|
||||
resolveEnvelopeFormatOptions,
|
||||
toLocationContext,
|
||||
type NormalizedLocation,
|
||||
@@ -483,6 +484,24 @@ export async function buildTelegramInboundContextPayload(params: {
|
||||
forwardedFrom: visibleForwardOrigin?.from,
|
||||
forwardedDate: visibleForwardOrigin?.date ? visibleForwardOrigin.date * 1000 : undefined,
|
||||
});
|
||||
// Record terminal download outcomes after body assembly, including buffered forwards.
|
||||
// Missing paths alone also describe intentionally unsupported media; raw commands stay untouched.
|
||||
const unavailableMedia = allMedia.flatMap((media) =>
|
||||
media.unavailable ? [media.unavailable] : [],
|
||||
);
|
||||
const unavailableReason =
|
||||
allMedia.length > 1
|
||||
? `${unavailableMedia.length} of ${allMedia.length} attachments could not be downloaded`
|
||||
: unavailableMedia[0]?.reason === "oversize"
|
||||
? `file exceeds ${unavailableMedia[0].limitMb}MB limit`
|
||||
: "download failed";
|
||||
const appendMediaUnavailableNotice = (text: string) =>
|
||||
unavailableMedia.length > 0
|
||||
? formatInboundMediaUnavailableText({
|
||||
body: text,
|
||||
notice: `[media unavailable: ${unavailableReason}]`,
|
||||
})
|
||||
: text;
|
||||
const replySuffix =
|
||||
visibleReplyChain.length > 0
|
||||
? `\n\n[Reply chain - nearest first]\n${visibleReplyChain
|
||||
@@ -536,7 +555,7 @@ export async function buildTelegramInboundContextPayload(params: {
|
||||
channel: "Telegram",
|
||||
from: conversationLabel,
|
||||
timestamp: msg.date ? msg.date * 1000 : undefined,
|
||||
body: `${visibleBodyText}${replySuffix}`,
|
||||
body: `${appendMediaUnavailableNotice(visibleBodyText)}${replySuffix}`,
|
||||
chatType: isGroup ? "group" : "direct",
|
||||
sender: {
|
||||
name: senderName,
|
||||
@@ -714,7 +733,9 @@ export async function buildTelegramInboundContextPayload(params: {
|
||||
inboundEventKind,
|
||||
body,
|
||||
rawBody,
|
||||
bodyForAgent: shouldRenderBufferedBody ? visibleBodyText : bodyText,
|
||||
bodyForAgent: appendMediaUnavailableNotice(
|
||||
shouldRenderBufferedBody ? visibleBodyText : bodyText,
|
||||
),
|
||||
commandBody,
|
||||
inboundHistory,
|
||||
sourceModality: msg.voice ? "voice" : undefined,
|
||||
|
||||
@@ -27,6 +27,7 @@ export type TelegramMediaRef = {
|
||||
fileName?: string;
|
||||
stickerMetadata?: StickerMetadata;
|
||||
sourceMessageId?: string;
|
||||
unavailable?: { reason: "oversize"; limitMb: number } | { reason: "download-failed" };
|
||||
};
|
||||
|
||||
export type TelegramChannelIngressResolver = (
|
||||
|
||||
@@ -216,10 +216,15 @@ function replyPayload(): Record<string, unknown> {
|
||||
return call[0] as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function expectTypeOnlyMediaPayload(kind: string, rawBody = "") {
|
||||
function expectUnavailableMediaPayload(
|
||||
kind: string,
|
||||
rawBody = "",
|
||||
notice = "[media unavailable: download failed]",
|
||||
) {
|
||||
const payload = replyPayload();
|
||||
expect(payload).toMatchObject({
|
||||
BodyForAgent: rawBody,
|
||||
Body: expect.stringContaining(notice),
|
||||
BodyForAgent: [rawBody, notice].filter(Boolean).join("\n\n"),
|
||||
media: [expect.objectContaining({ kind })],
|
||||
RawBody: rawBody,
|
||||
});
|
||||
@@ -524,7 +529,7 @@ describe("createTelegramBot channel_post media", () => {
|
||||
|
||||
expect(replySpy).toHaveBeenCalledOnce();
|
||||
expect(sendMessageSpy).not.toHaveBeenCalled();
|
||||
expectTypeOnlyMediaPayload("image");
|
||||
expectUnavailableMediaPayload("image", "", "[media unavailable: file exceeds 0MB limit]");
|
||||
fetchSpy.mockRestore();
|
||||
});
|
||||
|
||||
@@ -541,7 +546,7 @@ describe("createTelegramBot channel_post media", () => {
|
||||
await waitForTelegramMockCalls(sendMessageSpy, 1);
|
||||
expectTelegramDownloadWarning(411);
|
||||
expect(replySpy).toHaveBeenCalledOnce();
|
||||
expectTypeOnlyMediaPayload("image");
|
||||
expectUnavailableMediaPayload("image");
|
||||
} finally {
|
||||
fetchSpy.mockRestore();
|
||||
}
|
||||
@@ -564,7 +569,7 @@ describe("createTelegramBot channel_post media", () => {
|
||||
await waitForTelegramMockCalls(sendMessageSpy, 1);
|
||||
expectTelegramDownloadWarning(100000);
|
||||
expect(replySpy).toHaveBeenCalledOnce();
|
||||
expectTypeOnlyMediaPayload("document");
|
||||
expectUnavailableMediaPayload("document");
|
||||
expect(saveRemoteMedia).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -594,7 +599,11 @@ describe("createTelegramBot channel_post media", () => {
|
||||
`⚠️ File too large. Maximum size is ${expectedLimitMb}MB.`,
|
||||
);
|
||||
expect(replySpy).toHaveBeenCalledOnce();
|
||||
expectTypeOnlyMediaPayload("document");
|
||||
expectUnavailableMediaPayload(
|
||||
"document",
|
||||
"",
|
||||
`[media unavailable: file exceeds ${expectedLimitMb}MB limit]`,
|
||||
);
|
||||
expect(saveRemoteMedia).not.toHaveBeenCalled();
|
||||
},
|
||||
);
|
||||
@@ -615,6 +624,7 @@ describe("createTelegramBot channel_post media", () => {
|
||||
error: new MediaFetchError("max_bytes", "Failed to fetch media: payload exceeds maxBytes 10"),
|
||||
result: { kind: "completed" },
|
||||
warning: "⚠️ File too large. Maximum size is 100MB.",
|
||||
notice: "[media unavailable: file exceeds 100MB limit]",
|
||||
},
|
||||
{
|
||||
name: "permanent SSRF rejection",
|
||||
@@ -645,7 +655,7 @@ describe("createTelegramBot channel_post media", () => {
|
||||
expect(sendMessageSpy.mock.calls[0]?.[1]).toBe(testCase.warning);
|
||||
if (testCase.warning) {
|
||||
expectTelegramDownloadWarning(testCase.messageId, testCase.warning);
|
||||
expectTypeOnlyMediaPayload("document");
|
||||
expectUnavailableMediaPayload("document", "", testCase.notice);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -899,7 +909,7 @@ describe("createTelegramBot channel_post media", () => {
|
||||
}),
|
||||
);
|
||||
expect(replySpy).toHaveBeenCalledOnce();
|
||||
expectTypeOnlyMediaPayload("image", "caption" in testCase ? testCase.caption : "");
|
||||
expectUnavailableMediaPayload("image", "caption" in testCase ? testCase.caption : "");
|
||||
} finally {
|
||||
fetchSpy.mockRestore();
|
||||
}
|
||||
|
||||
+2
-2
@@ -166,7 +166,7 @@ describe("telegram inbound media", () => {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "skips when file_path is missing",
|
||||
name: "reports unavailable media when file_path is missing",
|
||||
messageId: 2,
|
||||
getFile: async () => ({}),
|
||||
setupFetch: () => watchTelegramFetch(),
|
||||
@@ -178,7 +178,7 @@ describe("telegram inbound media", () => {
|
||||
expect(params.fetchSpy).not.toHaveBeenCalled();
|
||||
expect(params.replySpy).toHaveBeenCalledTimes(1);
|
||||
expect(replyPayload(params.replySpy)).toMatchObject({
|
||||
BodyForAgent: "",
|
||||
BodyForAgent: "[media unavailable: download failed]",
|
||||
MediaTypes: ["image"],
|
||||
RawBody: "",
|
||||
});
|
||||
|
||||
@@ -1,12 +1,30 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
onSpy,
|
||||
readRemoteMediaBufferSpy,
|
||||
telegramBotDepsForTest,
|
||||
telegramMediaHarnessSendMessageSpy,
|
||||
} from "./bot.media.e2e.test-harness.js";
|
||||
import { createBotHandlerWithOptions } from "./bot.media.test-utils.js";
|
||||
import { createBotHandlerWithOptions, mockTelegramPngDownload } from "./bot.media.test-utils.js";
|
||||
|
||||
describe("Telegram single-media warnings", () => {
|
||||
describe("Telegram media failure notices", () => {
|
||||
beforeEach(() => {
|
||||
telegramBotDepsForTest.getRuntimeConfig = () => ({
|
||||
channels: {
|
||||
telegram: {
|
||||
dmPolicy: "open",
|
||||
allowFrom: ["*"],
|
||||
groupAllowFrom: ["777", "-10042"],
|
||||
groupPolicy: "open",
|
||||
mediaMaxMb: 8,
|
||||
groups: {
|
||||
"-10042": { groupPolicy: "open", requireMention: false },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
telegramMediaHarnessSendMessageSpy.mockClear();
|
||||
});
|
||||
it.each([
|
||||
{
|
||||
description: "forum topic",
|
||||
@@ -15,6 +33,7 @@ describe("Telegram single-media warnings", () => {
|
||||
expectedThreadId: 202,
|
||||
failure: new Error("Telegram media exceeds 20 MB limit"),
|
||||
expectedWarning: "File too large",
|
||||
expectedNotice: "[media unavailable: file exceeds 8MB limit]",
|
||||
},
|
||||
{
|
||||
description: "bot DM topic",
|
||||
@@ -23,6 +42,7 @@ describe("Telegram single-media warnings", () => {
|
||||
expectedThreadId: 303,
|
||||
failure: new Error("Telegram media exceeds 20 MB limit"),
|
||||
expectedWarning: "File too large",
|
||||
expectedNotice: "[media unavailable: file exceeds 8MB limit]",
|
||||
},
|
||||
{
|
||||
description: "forum General topic",
|
||||
@@ -31,6 +51,7 @@ describe("Telegram single-media warnings", () => {
|
||||
expectedThreadId: undefined,
|
||||
failure: new Error("Telegram media exceeds 20 MB limit"),
|
||||
expectedWarning: "File too large",
|
||||
expectedNotice: "[media unavailable: file exceeds 8MB limit]",
|
||||
},
|
||||
{
|
||||
description: "forum topic after an ordinary download failure",
|
||||
@@ -39,63 +60,127 @@ describe("Telegram single-media warnings", () => {
|
||||
expectedThreadId: 404,
|
||||
failure: new Error("permanent download failure"),
|
||||
expectedWarning: "Failed to download media",
|
||||
expectedNotice: "[media unavailable: download failed]",
|
||||
},
|
||||
])(
|
||||
"keeps $description warnings in their originating Telegram thread",
|
||||
async ({ chat, threadId, expectedThreadId, failure, expectedWarning }) => {
|
||||
const originalLoadConfig = telegramBotDepsForTest.getRuntimeConfig;
|
||||
telegramBotDepsForTest.getRuntimeConfig = (() => ({
|
||||
channels: {
|
||||
telegram: {
|
||||
dmPolicy: "open",
|
||||
allowFrom: ["*"],
|
||||
groupAllowFrom: ["777"],
|
||||
groupPolicy: "open",
|
||||
groups: {
|
||||
"-10042": { allowFrom: ["777"], groupPolicy: "open", requireMention: false },
|
||||
},
|
||||
},
|
||||
async ({ chat, threadId, expectedThreadId, failure, expectedWarning, expectedNotice }) => {
|
||||
const { handler, replySpy } = await createBotHandlerWithOptions({});
|
||||
telegramMediaHarnessSendMessageSpy.mockClear();
|
||||
readRemoteMediaBufferSpy.mockRejectedValueOnce(failure);
|
||||
|
||||
await handler({
|
||||
message: {
|
||||
chat,
|
||||
from: { id: 777, is_bot: false, first_name: "Ada" },
|
||||
message_id: 901,
|
||||
message_thread_id: threadId,
|
||||
is_topic_message: true,
|
||||
caption: "Topic attachment",
|
||||
date: 1736380800,
|
||||
photo: [{ file_id: "topic-attachment" }],
|
||||
},
|
||||
})) as typeof telegramBotDepsForTest.getRuntimeConfig;
|
||||
me: { username: "openclaw_bot", has_topics_enabled: true },
|
||||
getFile: async () => ({ file_path: "photos/topic-attachment.jpg" }),
|
||||
});
|
||||
|
||||
try {
|
||||
const { handler } = await createBotHandlerWithOptions({});
|
||||
telegramMediaHarnessSendMessageSpy.mockClear();
|
||||
readRemoteMediaBufferSpy.mockRejectedValueOnce(failure);
|
||||
|
||||
await handler({
|
||||
message: {
|
||||
chat,
|
||||
from: { id: 777, is_bot: false, first_name: "Ada" },
|
||||
message_id: 901,
|
||||
message_thread_id: threadId,
|
||||
is_topic_message: true,
|
||||
caption: "Topic attachment",
|
||||
date: 1736380800,
|
||||
photo: [{ file_id: "topic-attachment" }],
|
||||
},
|
||||
me: { username: "openclaw_bot", has_topics_enabled: true },
|
||||
getFile: async () => ({ file_path: "photos/topic-attachment.jpg" }),
|
||||
});
|
||||
|
||||
expect(telegramMediaHarnessSendMessageSpy).toHaveBeenCalledWith(
|
||||
chat.id,
|
||||
expect.stringContaining(expectedWarning),
|
||||
expect.objectContaining({
|
||||
reply_parameters: expect.objectContaining({ message_id: 901 }),
|
||||
}),
|
||||
);
|
||||
const warning = telegramMediaHarnessSendMessageSpy.mock.calls.find(
|
||||
([, text]) => typeof text === "string" && text.includes(expectedWarning),
|
||||
);
|
||||
if (!warning) {
|
||||
throw new Error(`Missing ${expectedWarning} Telegram media warning`);
|
||||
}
|
||||
const options = warning[2] as { message_thread_id?: number };
|
||||
expect(options.message_thread_id).toBe(expectedThreadId);
|
||||
} finally {
|
||||
telegramBotDepsForTest.getRuntimeConfig = originalLoadConfig;
|
||||
expect(telegramMediaHarnessSendMessageSpy).toHaveBeenCalledWith(
|
||||
chat.id,
|
||||
expect.stringContaining(expectedWarning),
|
||||
expect.objectContaining({
|
||||
reply_parameters: expect.objectContaining({ message_id: 901 }),
|
||||
}),
|
||||
);
|
||||
const warning = telegramMediaHarnessSendMessageSpy.mock.calls.find(
|
||||
([, text]) => typeof text === "string" && text.includes(expectedWarning),
|
||||
);
|
||||
if (!warning) {
|
||||
throw new Error(`Missing ${expectedWarning} Telegram media warning`);
|
||||
}
|
||||
const options = warning[2] as { message_thread_id?: number };
|
||||
expect(options.message_thread_id).toBe(expectedThreadId);
|
||||
expect(replySpy).toHaveBeenCalledTimes(1);
|
||||
expect(replySpy.mock.calls[0]?.[0]).toMatchObject({
|
||||
Body: expect.stringContaining(expectedNotice),
|
||||
BodyForAgent: `Topic attachment\n\n${expectedNotice}`,
|
||||
RawBody: "Topic attachment",
|
||||
CommandBody: "Topic attachment",
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it.each([false, true])(
|
||||
"keeps channel-post oversize warnings suppressed (failure=%s)",
|
||||
async (fails) => {
|
||||
const { replySpy } = await createBotHandlerWithOptions({});
|
||||
const handler = onSpy.mock.calls.find(([event]) => event === "channel_post")?.[1];
|
||||
mockTelegramPngDownload();
|
||||
if (fails) {
|
||||
readRemoteMediaBufferSpy.mockRejectedValueOnce(
|
||||
new Error("Telegram media exceeds 8 MB limit"),
|
||||
);
|
||||
}
|
||||
await handler({
|
||||
channelPost: {
|
||||
chat: { id: -10042, type: "channel", title: "Announcements" },
|
||||
sender_chat: { id: -10042, type: "channel", title: "Announcements" },
|
||||
message_id: 902,
|
||||
date: 1736380800,
|
||||
caption: "Channel attachment",
|
||||
photo: [{ file_id: "channel-attachment" }],
|
||||
},
|
||||
me: { username: "openclaw_bot" },
|
||||
getFile: async () => ({ file_path: "photos/channel-attachment.jpg" }),
|
||||
});
|
||||
expect(telegramMediaHarnessSendMessageSpy).not.toHaveBeenCalled();
|
||||
expect(replySpy).toHaveBeenCalledTimes(1);
|
||||
const payload = replySpy.mock.calls[0]?.[0];
|
||||
expect(payload).toMatchObject({
|
||||
BodyForAgent: fails
|
||||
? "Channel attachment\n\n[media unavailable: file exceeds 8MB limit]"
|
||||
: "Channel attachment",
|
||||
RawBody: "Channel attachment",
|
||||
CommandBody: "Channel attachment",
|
||||
});
|
||||
expect(payload.Body.includes("[media unavailable:")).toBe(fails);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([0, 1, 2])("accounts for %s failed attachments in an album", async (failedCount) => {
|
||||
const { handler, replySpy } = await createBotHandlerWithOptions({});
|
||||
mockTelegramPngDownload();
|
||||
for (let index = 0; index < failedCount; index++) {
|
||||
readRemoteMediaBufferSpy.mockRejectedValueOnce(
|
||||
new Error("Telegram media exceeds 8 MB limit"),
|
||||
);
|
||||
}
|
||||
for (let index = 0; index < 2; index++) {
|
||||
await handler({
|
||||
message: {
|
||||
chat: { id: 4242, type: "private" },
|
||||
from: { id: 777, is_bot: false, first_name: "Ada" },
|
||||
message_id: 910 + index,
|
||||
date: 1736380800,
|
||||
media_group_id: "failure-notice-album",
|
||||
caption: index === 0 ? "Album attachment" : undefined,
|
||||
photo: [{ file_id: `album-${index}` }],
|
||||
},
|
||||
me: { username: "openclaw_bot" },
|
||||
getFile: async () => ({ file_path: `photos/album-${index}.jpg` }),
|
||||
});
|
||||
}
|
||||
await vi.waitFor(() => expect(replySpy).toHaveBeenCalledTimes(1));
|
||||
const payload = replySpy.mock.calls[0]?.[0];
|
||||
expect(payload).toMatchObject({
|
||||
BodyForAgent: failedCount
|
||||
? `Album attachment\n\n[media unavailable: ${failedCount} of 2 attachments could not be downloaded]`
|
||||
: "Album attachment",
|
||||
RawBody: "Album attachment",
|
||||
CommandBody: "Album attachment",
|
||||
});
|
||||
expect(payload.Body.includes("[media unavailable:")).toBe(failedCount > 0);
|
||||
expect(payload.media.filter((media: { path?: string }) => media.path)).toHaveLength(
|
||||
2 - failedCount,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user