chore: fork @llblab/[email protected].0 as @evandrodevbr/oh-my-pi-telegram
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)
This commit is contained in:
commit
d1d04001d0
155 files changed
+107848
No files matched your search
@@ -0,0 +1,452 @@
|
||||
/**
|
||||
* 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);
|
||||
});
|
||||
Reference in new issue
Block a user