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)
453 lines
12 KiB
TypeScript
453 lines
12 KiB
TypeScript
/**
|
|
* Regression tests for Telegram media and text extraction helpers
|
|
* Covers inbound file-info collection, text extraction, media groups, id collection, and history formatting
|
|
*/
|
|
|
|
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
|
|
import {
|
|
collectTelegramFileInfos,
|
|
collectTelegramMessageIds,
|
|
createTelegramMediaGroupController,
|
|
createTelegramMediaGroupDispatchRuntime,
|
|
downloadTelegramMessageFiles,
|
|
extractFirstTelegramMessageText,
|
|
extractTelegramMessagePromptText,
|
|
extractTelegramMessagesPromptText,
|
|
extractTelegramMessagesText,
|
|
formatTelegramHistoryText,
|
|
getTelegramMediaGroupKey,
|
|
guessMediaType,
|
|
queueTelegramMediaGroupMessage,
|
|
removePendingTelegramMediaGroupMessages,
|
|
type TelegramMediaGroupState,
|
|
} from "../lib/media.ts";
|
|
|
|
type TestTimer = ReturnType<typeof setTimeout>;
|
|
|
|
function createTestTimer(id: number): TestTimer {
|
|
return id as unknown as TestTimer;
|
|
}
|
|
|
|
function getTestTimerId(timer: TestTimer): number {
|
|
return timer as unknown as number;
|
|
}
|
|
|
|
test("Media helpers collect file infos across Telegram message variants", () => {
|
|
const files = collectTelegramFileInfos([
|
|
{
|
|
message_id: 1,
|
|
text: "hello",
|
|
photo: [
|
|
{ file_id: "small", file_size: 1 },
|
|
{ file_id: "large", file_size: 10 },
|
|
],
|
|
document: {
|
|
file_id: "doc",
|
|
file_name: "report.png",
|
|
mime_type: "image/png",
|
|
},
|
|
voice: {
|
|
file_id: "voice",
|
|
mime_type: "audio/ogg",
|
|
},
|
|
sticker: {
|
|
file_id: "sticker",
|
|
},
|
|
},
|
|
]);
|
|
assert.deepEqual(
|
|
files.map((file) => ({
|
|
id: file.file_id,
|
|
name: file.fileName,
|
|
image: file.isImage,
|
|
})),
|
|
[
|
|
{ id: "large", name: "photo-1.jpg", image: true },
|
|
{ id: "doc", name: "report.png", image: true },
|
|
{ id: "voice", name: "voice-1.ogg", image: false },
|
|
{ id: "sticker", name: "sticker-1.webp", image: true },
|
|
],
|
|
);
|
|
});
|
|
|
|
test("Media helpers download collected file infos", async () => {
|
|
const downloaded = await downloadTelegramMessageFiles(
|
|
[
|
|
{
|
|
message_id: 3,
|
|
document: {
|
|
file_id: "doc-id",
|
|
file_name: "report.pdf",
|
|
mime_type: "application/pdf",
|
|
},
|
|
},
|
|
],
|
|
{
|
|
downloadFile: async (fileId, fileName) => `/tmp/${fileId}-${fileName}`,
|
|
},
|
|
);
|
|
assert.deepEqual(downloaded, [
|
|
{
|
|
path: "/tmp/doc-id-report.pdf",
|
|
fileName: "report.pdf",
|
|
isImage: false,
|
|
mimeType: "application/pdf",
|
|
kind: "document",
|
|
},
|
|
]);
|
|
});
|
|
|
|
test("Media helpers extract text, ids, and history summaries", () => {
|
|
const messages = [
|
|
{ message_id: 1, text: "first" },
|
|
{ message_id: 2, caption: "second" },
|
|
{ message_id: 2, text: "duplicate id" },
|
|
];
|
|
assert.equal(
|
|
extractTelegramMessagesText(messages),
|
|
"first\n\nsecond\n\nduplicate id",
|
|
);
|
|
assert.equal(extractFirstTelegramMessageText(messages), "first");
|
|
assert.deepEqual(collectTelegramMessageIds(messages), [1, 2]);
|
|
assert.equal(
|
|
formatTelegramHistoryText(
|
|
"hello",
|
|
[{ path: "/tmp/demo.txt" }],
|
|
["transcript"],
|
|
),
|
|
"hello\n\n[attachments] /tmp\n- /demo.txt\n\n[outputs]\n- transcript",
|
|
);
|
|
});
|
|
|
|
test("Media helpers keep raw text command-safe and add reply context only for prompts", () => {
|
|
const repliedCommand = {
|
|
message_id: 1,
|
|
text: "/status",
|
|
reply_to_message: { text: "quoted context" },
|
|
};
|
|
const captionReply = {
|
|
message_id: 2,
|
|
caption: "caption reply",
|
|
reply_to_message: { caption: "quoted caption" },
|
|
};
|
|
assert.equal(extractFirstTelegramMessageText([repliedCommand]), "/status");
|
|
assert.equal(extractTelegramMessagesText([repliedCommand]), "/status");
|
|
assert.equal(
|
|
extractTelegramMessagePromptText(repliedCommand),
|
|
"/status\n\n[reply] quoted context",
|
|
);
|
|
assert.equal(
|
|
extractTelegramMessagesPromptText([captionReply]),
|
|
"caption reply\n\n[reply] quoted caption",
|
|
);
|
|
});
|
|
|
|
test("Media helpers prefer rich-message text for reply context", () => {
|
|
const message = {
|
|
message_id: 1,
|
|
text: "current",
|
|
reply_to_message: {
|
|
text: "## Raw **Markdown** fallback",
|
|
rich_message: {
|
|
blocks: [
|
|
{ type: "heading", text: "Rendered heading", size: 2 },
|
|
{
|
|
type: "paragraph",
|
|
text: ["Plain ", { type: "bold", text: "reply" }, " text"],
|
|
},
|
|
{
|
|
type: "list",
|
|
items: [
|
|
{
|
|
label: "1.",
|
|
blocks: [{ type: "paragraph", text: "First item" }],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
},
|
|
};
|
|
assert.equal(
|
|
extractTelegramMessagePromptText(message),
|
|
"current\n\n[reply] Rendered heading\n\nPlain reply text\n\n1. First item",
|
|
);
|
|
});
|
|
|
|
test("Media helpers truncate long reply context for prompt text", () => {
|
|
const longQuote = `${"a".repeat(1000)} extra`;
|
|
assert.equal(
|
|
extractTelegramMessagesPromptText([
|
|
{
|
|
message_id: 1,
|
|
text: "current",
|
|
reply_to_message: { text: longQuote },
|
|
},
|
|
]),
|
|
`current\n\n[reply] ${"a".repeat(1000)}…`,
|
|
);
|
|
});
|
|
|
|
test("Media helpers infer image media types from file paths", () => {
|
|
assert.equal(guessMediaType("/tmp/demo.png"), "image/png");
|
|
assert.equal(guessMediaType("/tmp/demo.txt"), undefined);
|
|
});
|
|
|
|
test("Media helpers key messages by chat and media group", () => {
|
|
assert.equal(
|
|
getTelegramMediaGroupKey({
|
|
message_id: 1,
|
|
chat: { id: 7 },
|
|
media_group_id: "album",
|
|
}),
|
|
"7:private:album",
|
|
);
|
|
assert.equal(
|
|
getTelegramMediaGroupKey({
|
|
message_id: 1,
|
|
chat: { id: 7 },
|
|
message_thread_id: 42,
|
|
media_group_id: "album",
|
|
}),
|
|
"7:thread:42:album",
|
|
);
|
|
assert.equal(
|
|
getTelegramMediaGroupKey({ message_id: 1, chat: { id: 7 } }),
|
|
undefined,
|
|
);
|
|
});
|
|
|
|
test("Media helpers replace debounce timers and dispatch grouped messages", async () => {
|
|
const groups = new Map<
|
|
string,
|
|
TelegramMediaGroupState<{
|
|
message_id: number;
|
|
chat: { id: number };
|
|
media_group_id?: string;
|
|
}>
|
|
>();
|
|
const cleared: number[] = [];
|
|
const callbacks: Array<() => void> = [];
|
|
const dispatched: number[][] = [];
|
|
let nextTimer = 1;
|
|
const setTimer = (callback: () => void): TestTimer => {
|
|
callbacks.push(callback);
|
|
return createTestTimer(nextTimer++);
|
|
};
|
|
const clearTimer = (timer: TestTimer): void => {
|
|
cleared.push(getTestTimerId(timer));
|
|
};
|
|
assert.equal(
|
|
queueTelegramMediaGroupMessage({
|
|
message: { message_id: 1, chat: { id: 7 }, media_group_id: "album" },
|
|
groups,
|
|
debounceMs: 100,
|
|
setTimer,
|
|
clearTimer,
|
|
dispatchMessages: (messages) =>
|
|
dispatched.push(messages.map((message) => message.message_id)),
|
|
}),
|
|
true,
|
|
);
|
|
queueTelegramMediaGroupMessage({
|
|
message: { message_id: 2, chat: { id: 7 }, media_group_id: "album" },
|
|
groups,
|
|
debounceMs: 100,
|
|
setTimer,
|
|
clearTimer,
|
|
dispatchMessages: (messages) =>
|
|
dispatched.push(messages.map((message) => message.message_id)),
|
|
});
|
|
assert.deepEqual(cleared, [1]);
|
|
callbacks.at(-1)?.();
|
|
await Promise.resolve();
|
|
assert.deepEqual(dispatched, [[1, 2]]);
|
|
assert.equal(groups.size, 0);
|
|
});
|
|
|
|
test("Media group keeps messages until asynchronous dispatch succeeds", async () => {
|
|
const groups = new Map<
|
|
string,
|
|
TelegramMediaGroupState<{
|
|
message_id: number;
|
|
chat: { id: number };
|
|
media_group_id?: string;
|
|
}>
|
|
>();
|
|
const callbacks: Array<() => void> = [];
|
|
let attempts = 0;
|
|
queueTelegramMediaGroupMessage({
|
|
message: { message_id: 1, chat: { id: 7 }, media_group_id: "album" },
|
|
groups,
|
|
debounceMs: 10,
|
|
setTimer: (callback) => {
|
|
callbacks.push(callback);
|
|
return createTestTimer(callbacks.length);
|
|
},
|
|
clearTimer: () => {},
|
|
dispatchMessages: async () => {
|
|
attempts += 1;
|
|
if (attempts === 1) throw new Error("queue admission failed");
|
|
},
|
|
});
|
|
|
|
callbacks[0]?.();
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
assert.equal(groups.size, 1);
|
|
assert.equal(callbacks.length, 2);
|
|
callbacks[1]?.();
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
assert.equal(attempts, 2);
|
|
assert.equal(groups.size, 0);
|
|
});
|
|
|
|
test("Media group controller owns timers, removal, and cleanup", () => {
|
|
const cleared: number[] = [];
|
|
const callbacks: Array<() => void> = [];
|
|
const dispatched: number[][] = [];
|
|
let nextTimer = 1;
|
|
const controller = createTelegramMediaGroupController<{
|
|
message_id: number;
|
|
chat: { id: number };
|
|
media_group_id?: string;
|
|
}>({
|
|
debounceMs: 100,
|
|
setTimer: (callback) => {
|
|
callbacks.push(callback);
|
|
return createTestTimer(nextTimer++);
|
|
},
|
|
clearTimer: (timer) => {
|
|
cleared.push(getTestTimerId(timer));
|
|
},
|
|
});
|
|
assert.equal(
|
|
controller.queueMessage({
|
|
message: { message_id: 1, chat: { id: 7 }, media_group_id: "album" },
|
|
dispatchMessages: (messages) =>
|
|
dispatched.push(messages.map((message) => message.message_id)),
|
|
}),
|
|
true,
|
|
);
|
|
controller.queueMessage({
|
|
message: { message_id: 2, chat: { id: 7 }, media_group_id: "album" },
|
|
dispatchMessages: (messages) =>
|
|
dispatched.push(messages.map((message) => message.message_id)),
|
|
});
|
|
assert.deepEqual(cleared, [1]);
|
|
assert.equal(controller.removeMessages([2]), 1);
|
|
assert.deepEqual(cleared, [1, 2]);
|
|
callbacks.at(-1)?.();
|
|
assert.equal(dispatched.length, 0);
|
|
controller.queueMessage({
|
|
message: { message_id: 3, chat: { id: 7 }, media_group_id: "album" },
|
|
dispatchMessages: (messages) =>
|
|
dispatched.push(messages.map((message) => message.message_id)),
|
|
});
|
|
controller.clear();
|
|
assert.deepEqual(cleared, [1, 2, 3]);
|
|
});
|
|
|
|
test("Media group suspension preserves admitted messages for replacement context", async () => {
|
|
const callbacks: Array<() => void> = [];
|
|
const cleared: number[] = [];
|
|
const dispatched: Array<{ ids: number[]; ctx?: string }> = [];
|
|
let nextTimer = 1;
|
|
const controller = createTelegramMediaGroupController<
|
|
{ message_id: number; chat: { id: number }; media_group_id?: string },
|
|
string
|
|
>({
|
|
debounceMs: 100,
|
|
setTimer: (callback) => {
|
|
callbacks.push(callback);
|
|
return createTestTimer(nextTimer++);
|
|
},
|
|
clearTimer: (timer) => cleared.push(getTestTimerId(timer)),
|
|
});
|
|
controller.queueMessage({
|
|
message: { message_id: 1, chat: { id: 7 }, media_group_id: "album" },
|
|
context: "old-session",
|
|
dispatchMessages: (messages, ctx) =>
|
|
dispatched.push({
|
|
ids: messages.map((message) => message.message_id),
|
|
ctx,
|
|
}),
|
|
});
|
|
|
|
controller.suspend();
|
|
controller.resume("new-session");
|
|
callbacks[1]?.();
|
|
await Promise.resolve();
|
|
|
|
assert.deepEqual(cleared, [1]);
|
|
assert.deepEqual(dispatched, [{ ids: [1], ctx: "new-session" }]);
|
|
});
|
|
|
|
test("Media group dispatch runtime handles immediate and grouped messages", async () => {
|
|
const callbacks: Array<() => void> = [];
|
|
const dispatched: Array<{ ids: number[]; ctx: string }> = [];
|
|
const controller = createTelegramMediaGroupController<
|
|
{
|
|
message_id: number;
|
|
chat: { id: number };
|
|
media_group_id?: string;
|
|
},
|
|
string
|
|
>({
|
|
setTimer: (callback) => {
|
|
callbacks.push(callback);
|
|
return createTestTimer(callbacks.length);
|
|
},
|
|
clearTimer: () => {},
|
|
});
|
|
const runtime = createTelegramMediaGroupDispatchRuntime({
|
|
mediaGroups: controller,
|
|
dispatchMessages: async (messages, ctx: string) => {
|
|
dispatched.push({
|
|
ids: messages.map((message) => message.message_id),
|
|
ctx,
|
|
});
|
|
},
|
|
});
|
|
await runtime.handleMessage({ message_id: 1, chat: { id: 7 } }, "ctx-a");
|
|
await runtime.handleMessage(
|
|
{ message_id: 2, chat: { id: 7 }, media_group_id: "album" },
|
|
"ctx-b",
|
|
);
|
|
await runtime.handleMessage(
|
|
{ message_id: 3, chat: { id: 7 }, media_group_id: "album" },
|
|
"ctx-b",
|
|
);
|
|
callbacks.at(-1)?.();
|
|
assert.deepEqual(dispatched, [
|
|
{ ids: [1], ctx: "ctx-a" },
|
|
{ ids: [2, 3], ctx: "ctx-b" },
|
|
]);
|
|
});
|
|
|
|
test("Media helpers remove pending groups by message id", () => {
|
|
const groups = new Map<
|
|
string,
|
|
TelegramMediaGroupState<{ message_id: number; chat: { id: number } }>
|
|
>();
|
|
groups.set("7:album", {
|
|
messages: [
|
|
{ message_id: 1, chat: { id: 7 } },
|
|
{ message_id: 2, chat: { id: 7 } },
|
|
],
|
|
flushTimer: createTestTimer(10),
|
|
});
|
|
const cleared: number[] = [];
|
|
assert.equal(
|
|
removePendingTelegramMediaGroupMessages(groups, [2], (timer) => {
|
|
cleared.push(getTestTimerId(timer));
|
|
}),
|
|
1,
|
|
);
|
|
assert.deepEqual(cleared, [10]);
|
|
assert.equal(groups.size, 0);
|
|
});
|