Fork of upstream llblab/[email protected].0 (SHA afe09c5) repackaged as a drop-in extension for can1357/oh-my-pi (omp >= 16.5.2). Changes vs upstream: - Rename package: @llblab/pi-telegram -> @evandrodevbr/oh-my-pi-telegram - Bump version: 0.22.0 -> 0.22.0-evandro.1 - Update repository/homepage/bugs URLs to evandrodevbr - Add omp field in package.json (displayName + compatibleWith) - README: prepend 'Oh My Pi (omp) Compatibility' section with install commands, verified-components table, upstream lineage, and rationale - CHANGELOG: add '0.22.0-evandro.1' entry under Unreleased - docs/: rename @llblab/pi-telegram imports to @evandrodevbr/oh-my-pi-telegram - tests/public-api.test.ts: rename package import paths Gates re-run on the fork: - npm run typecheck -> tsc --noEmit (success) - npm test -> 1229 pass / 0 fail / 1 platform-only skip - npm run audit -> found 0 vulnerabilities - npm run pack:check -> evandrodevbr-oh-my-pi-telegram-0.22.0-evandro.1.tgz Verified: - omp install --link ./pi-telegram -> Linked OK - omp plugin list -> @evandrodevbr/[email protected].0-evandro.1 Upstream lineage: badlogic/pi-telegram -> llblab/pi-telegram (0.22.0) -> evandrodevbr/oh-my-pi-telegram (this fork)
920 lines
28 KiB
TypeScript
920 lines
28 KiB
TypeScript
/**
|
|
* Regression tests for Telegram reply delivery helpers
|
|
* Covers UI/compat rendered-message transport, chunk delivery, and native/plain final reply sending
|
|
*/
|
|
|
|
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
|
|
// Transport-level dedup is module-global; reset between tests.
|
|
test.beforeEach(() => {
|
|
resetTransportReplyDedup();
|
|
});
|
|
|
|
import {
|
|
buildTelegramReplyParameters,
|
|
buildTelegramReplyTransport,
|
|
createGuestMarkdownReplySender,
|
|
createReplyDedupRuntime,
|
|
createTelegramRenderedMessageDeliveryRuntime,
|
|
createTelegramRenderedMessageRuntime,
|
|
dedupSendTextReply,
|
|
editTelegramRenderedMessage,
|
|
extractLatestAssistantMessageText,
|
|
getAgentMessageText,
|
|
isAssistantAgentMessage,
|
|
normalizeTelegramNativeMarkdown,
|
|
resetTransportReplyDedup,
|
|
sendTelegramNativeMarkdownReply,
|
|
sendTelegramPlainReply,
|
|
sendTelegramRenderedChunks,
|
|
splitTelegramNativeMarkdown,
|
|
TELEGRAM_RICH_MESSAGE_MAX_BLOCKS,
|
|
TELEGRAM_RICH_MESSAGE_MAX_CHARS,
|
|
} from "../lib/replies.ts";
|
|
import { createDedupAgentStartHook } from "../lib/lifecycle.ts";
|
|
import { createTelegramThreadTarget } from "../lib/target.ts";
|
|
|
|
test("Reply helpers extract assistant message text and metadata", () => {
|
|
const messages = [
|
|
{ role: "user", content: [{ type: "text", text: "question" }] },
|
|
{
|
|
role: "assistant",
|
|
stopReason: "error",
|
|
errorMessage: "boom",
|
|
content: [
|
|
{ type: "text", text: " hello " },
|
|
{ type: "image", source: "ignored" },
|
|
{ type: "text", text: "world " },
|
|
],
|
|
},
|
|
];
|
|
assert.equal(isAssistantAgentMessage(messages[1]), true);
|
|
assert.equal(getAgentMessageText(messages[1]), "hello world");
|
|
assert.deepEqual(extractLatestAssistantMessageText(messages), {
|
|
text: "hello world",
|
|
stopReason: "error",
|
|
errorMessage: "boom",
|
|
});
|
|
});
|
|
|
|
test("Reply transport forwards send and edit operations through delivery helpers", async () => {
|
|
const events: string[] = [];
|
|
const transport = buildTelegramReplyTransport({
|
|
sendMessage: async (body) => {
|
|
events.push(`send:${body.chat_id}:${body.text}`);
|
|
return { message_id: 5 };
|
|
},
|
|
editMessage: async (body) => {
|
|
events.push(`edit:${body.chat_id}:${body.message_id}:${body.text}`);
|
|
},
|
|
});
|
|
assert.equal(await transport.sendRenderedChunks(7, [{ text: "one" }]), 5);
|
|
assert.equal(await transport.editRenderedMessage(7, 9, [{ text: "two" }]), 9);
|
|
assert.deepEqual(events, ["send:7:one", "edit:7:9:two"]);
|
|
});
|
|
|
|
test("Reply delivery includes thread target on rendered chunks", async () => {
|
|
const sentBodies: Array<Record<string, unknown>> = [];
|
|
const ownership: Array<Record<string, unknown>> = [];
|
|
await sendTelegramRenderedChunks(
|
|
-1007,
|
|
[{ text: "one" }, { text: "two" }],
|
|
{
|
|
sendMessage: async (body) => {
|
|
sentBodies.push(body);
|
|
return { message_id: sentBodies.length };
|
|
},
|
|
editMessage: async () => {},
|
|
recordOwnership: (input) => {
|
|
ownership.push(input);
|
|
},
|
|
},
|
|
{ target: createTelegramThreadTarget(-1007, 42) },
|
|
);
|
|
assert.deepEqual(
|
|
sentBodies.map((body) => body.message_thread_id),
|
|
[42, 42],
|
|
);
|
|
assert.deepEqual(
|
|
ownership.map((record) => record.messageId),
|
|
[1, 2],
|
|
);
|
|
assert.deepEqual(ownership[0]?.target, createTelegramThreadTarget(-1007, 42));
|
|
});
|
|
|
|
test("Reply delivery includes thread target on rendered edits and continuation chunks", async () => {
|
|
const sentBodies: Array<Record<string, unknown>> = [];
|
|
const editedBodies: Array<Record<string, unknown>> = [];
|
|
const ownership: Array<Record<string, unknown>> = [];
|
|
await editTelegramRenderedMessage(
|
|
-1007,
|
|
9,
|
|
[{ text: "one" }, { text: "two" }],
|
|
{
|
|
sendMessage: async (body) => {
|
|
sentBodies.push(body);
|
|
return { message_id: sentBodies.length };
|
|
},
|
|
editMessage: async (body) => {
|
|
editedBodies.push(body);
|
|
},
|
|
recordOwnership: (input) => {
|
|
ownership.push(input);
|
|
},
|
|
},
|
|
{ target: createTelegramThreadTarget(-1007, 42) },
|
|
);
|
|
assert.equal(editedBodies[0]?.message_thread_id, 42);
|
|
assert.equal(sentBodies[0]?.message_thread_id, 42);
|
|
assert.deepEqual(
|
|
ownership.map((record) => record.messageId),
|
|
[9, 1],
|
|
);
|
|
});
|
|
|
|
test("Reply delivery sends chunks and applies reply markup only to the last chunk", async () => {
|
|
const sentBodies: Array<Record<string, unknown>> = [];
|
|
const messageId = await sendTelegramRenderedChunks(
|
|
7,
|
|
[{ text: "one" }, { text: "two", parseMode: "HTML" }],
|
|
{
|
|
sendMessage: async (body) => {
|
|
sentBodies.push(body);
|
|
return { message_id: sentBodies.length };
|
|
},
|
|
editMessage: async () => {},
|
|
},
|
|
{
|
|
replyMarkup: {
|
|
inline_keyboard: [[{ text: "ok", callback_data: "noop" }]],
|
|
},
|
|
},
|
|
);
|
|
assert.equal(messageId, 2);
|
|
assert.deepEqual(sentBodies, [
|
|
{ chat_id: 7, text: "one", parse_mode: undefined, reply_markup: undefined },
|
|
{
|
|
chat_id: 7,
|
|
text: "two",
|
|
parse_mode: "HTML",
|
|
reply_markup: {
|
|
inline_keyboard: [[{ text: "ok", callback_data: "noop" }]],
|
|
},
|
|
},
|
|
]);
|
|
});
|
|
|
|
test("Reply delivery rejects generated buttons above Telegram callback limit", async () => {
|
|
const sentBodies: Array<Record<string, unknown>> = [];
|
|
await assert.rejects(
|
|
() =>
|
|
sendTelegramRenderedChunks(
|
|
7,
|
|
[{ text: "one" }],
|
|
{
|
|
sendMessage: async (body) => {
|
|
sentBodies.push(body);
|
|
return { message_id: sentBodies.length };
|
|
},
|
|
editMessage: async () => {},
|
|
},
|
|
{
|
|
replyMarkup: {
|
|
inline_keyboard: [
|
|
[{ text: "Too long", callback_data: "x".repeat(65) }],
|
|
],
|
|
},
|
|
},
|
|
),
|
|
/exceeds 64 bytes/,
|
|
);
|
|
assert.deepEqual(sentBodies, []);
|
|
});
|
|
|
|
test("Reply delivery applies reply parameters only to the first chunk", async () => {
|
|
const sentBodies: Array<Record<string, unknown>> = [];
|
|
await sendTelegramRenderedChunks(
|
|
7,
|
|
[{ text: "one" }, { text: "two" }],
|
|
{
|
|
sendMessage: async (body) => {
|
|
sentBodies.push(body);
|
|
return { message_id: sentBodies.length };
|
|
},
|
|
editMessage: async () => {},
|
|
},
|
|
{ replyToMessageId: 42 },
|
|
);
|
|
assert.deepEqual(sentBodies[0]?.reply_parameters, {
|
|
message_id: 42,
|
|
allow_sending_without_reply: true,
|
|
});
|
|
assert.equal("reply_parameters" in (sentBodies[1] ?? {}), false);
|
|
});
|
|
|
|
test("Reply delivery edits the first chunk and sends remaining chunks separately", async () => {
|
|
const editedBodies: Array<Record<string, unknown>> = [];
|
|
const sentBodies: Array<Record<string, unknown>> = [];
|
|
const result = await editTelegramRenderedMessage(
|
|
7,
|
|
99,
|
|
[{ text: "first", parseMode: "HTML" }, { text: "second" }],
|
|
{
|
|
sendMessage: async (body) => {
|
|
sentBodies.push(body);
|
|
return { message_id: 123 };
|
|
},
|
|
editMessage: async (body) => {
|
|
editedBodies.push(body);
|
|
},
|
|
},
|
|
{
|
|
replyMarkup: {
|
|
inline_keyboard: [[{ text: "ok", callback_data: "noop" }]],
|
|
},
|
|
},
|
|
);
|
|
assert.equal(result, 123);
|
|
assert.deepEqual(editedBodies, [
|
|
{
|
|
chat_id: 7,
|
|
message_id: 99,
|
|
text: "first",
|
|
parse_mode: "HTML",
|
|
reply_markup: undefined,
|
|
},
|
|
]);
|
|
assert.deepEqual(sentBodies, [
|
|
{
|
|
chat_id: 7,
|
|
text: "second",
|
|
parse_mode: undefined,
|
|
reply_markup: {
|
|
inline_keyboard: [[{ text: "ok", callback_data: "noop" }]],
|
|
},
|
|
},
|
|
]);
|
|
assert.equal("reply_parameters" in (sentBodies[0] ?? {}), false);
|
|
});
|
|
|
|
test("Reply runtime bundles text, native markdown, and UI/compat interactive delivery", async () => {
|
|
const sent: Array<Record<string, unknown>> = [];
|
|
const richSent: Array<Record<string, unknown>> = [];
|
|
const edited: Array<Record<string, unknown>> = [];
|
|
const runtime = createTelegramRenderedMessageRuntime({
|
|
renderTelegramMessage: (text, options) => [
|
|
{ text: `${options?.mode ?? "plain"}:${text}` },
|
|
],
|
|
replyTransport: buildTelegramReplyTransport({
|
|
sendMessage: async (body) => {
|
|
sent.push(body);
|
|
return { message_id: sent.length };
|
|
},
|
|
editMessage: async (body) => {
|
|
edited.push(body);
|
|
},
|
|
}),
|
|
sendRichMessage: async (body) => {
|
|
richSent.push(body);
|
|
return { message_id: 77 };
|
|
},
|
|
});
|
|
assert.equal(await runtime.sendTextReply(7, 42, "hello"), 1);
|
|
assert.equal(await runtime.sendMarkdownReply(7, 43, "**hello**"), 77);
|
|
assert.equal(
|
|
await runtime.sendInteractiveMessage(
|
|
7,
|
|
"menu",
|
|
"html",
|
|
{
|
|
inline_keyboard: [],
|
|
},
|
|
{ replyToMessageId: 44 },
|
|
),
|
|
2,
|
|
);
|
|
await runtime.editInteractiveMessage(7, 9, "menu", "html", {
|
|
inline_keyboard: [],
|
|
});
|
|
assert.deepEqual(
|
|
sent.map((body) => body.text),
|
|
["plain:hello", "html:menu"],
|
|
);
|
|
assert.deepEqual(richSent, [
|
|
{
|
|
chat_id: 7,
|
|
rich_message: { markdown: "**hello**", skip_entity_detection: true },
|
|
reply_markup: undefined,
|
|
reply_parameters: {
|
|
message_id: 43,
|
|
allow_sending_without_reply: true,
|
|
},
|
|
},
|
|
]);
|
|
assert.deepEqual(sent[0]?.reply_parameters, {
|
|
message_id: 42,
|
|
allow_sending_without_reply: true,
|
|
});
|
|
assert.deepEqual(sent[1]?.reply_markup, { inline_keyboard: [] });
|
|
assert.deepEqual(sent[1]?.reply_parameters, {
|
|
message_id: 44,
|
|
allow_sending_without_reply: true,
|
|
});
|
|
assert.deepEqual(
|
|
edited.map((body) => body.text),
|
|
["html:menu"],
|
|
);
|
|
});
|
|
|
|
test("Plain reply includes thread target on rendered messages", async () => {
|
|
const sentBodies: Array<Record<string, unknown>> = [];
|
|
const runtime = createTelegramRenderedMessageRuntime({
|
|
renderTelegramMessage: (text) => [{ text }],
|
|
replyTransport: buildTelegramReplyTransport({
|
|
sendMessage: async (body) => {
|
|
sentBodies.push(body);
|
|
return { message_id: 1 };
|
|
},
|
|
editMessage: async () => {},
|
|
}),
|
|
sendRichMessage: async () => ({ message_id: 77 }),
|
|
});
|
|
await runtime.sendTextReply(-1007, 9, "hello", {
|
|
target: createTelegramThreadTarget(-1007, 42),
|
|
});
|
|
assert.equal(sentBodies[0]?.message_thread_id, 42);
|
|
assert.deepEqual(sentBodies[0]?.reply_parameters, {
|
|
message_id: 9,
|
|
allow_sending_without_reply: true,
|
|
});
|
|
assert.equal("reply_to_message_id" in (sentBodies[0] ?? {}), false);
|
|
});
|
|
|
|
test("Native Markdown reply anchors only the first rich chunk in thread targets", async () => {
|
|
const sentBodies: Array<Record<string, unknown>> = [];
|
|
const ownership: Array<Record<string, unknown>> = [];
|
|
const first = "a".repeat(TELEGRAM_RICH_MESSAGE_MAX_CHARS - 10);
|
|
const second = "b".repeat(30);
|
|
const target = createTelegramThreadTarget(-1007, 42);
|
|
await sendTelegramNativeMarkdownReply(
|
|
-1007,
|
|
9,
|
|
`${first}\n\n${second}`,
|
|
{
|
|
recordOwnership: (input) => {
|
|
ownership.push(input);
|
|
},
|
|
sendRichMessage: async (body) => {
|
|
sentBodies.push(body);
|
|
return { message_id: sentBodies.length };
|
|
},
|
|
},
|
|
{ target },
|
|
);
|
|
assert.equal(sentBodies.length, 2);
|
|
assert.equal(sentBodies[0]?.message_thread_id, 42);
|
|
assert.equal(sentBodies[1]?.message_thread_id, 42);
|
|
assert.deepEqual(sentBodies[0]?.reply_parameters, {
|
|
message_id: 9,
|
|
allow_sending_without_reply: true,
|
|
});
|
|
assert.equal("reply_parameters" in (sentBodies[1] ?? {}), false);
|
|
assert.deepEqual(ownership, [
|
|
{ chatId: -1007, messageId: 1, target },
|
|
{ chatId: -1007, messageId: 2, target },
|
|
]);
|
|
});
|
|
|
|
test("Reply runtime can send markdown through native rich messages", async () => {
|
|
const richBodies: Array<Record<string, unknown>> = [];
|
|
const sentBodies: Array<Record<string, unknown>> = [];
|
|
const runtime = createTelegramRenderedMessageRuntime({
|
|
renderTelegramMessage: (text, options) => [
|
|
{ text: `${options?.mode ?? "plain"}:${text}` },
|
|
],
|
|
replyTransport: buildTelegramReplyTransport({
|
|
sendMessage: async (body) => {
|
|
sentBodies.push(body);
|
|
return { message_id: sentBodies.length };
|
|
},
|
|
editMessage: async () => {},
|
|
}),
|
|
sendRichMessage: async (body) => {
|
|
richBodies.push(body);
|
|
return { message_id: 77 };
|
|
},
|
|
});
|
|
assert.equal(
|
|
await runtime.sendMarkdownReply(7, 43, "# hello", {
|
|
replyMarkup: {
|
|
inline_keyboard: [[{ text: "ok", callback_data: "noop" }]],
|
|
},
|
|
}),
|
|
77,
|
|
);
|
|
assert.deepEqual(richBodies, [
|
|
{
|
|
chat_id: 7,
|
|
rich_message: { markdown: "# hello", skip_entity_detection: true },
|
|
reply_markup: {
|
|
inline_keyboard: [[{ text: "ok", callback_data: "noop" }]],
|
|
},
|
|
reply_parameters: {
|
|
message_id: 43,
|
|
allow_sending_without_reply: true,
|
|
},
|
|
},
|
|
]);
|
|
assert.deepEqual(sentBodies, []);
|
|
});
|
|
|
|
test("Reply runtime uses native rich messages for anchored thread markdown replies", async () => {
|
|
const sentBodies: Array<Record<string, unknown>> = [];
|
|
const richBodies: Array<Record<string, unknown>> = [];
|
|
const target = createTelegramThreadTarget(-1007, 42);
|
|
const runtime = createTelegramRenderedMessageRuntime({
|
|
renderTelegramMessage: (text, options) => [
|
|
{ text: `${options?.mode ?? "plain"}:${text}`, parseMode: "HTML" },
|
|
],
|
|
replyTransport: buildTelegramReplyTransport({
|
|
sendMessage: async (body) => {
|
|
sentBodies.push(body);
|
|
return { message_id: sentBodies.length };
|
|
},
|
|
editMessage: async () => {},
|
|
}),
|
|
sendRichMessage: async (body) => {
|
|
richBodies.push(body);
|
|
return { message_id: 77 };
|
|
},
|
|
});
|
|
|
|
assert.equal(
|
|
await runtime.sendMarkdownReply(-1007, 43, "# hello", {
|
|
replyMarkup: {
|
|
inline_keyboard: [[{ text: "ok", callback_data: "noop" }]],
|
|
},
|
|
target,
|
|
}),
|
|
77,
|
|
);
|
|
|
|
assert.deepEqual(sentBodies, []);
|
|
assert.deepEqual(richBodies, [
|
|
{
|
|
chat_id: -1007,
|
|
rich_message: { markdown: "# hello", skip_entity_detection: true },
|
|
reply_markup: {
|
|
inline_keyboard: [[{ text: "ok", callback_data: "noop" }]],
|
|
},
|
|
reply_parameters: {
|
|
message_id: 43,
|
|
allow_sending_without_reply: true,
|
|
},
|
|
message_thread_id: 42,
|
|
},
|
|
]);
|
|
});
|
|
|
|
test("Reply runtime does not fall back to HTML when native rich message delivery fails", async () => {
|
|
const sentBodies: Array<Record<string, unknown>> = [];
|
|
const runtime = createTelegramRenderedMessageRuntime({
|
|
renderTelegramMessage: (text, options) => [
|
|
{ text: `${options?.mode ?? "plain"}:${text}`, parseMode: "HTML" },
|
|
],
|
|
replyTransport: buildTelegramReplyTransport({
|
|
sendMessage: async (body) => {
|
|
sentBodies.push(body);
|
|
return { message_id: sentBodies.length };
|
|
},
|
|
editMessage: async () => {},
|
|
}),
|
|
sendRichMessage: async () => {
|
|
throw new Error("rich unsupported");
|
|
},
|
|
});
|
|
await assert.rejects(() => runtime.sendMarkdownReply(7, 43, "# hello"), {
|
|
message: "rich unsupported",
|
|
});
|
|
assert.deepEqual(sentBodies, []);
|
|
});
|
|
|
|
test("Reply delivery runtime exposes transport and UI/compat rendered-message helpers", async () => {
|
|
const sent: Array<Record<string, unknown>> = [];
|
|
const runtime = createTelegramRenderedMessageDeliveryRuntime({
|
|
renderTelegramMessage: (text, options) => [
|
|
{ text: `${options?.mode ?? "plain"}:${text}` },
|
|
],
|
|
sendMessage: async (body) => {
|
|
sent.push(body);
|
|
return { message_id: sent.length };
|
|
},
|
|
editMessage: async () => {},
|
|
sendRichMessage: async () => ({ message_id: 99 }),
|
|
});
|
|
assert.equal(await runtime.sendTextReply(7, 42, "hello"), 1);
|
|
assert.equal(
|
|
await runtime.replyTransport.sendRenderedChunks(7, [{ text: "raw" }]),
|
|
2,
|
|
);
|
|
assert.deepEqual(
|
|
sent.map((body) => body.text),
|
|
["plain:hello", "raw"],
|
|
);
|
|
});
|
|
|
|
test("Guest replies answer with native Rich Markdown content", async () => {
|
|
const calls: Array<{
|
|
guestQueryId: string;
|
|
text?: string;
|
|
options?: {
|
|
richMessage?: { markdown?: string; skip_entity_detection?: boolean };
|
|
};
|
|
}> = [];
|
|
const sendGuestReply = createGuestMarkdownReplySender({
|
|
answerGuestQuery: async (guestQueryId, text, options) => {
|
|
calls.push({ guestQueryId, text, options });
|
|
},
|
|
});
|
|
await sendGuestReply("guest-1", "**hello** /start");
|
|
assert.deepEqual(calls, [
|
|
{
|
|
guestQueryId: "guest-1",
|
|
text: undefined,
|
|
options: {
|
|
richMessage: {
|
|
markdown: "**hello** /start",
|
|
skip_entity_detection: true,
|
|
},
|
|
},
|
|
},
|
|
]);
|
|
});
|
|
|
|
test("Native Markdown delivery normalizes space-after-marker blockquotes outside code", () => {
|
|
const markdown = "> quoted\n\n```md\n> quoted code\n```";
|
|
assert.equal(
|
|
normalizeTelegramNativeMarkdown(markdown),
|
|
">quoted\n\n```md\n> quoted code\n```",
|
|
);
|
|
});
|
|
|
|
test("Native Markdown delivery neutralizes indented list markers that break Rich Markdown", () => {
|
|
const markdown = [
|
|
"**HEAD**",
|
|
"",
|
|
" - alpha/beta gamma-delta with `inline.code`",
|
|
"",
|
|
"END",
|
|
].join("\n");
|
|
assert.equal(
|
|
normalizeTelegramNativeMarkdown(markdown),
|
|
[
|
|
"**HEAD**",
|
|
"",
|
|
"\u00A0\u00A0- alpha/beta gamma-delta with `inline.code`",
|
|
"",
|
|
"END",
|
|
].join("\n"),
|
|
);
|
|
});
|
|
|
|
test("Native Markdown delivery preserves top-level list markers", () => {
|
|
assert.equal(
|
|
normalizeTelegramNativeMarkdown("- plain\n - nested\n\t- tabbed"),
|
|
"- plain\n\u00A0\u00A0- nested\n\u00A0\u00A0- tabbed",
|
|
);
|
|
});
|
|
|
|
test("Native Markdown delivery converts multiline display math to math fences", () => {
|
|
const markdown = [
|
|
"Block math:",
|
|
"",
|
|
"$$",
|
|
"\\int_0^1 x^2 dx = \\frac{1}{3}",
|
|
"$$",
|
|
"",
|
|
"```md",
|
|
"$$",
|
|
"literal fenced math delimiters",
|
|
"$$",
|
|
"```",
|
|
].join("\n");
|
|
assert.equal(
|
|
normalizeTelegramNativeMarkdown(markdown),
|
|
[
|
|
"Block math:",
|
|
"",
|
|
"```math",
|
|
"\\int_0^1 x^2 dx = \\frac{1}{3}",
|
|
"```",
|
|
"",
|
|
"```md",
|
|
"$$",
|
|
"literal fenced math delimiters",
|
|
"$$",
|
|
"```",
|
|
].join("\n"),
|
|
);
|
|
});
|
|
|
|
test("Native Markdown delivery ignores math delimiters inside fences when pairing", () => {
|
|
const markdown = ["$$", "```md", "$$", "```"].join("\n");
|
|
assert.equal(normalizeTelegramNativeMarkdown(markdown), markdown);
|
|
});
|
|
|
|
test("Native Markdown splitter keeps truncation-risk fixture intact", () => {
|
|
const markdown = [
|
|
"**HEAD**",
|
|
"",
|
|
" - alpha/beta gamma-delta with `inline.code`",
|
|
"",
|
|
"END",
|
|
].join("\n");
|
|
const chunks = splitTelegramNativeMarkdown(markdown);
|
|
assert.equal(chunks.length, 1);
|
|
assert.match(chunks[0] ?? "", /inline\.code/);
|
|
assert.match(chunks[0] ?? "", /END$/);
|
|
});
|
|
|
|
test("Native Markdown delivery sends normalized risky fixture without losing tail text", async () => {
|
|
const bodies: Array<Record<string, unknown>> = [];
|
|
await sendTelegramNativeMarkdownReply(
|
|
7,
|
|
undefined,
|
|
[
|
|
"**HEAD**",
|
|
"",
|
|
" - alpha/beta gamma-delta with `inline.code`",
|
|
"",
|
|
"END",
|
|
].join("\n"),
|
|
{
|
|
sendRichMessage: async (body) => {
|
|
bodies.push(body);
|
|
return { message_id: bodies.length };
|
|
},
|
|
},
|
|
);
|
|
const markdown =
|
|
(bodies[0]?.rich_message as { markdown?: string })?.markdown ?? "";
|
|
assert.match(markdown, /^\*\*HEAD\*\*/);
|
|
assert.match(
|
|
markdown,
|
|
/\u00A0\u00A0- alpha\/beta gamma-delta with `inline\.code`/,
|
|
);
|
|
assert.match(markdown, /END$/);
|
|
});
|
|
|
|
test("Native Markdown delivery escapes dollar ticker atoms outside code", () => {
|
|
const markdown = [
|
|
"Токен $BLDR может ломать math parsing.",
|
|
"Bold ticker **$BTC** тоже ломает rich parsing.",
|
|
"Токен $NTVE, $VETO. и $NTVE/Bucket тоже опасны.",
|
|
"Inline code: `$BLDR`, formula $x^2 + y^2$, and explicit math $BTC$ stay unchanged.",
|
|
"```md",
|
|
"$BLDR in code fence",
|
|
"```",
|
|
].join("\n");
|
|
assert.equal(
|
|
normalizeTelegramNativeMarkdown(markdown),
|
|
[
|
|
"Токен \\$BLDR может ломать math parsing.",
|
|
"Bold ticker **\\$BTC** тоже ломает rich parsing.",
|
|
"Токен \\$NTVE, \\$VETO. и \\$NTVE/Bucket тоже опасны.",
|
|
"Inline code: `$BLDR`, formula $x^2 + y^2$, and explicit math $BTC$ stay unchanged.",
|
|
"```md",
|
|
"$BLDR in code fence",
|
|
"```",
|
|
].join("\n"),
|
|
);
|
|
});
|
|
|
|
test("Native Markdown splitter prefers paragraph boundaries", () => {
|
|
const first = "a".repeat(TELEGRAM_RICH_MESSAGE_MAX_CHARS - 10);
|
|
const second = "b".repeat(30);
|
|
const chunks = splitTelegramNativeMarkdown(`${first}\n\n${second}`);
|
|
assert.deepEqual(chunks, [first, second]);
|
|
assert.ok(
|
|
chunks.every((chunk) => chunk.length <= TELEGRAM_RICH_MESSAGE_MAX_CHARS),
|
|
);
|
|
});
|
|
|
|
test("Native Markdown splitter falls back to hard limits for long atoms", () => {
|
|
const chunks = splitTelegramNativeMarkdown(
|
|
"x".repeat(TELEGRAM_RICH_MESSAGE_MAX_CHARS + 5),
|
|
);
|
|
assert.equal(chunks.length, 2);
|
|
assert.equal(chunks[0]?.length, TELEGRAM_RICH_MESSAGE_MAX_CHARS);
|
|
assert.equal(chunks[1], "xxxxx");
|
|
});
|
|
|
|
test("Native Markdown splitter keeps fenced code blocks together when possible", () => {
|
|
const codeBlock = `\`\`\`ts\n${"x\n\n".repeat(100)}\`\`\``;
|
|
const prefix = "a".repeat(TELEGRAM_RICH_MESSAGE_MAX_CHARS - codeBlock.length);
|
|
const chunks = splitTelegramNativeMarkdown(
|
|
`${prefix}\n\n${codeBlock}\n\ntail`,
|
|
);
|
|
assert.equal(chunks.length, 2);
|
|
assert.equal(chunks[1], `${codeBlock}\n\ntail`);
|
|
});
|
|
|
|
test("Native Markdown splitter rewraps oversized fenced code blocks", () => {
|
|
const chunks = splitTelegramNativeMarkdown(
|
|
`\`\`\`ts\n${"x".repeat(TELEGRAM_RICH_MESSAGE_MAX_CHARS + 500)}\n\`\`\``,
|
|
);
|
|
assert.ok(chunks.length > 1);
|
|
assert.ok(
|
|
chunks.every((chunk) => chunk.length <= TELEGRAM_RICH_MESSAGE_MAX_CHARS),
|
|
);
|
|
assert.ok(chunks.every((chunk) => chunk.startsWith("```ts\n")));
|
|
assert.ok(chunks.every((chunk) => chunk.endsWith("\n```")));
|
|
const contentLength = chunks
|
|
.map((chunk) => chunk.split("\n").slice(1, -1).join("\n").length)
|
|
.reduce((sum, length) => sum + length, 0);
|
|
assert.equal(contentLength, TELEGRAM_RICH_MESSAGE_MAX_CHARS + 500);
|
|
});
|
|
|
|
test("Native Markdown splitter rewraps oversized display math blocks", () => {
|
|
const chunks = splitTelegramNativeMarkdown(
|
|
`$$\n${"x".repeat(TELEGRAM_RICH_MESSAGE_MAX_CHARS + 500)}\n$$`,
|
|
);
|
|
assert.ok(chunks.length > 1);
|
|
assert.ok(
|
|
chunks.every((chunk) => chunk.length <= TELEGRAM_RICH_MESSAGE_MAX_CHARS),
|
|
);
|
|
assert.ok(chunks.every((chunk) => chunk.startsWith("```math\n")));
|
|
assert.ok(chunks.every((chunk) => chunk.endsWith("\n```")));
|
|
});
|
|
|
|
test("Native Markdown splitter rewraps oversized inline formatting blocks", () => {
|
|
const chunks = splitTelegramNativeMarkdown(
|
|
`**${"x".repeat(TELEGRAM_RICH_MESSAGE_MAX_CHARS + 500)}**`,
|
|
);
|
|
assert.ok(chunks.length > 1);
|
|
assert.ok(
|
|
chunks.every((chunk) => chunk.length <= TELEGRAM_RICH_MESSAGE_MAX_CHARS),
|
|
);
|
|
assert.ok(chunks.every((chunk) => chunk.startsWith("**")));
|
|
assert.ok(chunks.every((chunk) => chunk.endsWith("**")));
|
|
assert.equal(
|
|
chunks
|
|
.map((chunk) => chunk.slice(2, -2).length)
|
|
.reduce((sum, length) => sum + length, 0),
|
|
TELEGRAM_RICH_MESSAGE_MAX_CHARS + 500,
|
|
);
|
|
});
|
|
|
|
test("Native Markdown splitter respects the rich-message block limit for lists", () => {
|
|
const markdown = Array.from(
|
|
{ length: TELEGRAM_RICH_MESSAGE_MAX_BLOCKS + 5 },
|
|
(_, index) => `- item ${index + 1}`,
|
|
).join("\n");
|
|
const chunks = splitTelegramNativeMarkdown(markdown);
|
|
assert.equal(chunks.length, 2);
|
|
assert.equal(chunks[0]?.split("\n").length, TELEGRAM_RICH_MESSAGE_MAX_BLOCKS);
|
|
assert.equal(chunks[1]?.split("\n").length, 5);
|
|
});
|
|
|
|
test("Native Markdown delivery attaches reply metadata first and markup last", async () => {
|
|
const bodies: Array<Record<string, unknown>> = [];
|
|
const markdown = `${"a".repeat(TELEGRAM_RICH_MESSAGE_MAX_CHARS - 10)}\n\n${"b".repeat(30)}`;
|
|
const lastId = await sendTelegramNativeMarkdownReply(
|
|
7,
|
|
42,
|
|
markdown,
|
|
{
|
|
sendRichMessage: async (body) => {
|
|
bodies.push(body);
|
|
return { message_id: bodies.length };
|
|
},
|
|
},
|
|
{
|
|
replyMarkup: {
|
|
inline_keyboard: [[{ text: "ok", callback_data: "noop" }]],
|
|
},
|
|
},
|
|
);
|
|
assert.equal(lastId, 2);
|
|
assert.equal(bodies.length, 2);
|
|
assert.deepEqual(bodies[0]?.reply_parameters, {
|
|
message_id: 42,
|
|
allow_sending_without_reply: true,
|
|
});
|
|
assert.equal("reply_markup" in (bodies[0] ?? {}), true);
|
|
assert.equal(bodies[0]?.reply_markup, undefined);
|
|
assert.equal("reply_parameters" in (bodies[1] ?? {}), false);
|
|
assert.deepEqual(bodies[1]?.reply_markup, {
|
|
inline_keyboard: [[{ text: "ok", callback_data: "noop" }]],
|
|
});
|
|
assert.deepEqual(
|
|
(bodies[0]?.rich_message as { markdown?: string })?.markdown,
|
|
"a".repeat(TELEGRAM_RICH_MESSAGE_MAX_CHARS - 10),
|
|
);
|
|
assert.deepEqual(
|
|
(bodies[1]?.rich_message as { markdown?: string })?.markdown,
|
|
"b".repeat(30),
|
|
);
|
|
});
|
|
|
|
test("Reply runtime sends plain replies using the requested parse mode", async () => {
|
|
const sent: string[] = [];
|
|
const messageId = await sendTelegramPlainReply(
|
|
"hello",
|
|
{
|
|
renderTelegramMessage: (_text, options) => [
|
|
{ text: options?.mode === "html" ? "html" : "plain" },
|
|
],
|
|
sendRenderedChunks: async (chunks) => {
|
|
sent.push(chunks[0]?.text ?? "");
|
|
return 7;
|
|
},
|
|
},
|
|
{ parseMode: "HTML" },
|
|
);
|
|
assert.equal(messageId, 7);
|
|
assert.deepEqual(sent, ["html"]);
|
|
});
|
|
|
|
test("Transport reply dedup scopes repeated prompt message ids by chat and thread", () => {
|
|
assert.equal(buildTelegramReplyParameters(1, 0), undefined);
|
|
assert.deepEqual(buildTelegramReplyParameters(1, 42), {
|
|
message_id: 42,
|
|
allow_sending_without_reply: true,
|
|
});
|
|
assert.equal(buildTelegramReplyParameters(1, 42), undefined);
|
|
assert.deepEqual(buildTelegramReplyParameters(2, 42), {
|
|
message_id: 42,
|
|
allow_sending_without_reply: true,
|
|
});
|
|
assert.deepEqual(
|
|
buildTelegramReplyParameters(1, 42, { chatId: 1, threadId: 10 }),
|
|
{
|
|
message_id: 42,
|
|
allow_sending_without_reply: true,
|
|
},
|
|
);
|
|
assert.equal(
|
|
buildTelegramReplyParameters(1, 42, { chatId: 1, threadId: 10 }),
|
|
undefined,
|
|
);
|
|
assert.deepEqual(
|
|
buildTelegramReplyParameters(1, 42, { chatId: 1, threadId: 11 }),
|
|
{
|
|
message_id: 42,
|
|
allow_sending_without_reply: true,
|
|
},
|
|
);
|
|
resetTransportReplyDedup();
|
|
assert.deepEqual(buildTelegramReplyParameters(1, 42), {
|
|
message_id: 42,
|
|
allow_sending_without_reply: true,
|
|
});
|
|
});
|
|
|
|
test("Reply dedup tracks first reply per prompt message id and resets", () => {
|
|
const dedup = createReplyDedupRuntime();
|
|
assert.equal(dedup.shouldReply(42), true);
|
|
assert.equal(dedup.shouldReply(42), false);
|
|
assert.equal(dedup.shouldReply(99), true);
|
|
dedup.reset();
|
|
assert.equal(dedup.shouldReply(42), true);
|
|
});
|
|
|
|
test("Dedup wrapper suppresses reply_to_message_id after the first message in a turn", async () => {
|
|
const dedup = createReplyDedupRuntime();
|
|
const passedReplyIds: Array<number | undefined> = [];
|
|
const inner = async (
|
|
_chatId: number,
|
|
replyToMessageId: number | undefined,
|
|
) => {
|
|
passedReplyIds.push(replyToMessageId);
|
|
return 1;
|
|
};
|
|
const wrapped = dedupSendTextReply(dedup, inner);
|
|
await wrapped(7, 42, "first");
|
|
await wrapped(7, 42, "second");
|
|
await wrapped(7, 99, "other");
|
|
assert.deepEqual(passedReplyIds, [42, undefined, 99]);
|
|
});
|
|
|
|
test("Dedup reset fires on agent_start through lifecycle hook", async () => {
|
|
const dedup = createReplyDedupRuntime();
|
|
dedup.shouldReply(42); // marks replied
|
|
let agentStartCalled = false;
|
|
const hook = createDedupAgentStartHook(dedup, async () => {
|
|
agentStartCalled = true;
|
|
});
|
|
await hook(
|
|
{} as Parameters<typeof hook>[0],
|
|
{} as Parameters<typeof hook>[1],
|
|
);
|
|
assert.equal(agentStartCalled, true);
|
|
assert.equal(
|
|
dedup.shouldReply(42),
|
|
true,
|
|
"reset clears previous reply state",
|
|
);
|
|
});
|