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)
354 lines
9.3 KiB
TypeScript
354 lines
9.3 KiB
TypeScript
/**
|
|
* Regression tests for Telegram bus-aware API runtime
|
|
* Verifies follower outbound API calls route through the local bus while leaders use direct transport
|
|
*/
|
|
|
|
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
|
|
import { createTelegramBusAwareApiRuntime } from "../lib/bus-api.ts";
|
|
import type { TelegramBridgeApiRuntime } from "../lib/telegram-api.ts";
|
|
|
|
function createDirectRuntime(calls: unknown[]): TelegramBridgeApiRuntime {
|
|
return {
|
|
call: async <TResponse>(method: string, body: Record<string, unknown>) => {
|
|
calls.push({ kind: "call", method, body });
|
|
return { ok: true } as TResponse;
|
|
},
|
|
callMultipart: async <TResponse>(
|
|
method: string,
|
|
fields: Record<string, string>,
|
|
fileField: string,
|
|
filePath: string,
|
|
fileName: string,
|
|
) => {
|
|
calls.push({
|
|
kind: "multipart",
|
|
method,
|
|
fields,
|
|
fileField,
|
|
filePath,
|
|
fileName,
|
|
});
|
|
return { ok: true } as TResponse;
|
|
},
|
|
downloadFile: async (fileId, suggestedName) => {
|
|
calls.push({ kind: "download", fileId, suggestedName });
|
|
return "/tmp/file";
|
|
},
|
|
deleteWebhook: async () => {
|
|
calls.push({ kind: "delete-webhook" });
|
|
return true;
|
|
},
|
|
getUpdates: async (body) => {
|
|
calls.push({ kind: "get-updates", body });
|
|
return [];
|
|
},
|
|
setMyCommands: async (commands) => {
|
|
calls.push({ kind: "commands", commands });
|
|
return true;
|
|
},
|
|
sendChatAction: async (chatId, action, options) => {
|
|
calls.push({ kind: "chat-action", chatId, action, options });
|
|
return true;
|
|
},
|
|
sendTypingAction: async (chatId, options) => {
|
|
calls.push({ kind: "typing", chatId, options });
|
|
return true;
|
|
},
|
|
sendRecordVoiceAction: async (chatId, options) => {
|
|
calls.push({ kind: "record-voice", chatId, options });
|
|
return true;
|
|
},
|
|
sendMessageDraft: async (chatId, draftId, text, options) => {
|
|
calls.push({ kind: "draft", chatId, draftId, text, options });
|
|
return true;
|
|
},
|
|
sendMessage: async (body) => {
|
|
calls.push({ kind: "message", body });
|
|
return { message_id: 1 };
|
|
},
|
|
sendRichMessage: async (body) => {
|
|
calls.push({ kind: "rich", body });
|
|
return { message_id: 2 };
|
|
},
|
|
sendRichMessageDraft: async (body) => {
|
|
calls.push({ kind: "rich-draft", body });
|
|
return true;
|
|
},
|
|
editMessageText: async (body) => {
|
|
calls.push({ kind: "edit", body });
|
|
return "edited";
|
|
},
|
|
answerCallbackQuery: async (callbackQueryId, text) => {
|
|
calls.push({ kind: "answer-callback", callbackQueryId, text });
|
|
},
|
|
answerGuestQuery: async (guestQueryId, text) => {
|
|
calls.push({ kind: "answer-guest", guestQueryId, text });
|
|
},
|
|
deleteMessage: async (chatId, messageId) => {
|
|
calls.push({ kind: "delete", chatId, messageId });
|
|
},
|
|
prepareTempDir: async () => 0,
|
|
};
|
|
}
|
|
|
|
test("Bus-aware API runtime uses direct transport while this instance owns Telegram", async () => {
|
|
const directCalls: unknown[] = [];
|
|
const busCalls: unknown[] = [];
|
|
const runtime = createTelegramBusAwareApiRuntime({
|
|
directRuntime: createDirectRuntime(directCalls),
|
|
ownsDirect: () => true,
|
|
callFollowerApi: async (method, args) => {
|
|
busCalls.push({ method, args });
|
|
return { message_id: 99 };
|
|
},
|
|
});
|
|
|
|
assert.deepEqual(
|
|
await runtime.sendRichMessage({
|
|
chat_id: 1,
|
|
rich_message: { markdown: "hi" },
|
|
}),
|
|
{
|
|
message_id: 2,
|
|
},
|
|
);
|
|
assert.deepEqual(directCalls, [
|
|
{ kind: "rich", body: { chat_id: 1, rich_message: { markdown: "hi" } } },
|
|
]);
|
|
assert.deepEqual(busCalls, []);
|
|
});
|
|
|
|
test("Bus-aware polling methods fail closed without direct ownership", async () => {
|
|
const directCalls: unknown[] = [];
|
|
const busCalls: unknown[] = [];
|
|
const runtime = createTelegramBusAwareApiRuntime({
|
|
directRuntime: createDirectRuntime(directCalls),
|
|
ownsDirect: () => false,
|
|
callFollowerApi: async (method, args) => {
|
|
busCalls.push({ method, args });
|
|
return true;
|
|
},
|
|
});
|
|
|
|
await assert.rejects(
|
|
runtime.deleteWebhook(),
|
|
/deleteWebhook requires direct transport ownership/,
|
|
);
|
|
await assert.rejects(
|
|
runtime.getUpdates({ offset: 1 }),
|
|
/getUpdates requires direct transport ownership/,
|
|
);
|
|
assert.deepEqual(directCalls, []);
|
|
assert.deepEqual(busCalls, []);
|
|
});
|
|
|
|
test("Bus-aware API runtime routes follower outbound calls through the leader", async () => {
|
|
const directCalls: unknown[] = [];
|
|
const busCalls: unknown[] = [];
|
|
const runtime = createTelegramBusAwareApiRuntime({
|
|
directRuntime: createDirectRuntime(directCalls),
|
|
ownsDirect: () => false,
|
|
callFollowerApi: async (method, args) => {
|
|
busCalls.push({ method, args });
|
|
if (args[0] === "sendRichMessage") return { message_id: 77 };
|
|
if (method === "downloadFile") return "/tmp/leader-photo.png";
|
|
return true;
|
|
},
|
|
});
|
|
|
|
assert.deepEqual(
|
|
await runtime.sendRichMessage({
|
|
chat_id: 1,
|
|
rich_message: { markdown: "hi" },
|
|
}),
|
|
{
|
|
message_id: 77,
|
|
},
|
|
);
|
|
assert.equal(
|
|
await runtime.sendChatAction(1, "typing", { message_thread_id: 5 }),
|
|
true,
|
|
);
|
|
assert.equal(
|
|
await runtime.call("sendChatAction", { chat_id: 1, action: "typing" }),
|
|
true,
|
|
);
|
|
assert.equal(
|
|
await runtime.sendMessageDraft(1, 2, "draft", { message_thread_id: 5 }),
|
|
true,
|
|
);
|
|
await runtime.deleteMessage(1, 9);
|
|
await runtime.answerCallbackQuery("cb1", "Done");
|
|
await runtime.answerGuestQuery("guest1", "Hello", { parseMode: "Markdown" });
|
|
await runtime.answerGuestQuery("guest2", undefined, {
|
|
result: {
|
|
type: "photo",
|
|
id: "photo-1",
|
|
photo_file_id: "cached-photo",
|
|
caption: "Result",
|
|
},
|
|
});
|
|
assert.equal(
|
|
await runtime.downloadFile("file1", "photo.png"),
|
|
"/tmp/leader-photo.png",
|
|
);
|
|
|
|
assert.deepEqual(directCalls, []);
|
|
assert.deepEqual(busCalls, [
|
|
{
|
|
method: "call",
|
|
args: [
|
|
"sendRichMessage",
|
|
{ chat_id: 1, rich_message: { markdown: "hi" } },
|
|
],
|
|
},
|
|
{
|
|
method: "call",
|
|
args: [
|
|
"sendChatAction",
|
|
{ chat_id: 1, action: "typing", message_thread_id: 5 },
|
|
],
|
|
},
|
|
{
|
|
method: "call",
|
|
args: ["sendChatAction", { chat_id: 1, action: "typing" }, undefined],
|
|
},
|
|
{
|
|
method: "call",
|
|
args: [
|
|
"sendMessageDraft",
|
|
{ chat_id: 1, draft_id: 2, text: "draft", message_thread_id: 5 },
|
|
],
|
|
},
|
|
{
|
|
method: "call",
|
|
args: ["deleteMessage", { chat_id: 1, message_id: 9 }],
|
|
},
|
|
{
|
|
method: "call",
|
|
args: ["answerCallbackQuery", { callback_query_id: "cb1", text: "Done" }],
|
|
},
|
|
{
|
|
method: "call",
|
|
args: [
|
|
"answerGuestQuery",
|
|
{
|
|
guest_query_id: "guest1",
|
|
result: {
|
|
type: "article",
|
|
id: "1",
|
|
title: "Response",
|
|
input_message_content: {
|
|
message_text: "Hello",
|
|
parse_mode: "Markdown",
|
|
},
|
|
},
|
|
},
|
|
],
|
|
},
|
|
{
|
|
method: "call",
|
|
args: [
|
|
"answerGuestQuery",
|
|
{
|
|
guest_query_id: "guest2",
|
|
result: {
|
|
type: "photo",
|
|
id: "photo-1",
|
|
photo_file_id: "cached-photo",
|
|
caption: "Result",
|
|
},
|
|
},
|
|
],
|
|
},
|
|
{
|
|
method: "downloadFile",
|
|
args: ["file1", "photo.png"],
|
|
},
|
|
]);
|
|
});
|
|
|
|
test("Bus-aware API runtime applies follower default thread to scoped actions", async () => {
|
|
const busCalls: unknown[] = [];
|
|
const runtime = createTelegramBusAwareApiRuntime({
|
|
directRuntime: createDirectRuntime([]),
|
|
ownsDirect: () => false,
|
|
getDefaultTarget: () => ({ chatId: 1, threadId: 5 }),
|
|
callFollowerApi: async (method, args) => {
|
|
busCalls.push({ method, args });
|
|
return true;
|
|
},
|
|
});
|
|
|
|
await runtime.sendTypingAction(1);
|
|
await runtime.sendRecordVoiceAction(1);
|
|
await runtime.sendMessageDraft(1, 2, "draft");
|
|
await runtime.sendTypingAction(2);
|
|
|
|
assert.deepEqual(busCalls, [
|
|
{
|
|
method: "call",
|
|
args: [
|
|
"sendChatAction",
|
|
{ chat_id: 1, action: "typing", message_thread_id: 5 },
|
|
],
|
|
},
|
|
{
|
|
method: "call",
|
|
args: [
|
|
"sendChatAction",
|
|
{ chat_id: 1, action: "record_voice", message_thread_id: 5 },
|
|
],
|
|
},
|
|
{
|
|
method: "call",
|
|
args: [
|
|
"sendMessageDraft",
|
|
{ chat_id: 1, draft_id: 2, text: "draft", message_thread_id: 5 },
|
|
],
|
|
},
|
|
{
|
|
method: "call",
|
|
args: ["sendChatAction", { chat_id: 2, action: "typing" }],
|
|
},
|
|
]);
|
|
});
|
|
|
|
test("Bus-aware API runtime routes follower multipart uploads through the leader", async () => {
|
|
const busCalls: unknown[] = [];
|
|
const runtime = createTelegramBusAwareApiRuntime({
|
|
directRuntime: createDirectRuntime([]),
|
|
ownsDirect: () => false,
|
|
callFollowerApi: async (method, args) => {
|
|
busCalls.push({ method, args });
|
|
return { ok: true };
|
|
},
|
|
});
|
|
|
|
assert.deepEqual(
|
|
await runtime.callMultipart(
|
|
"sendDocument",
|
|
{ chat_id: "1" },
|
|
"document",
|
|
"/tmp/a.txt",
|
|
"a.txt",
|
|
),
|
|
{ ok: true },
|
|
);
|
|
assert.deepEqual(busCalls, [
|
|
{
|
|
method: "callMultipart",
|
|
args: [
|
|
"sendDocument",
|
|
{ chat_id: "1" },
|
|
"document",
|
|
"/tmp/a.txt",
|
|
"a.txt",
|
|
undefined,
|
|
],
|
|
},
|
|
]);
|
|
});
|