Files
Evandro Fonseca Junior d1d04001d0
Release / release (push) Failing after 5s
Validate / validate (push) Failing after 5m21s
chore: fork @llblab/[email protected] 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)
2026-07-15 14:18:16 -03:00

1139 lines
33 KiB
TypeScript

/**
* Regression tests for Telegram API helpers
* Verifies direct helper behavior around missing tokens, callback-query failures, downloads, and runtime transport binding
*/
import assert from "node:assert/strict";
import {
mkdir,
mkdtemp,
readdir,
readFile,
stat,
utimes,
writeFile,
} from "node:fs/promises";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import test from "node:test";
import {
answerTelegramCallbackQuery,
callTelegram,
callTelegramMultipart,
cleanupTelegramTempFiles,
createDefaultTelegramBridgeApiRuntime,
createTelegramApiClient,
createTelegramAssistantDraftSender,
createTelegramBridgeApiRuntime,
createTelegramChatActionSender,
createTelegramNativeMarkdownDraftSender,
downloadTelegramFile,
fetchTelegramBotIdentity,
getTelegramInboundFileByteLimitFromEnv,
isTelegramApiCommitUnknownError,
isTelegramMessageNotModifiedError,
setTelegramApiHttpsFetchForTesting,
prepareTelegramTempDir,
TELEGRAM_FILE_MAX_BYTES,
type TelegramApiClient,
} from "../lib/telegram-api.ts";
function createApiResponseBody(result: unknown): { ok: true; result: unknown } {
return { ok: true, result };
}
function createApiJsonResponse(result: unknown): Response {
const body = createApiResponseBody(result);
return {
ok: true,
status: 200,
json: async () => body,
text: async () => JSON.stringify(body),
} as Response;
}
function createApiErrorResponse(
status: number,
description: string,
headers?: Headers,
): Response {
return {
ok: false,
status,
headers,
text: async () => JSON.stringify({ ok: false, description }),
} as Response;
}
function createMalformedApiTextResponse(text: string): Response {
return {
ok: true,
status: 200,
text: async () => text,
} as Response;
}
function getApiTestFetchUrl(input: string | URL | Request): string {
return typeof input === "string" ? input : input.toString();
}
function setApiTestFetch(fetchImpl: typeof fetch): () => void {
const originalFetch = globalThis.fetch;
globalThis.fetch = fetchImpl;
return () => {
globalThis.fetch = originalFetch;
};
}
function setApiTestNetworkFamily(value: string | undefined): () => void {
const previous = process.env.PI_TELEGRAM_NETWORK_FAMILY;
if (value === undefined) delete process.env.PI_TELEGRAM_NETWORK_FAMILY;
else process.env.PI_TELEGRAM_NETWORK_FAMILY = value;
return () => {
if (previous === undefined) delete process.env.PI_TELEGRAM_NETWORK_FAMILY;
else process.env.PI_TELEGRAM_NETWORK_FAMILY = previous;
};
}
function createSyntheticFetchFailure(): TypeError {
const ipv6Error = Object.assign(new Error("connect ENETUNREACH"), {
code: "ENETUNREACH",
address: "2a0a:f280::1",
port: 443,
family: 6,
});
const ipv4Error = Object.assign(new Error("connect ETIMEDOUT"), {
code: "ETIMEDOUT",
address: "149.154.167.220",
port: 443,
family: 4,
});
return new TypeError("fetch failed", {
cause: new AggregateError([ipv6Error, ipv4Error], "connect failed"),
});
}
function hasApiTestFamily(family: unknown): boolean {
return family === 4 || family === 6;
}
function createApiRuntimeClient(
overrides: Partial<TelegramApiClient> = {},
): TelegramApiClient {
return {
call: async <TResponse>() => true as TResponse,
callMultipart: async <TResponse>() => true as TResponse,
downloadFile: async () => "/tmp/file",
answerCallbackQuery: async () => {},
...overrides,
};
}
test("Telegram API byte-limit helpers expose the inbound file default", () => {
assert.equal(TELEGRAM_FILE_MAX_BYTES, 50 * 1024 * 1024);
assert.equal(
getTelegramInboundFileByteLimitFromEnv({}, []),
TELEGRAM_FILE_MAX_BYTES,
);
});
test("Telegram API byte-limit config prefers positive integer env values", () => {
assert.equal(
getTelegramInboundFileByteLimitFromEnv(
{ PI_TELEGRAM_INBOUND_FILE_MAX_BYTES: "12345" },
["PI_TELEGRAM_INBOUND_FILE_MAX_BYTES"],
99,
),
12345,
);
assert.equal(
getTelegramInboundFileByteLimitFromEnv(
{
PI_TELEGRAM_INBOUND_FILE_MAX_BYTES: "0",
TELEGRAM_MAX_FILE_SIZE_BYTES: "bad",
},
["PI_TELEGRAM_INBOUND_FILE_MAX_BYTES", "TELEGRAM_MAX_FILE_SIZE_BYTES"],
99,
),
99,
);
});
test("Telegram API helpers detect unchanged edit errors", () => {
assert.equal(
isTelegramMessageNotModifiedError(
new Error("Bad Request: message is not modified"),
),
true,
);
assert.equal(isTelegramMessageNotModifiedError(new Error("other")), false);
});
test("Telegram API chat-action sender binds a fixed action", async () => {
const calls: Array<[number, string, number | undefined]> = [];
const sendTyping = createTelegramChatActionSender(
async (chatId, action, options) => {
calls.push([chatId, action, options?.message_thread_id]);
},
"typing",
);
await sendTyping(7, { message_thread_id: 42 });
assert.deepEqual(calls, [[7, "typing", 42]]);
});
test("Telegram bridge API runtime includes thread target on chat actions", async () => {
const calls: Array<{ method: string; body: Record<string, unknown> }> = [];
const runtime = createTelegramBridgeApiRuntime({
client: createApiRuntimeClient({
call: async <TResponse>(
method: string,
body: Record<string, unknown>,
) => {
calls.push({ method, body });
return true as TResponse;
},
}),
tempDir: "/tmp",
maxFileSizeBytes: 1,
tempFileMaxAgeMs: 1,
recordRuntimeEvent: () => {},
});
await runtime.sendTypingAction(7, { message_thread_id: 42 });
await runtime.sendChatAction(7, "upload_document", { message_thread_id: 42 });
assert.deepEqual(calls, [
{
method: "sendChatAction",
body: { chat_id: 7, action: "typing", message_thread_id: 42 },
},
{
method: "sendChatAction",
body: { chat_id: 7, action: "upload_document", message_thread_id: 42 },
},
]);
});
test("Telegram native Markdown draft sender disables automatic entity detection", async () => {
const richBodies: Record<string, unknown>[] = [];
const legacyCalls: unknown[] = [];
const sendDraft = createTelegramNativeMarkdownDraftSender({
sendMessageDraft: async (...args) => {
legacyCalls.push(args);
return true;
},
sendRichMessageDraft: async (body) => {
richBodies.push(body);
return true;
},
});
await sendDraft(7, 9, "#tag /cmd https://example.com");
await sendDraft(7, 10, undefined);
assert.deepEqual(richBodies, [
{
chat_id: 7,
draft_id: 9,
rich_message: {
markdown: "#tag /cmd https://example.com",
skip_entity_detection: true,
},
},
]);
assert.equal(legacyCalls.length, 1);
});
test("Telegram assistant draft sender follows final rendering mode", async () => {
const richBodies: Record<string, unknown>[] = [];
const legacyCalls: unknown[] = [];
const sendDraft = createTelegramAssistantDraftSender({
getAssistantRenderingMode: () => "html",
renderMarkdownToHtmlDraft: (markdown) => `<b>${markdown}</b>`,
sendMessageDraft: async (...args) => {
legacyCalls.push(args);
return true;
},
sendRichMessageDraft: async (body) => {
richBodies.push(body);
return true;
},
});
await sendDraft(7, 9, "**draft**", { message_thread_id: 42 });
await sendDraft(7, 10, undefined, { message_thread_id: 42 });
assert.deepEqual(richBodies, []);
assert.deepEqual(legacyCalls, [
[7, 9, "<b>**draft**</b>", { message_thread_id: 42, parse_mode: "HTML" }],
[7, 10, undefined, { message_thread_id: 42 }],
]);
});
test("Telegram API helper fetches bot identity through getMe", async () => {
const response = await fetchTelegramBotIdentity("123:abc", async (url) => {
assert.equal(String(url), "https://api.telegram.org/bot123:abc/getMe");
return createApiJsonResponse({
id: 1,
is_bot: true,
first_name: "Demo",
username: "demo",
});
});
assert.equal(response.ok, true);
assert.equal(response.result?.username, "demo");
});
test("Telegram API helper fetches bot identity through IPv4 fallback", async () => {
const familySeen: boolean[] = [];
const restoreEnv = setApiTestNetworkFamily("ipv4-fallback");
const restoreFetch = setApiTestFetch(async () => {
familySeen.push(false);
throw createSyntheticFetchFailure();
});
const restoreHttpsFetch = setTelegramApiHttpsFetchForTesting(
async (_input, _init, family) => {
familySeen.push(hasApiTestFamily(family));
return createApiJsonResponse({
id: 1,
is_bot: true,
first_name: "Demo",
username: "demo",
});
},
);
try {
const response = await fetchTelegramBotIdentity("123:abc");
assert.equal(response.ok, true);
assert.equal(response.result?.username, "demo");
assert.deepEqual(familySeen, [false, true]);
} finally {
restoreHttpsFetch();
restoreFetch();
restoreEnv();
}
});
test("Telegram temp cleanup removes only stale files", async () => {
const tempDir = await mkdtemp(join(tmpdir(), "pi-telegram-cleanup-"));
const oldFile = join(tempDir, "old.txt");
const freshFile = join(tempDir, "fresh.txt");
const nestedDir = join(tempDir, "nested");
await writeFile(oldFile, "old", "utf8");
await writeFile(freshFile, "fresh", "utf8");
await mkdir(nestedDir);
await writeFile(join(nestedDir, "keep.txt"), "keep", "utf8");
await utimes(oldFile, new Date(1_000), new Date(1_000));
await utimes(freshFile, new Date(10_000), new Date(10_000));
assert.equal(await cleanupTelegramTempFiles(tempDir, 5_000, 11_000), 1);
assert.deepEqual((await readdir(tempDir)).sort(), ["fresh.txt", "nested"]);
});
test("Telegram temp preparation creates the directory and removes stale files", async () => {
const parentDir = await mkdtemp(
join(tmpdir(), "pi-telegram-prepare-parent-"),
);
const tempDir = join(parentDir, "nested", "telegram");
assert.equal(await prepareTelegramTempDir(tempDir, 5_000), 0);
assert.equal((await stat(tempDir)).mode & 0o777, 0o700);
const oldFile = join(tempDir, "old.txt");
await writeFile(oldFile, "old", "utf8");
await utimes(oldFile, new Date(1_000), new Date(1_000));
assert.equal(await prepareTelegramTempDir(tempDir, 5_000), 1);
assert.deepEqual(await readdir(tempDir), []);
});
test("Telegram API helpers reject missing bot token for direct calls", async () => {
await assert.rejects(() => callTelegram(undefined, "getMe", {}), {
message: "Telegram bot token is not configured",
});
await assert.rejects(
() =>
downloadTelegramFile(
undefined,
"file-id",
"demo.txt",
join(tmpdir(), "pi-telegram-missing-token"),
),
{
message: "Telegram bot token is not configured",
},
);
});
test("Telegram API helpers include HTTP status details for failed responses", async () => {
const restoreFetch = setApiTestFetch(async () => {
return createApiErrorResponse(429, "Too Many Requests");
});
try {
await assert.rejects(
() => callTelegram("123:abc", "sendMessage", {}, { maxAttempts: 1 }),
{
message: "Telegram API sendMessage failed: HTTP 429: Too Many Requests",
},
);
} finally {
restoreFetch();
}
});
test("Non-idempotent Telegram API calls retry explicit 429 but report 5xx commit unknown", async () => {
const sleeps: number[] = [];
let calls = 0;
const restoreFetch = setApiTestFetch(async () => {
calls += 1;
if (calls === 1) {
return createApiErrorResponse(
429,
"Too Many Requests",
new Headers({ "retry-after": "2" }),
);
}
if (calls === 2) {
return createApiErrorResponse(502, "Bad Gateway");
}
return createApiJsonResponse("sent");
});
try {
await assert.rejects(
() =>
callTelegram<string>("123:abc", "sendMessage", {}, {
retryBaseDelayMs: 10,
sleep: async (ms) => {
sleeps.push(ms);
},
}),
(error) =>
isTelegramApiCommitUnknownError(error) &&
error.method === "sendMessage",
);
assert.equal(calls, 2);
assert.deepEqual(sleeps, [2000]);
} finally {
restoreFetch();
}
});
test("Retry-safe Telegram API methods replay 5xx responses", async () => {
let calls = 0;
const restoreFetch = setApiTestFetch(async () => {
calls += 1;
return calls === 1
? createApiErrorResponse(502, "Bad Gateway")
: createApiJsonResponse("edited");
});
try {
assert.equal(
await callTelegram<string>(
"123:abc",
"editMessageText",
{},
{ retryBaseDelayMs: 0, sleep: async () => {} },
),
"edited",
);
assert.equal(calls, 2);
} finally {
restoreFetch();
}
});
test("Retry-safe Telegram API transport falls back to IPv4 once", async () => {
const familySeen: boolean[] = [];
let calls = 0;
const restoreEnv = setApiTestNetworkFamily("ipv4-fallback");
const restoreFetch = setApiTestFetch(async () => {
calls += 1;
familySeen.push(false);
throw createSyntheticFetchFailure();
});
const restoreHttpsFetch = setTelegramApiHttpsFetchForTesting(
async (_input, _init, family) => {
calls += 1;
familySeen.push(hasApiTestFamily(family));
return createApiJsonResponse("sent");
},
);
try {
assert.equal(
await callTelegram<string>(
"123:abc",
"editMessageText",
{},
{
maxAttempts: 1,
},
),
"sent",
);
assert.equal(calls, 2);
assert.deepEqual(familySeen, [false, true]);
} finally {
restoreHttpsFetch();
restoreFetch();
restoreEnv();
}
});
test("Telegram API transport does not IPv4-fallback retry HTTP 400", async () => {
const familySeen: boolean[] = [];
const restoreEnv = setApiTestNetworkFamily("ipv4-fallback");
const restoreFetch = setApiTestFetch(async () => {
familySeen.push(false);
return createApiErrorResponse(400, "Bad Request");
});
try {
await assert.rejects(
() => callTelegram("123:abc", "sendMessage", {}, { maxAttempts: 1 }),
{
message: "Telegram API sendMessage failed: HTTP 400: Bad Request",
},
);
assert.deepEqual(familySeen, [false]);
} finally {
restoreFetch();
restoreEnv();
}
});
test("Telegram multipart transport failure reports commit unknown without fallback", async () => {
const tempDir = await mkdtemp(join(tmpdir(), "pi-telegram-upload-fallback-"));
const filePath = join(tempDir, "demo.txt");
await writeFile(filePath, "hello", "utf8");
const formStates: string[] = [];
const forms = new Set<FormData>();
let calls = 0;
const restoreEnv = setApiTestNetworkFamily("ipv4-fallback");
const restoreFetch = setApiTestFetch(async (_input, init) => {
calls += 1;
const form = init?.body as FormData;
forms.add(form);
formStates.push(
`auto:${form.get("document") instanceof Blob ? "blob" : "missing"}`,
);
throw createSyntheticFetchFailure();
});
const restoreHttpsFetch = setTelegramApiHttpsFetchForTesting(
async (_input, init, family) => {
calls += 1;
formStates.push(
`${hasApiTestFamily(family) ? "ipv4" : "auto"}:${
init.body instanceof Uint8Array ? "buffer" : "missing"
}`,
);
return createApiJsonResponse(true);
},
);
try {
await assert.rejects(
() =>
callTelegramMultipart<boolean>(
"123:abc",
"sendDocument",
{ chat_id: "1" },
"document",
filePath,
"demo.txt",
{ maxAttempts: 1 },
),
(error) => isTelegramApiCommitUnknownError(error),
);
assert.deepEqual(formStates, ["auto:blob"]);
assert.equal(forms.size, 1);
assert.equal(calls, 1);
} finally {
restoreHttpsFetch();
restoreFetch();
restoreEnv();
}
});
test("Telegram file downloads use transport fallback for file content", async () => {
const tempDir = await mkdtemp(
join(tmpdir(), "pi-telegram-download-fallback-"),
);
const calls: string[] = [];
const restoreEnv = setApiTestNetworkFamily("ipv4-fallback");
const restoreFetch = setApiTestFetch(async (input) => {
const url = getApiTestFetchUrl(input);
if (url.includes("/getFile")) {
return createApiJsonResponse({ file_path: "files/demo" });
}
calls.push("auto");
throw createSyntheticFetchFailure();
});
const restoreHttpsFetch = setTelegramApiHttpsFetchForTesting(
async (_input, _init, family) => {
calls.push(hasApiTestFamily(family) ? "ipv4" : "auto");
return new Response("hello", { status: 200 });
},
);
try {
const path = await downloadTelegramFile(
"123:abc",
"file-id",
"demo.txt",
tempDir,
);
assert.deepEqual(calls, ["auto", "ipv4"]);
assert.equal(await readFile(path, "utf8"), "hello");
} finally {
restoreHttpsFetch();
restoreFetch();
restoreEnv();
}
});
test("Telegram transport diagnostics serialize nested fetch causes", async () => {
const events: Array<Record<string, unknown>> = [];
const runtime = createTelegramBridgeApiRuntime({
tempDir: "/tmp/telegram",
maxFileSizeBytes: 123,
tempFileMaxAgeMs: 60_000,
recordRuntimeEvent: (_kind, _error, details) => {
events.push(details ?? {});
},
client: createApiRuntimeClient({
call: async () => {
throw createSyntheticFetchFailure();
},
}),
});
await assert.rejects(() => runtime.call("sendMessage", {}), {
message: "fetch failed",
});
assert.deepEqual(events, [
{
method: "sendMessage",
transport: {
error: { name: "TypeError", message: "fetch failed" },
cause: { name: "AggregateError", message: "connect failed" },
attempts: [
{
name: "Error",
code: "ENETUNREACH",
address: "2a0a:f280::1",
port: 443,
family: 6,
},
{
name: "Error",
code: "ETIMEDOUT",
address: "149.154.167.220",
port: 443,
family: 4,
},
],
},
},
]);
});
test("Telegram multipart 5xx reports commit unknown without replay", async () => {
const tempDir = await mkdtemp(join(tmpdir(), "pi-telegram-upload-"));
const filePath = join(tempDir, "demo.txt");
await writeFile(filePath, "hello", "utf8");
const contentTypes: string[] = [];
let calls = 0;
const restoreFetch = setApiTestFetch(async (_input, init) => {
calls += 1;
contentTypes.push(
(init?.body as FormData).get("document") instanceof Blob
? "blob"
: "missing",
);
if (calls === 1) {
return createApiErrorResponse(500, "Server Error");
}
return createApiJsonResponse(true);
});
try {
await assert.rejects(
() =>
callTelegramMultipart<boolean>(
"123:abc",
"sendDocument",
{ chat_id: "1" },
"document",
filePath,
"demo.txt",
{ retryBaseDelayMs: 0, sleep: async () => {} },
),
(error) => isTelegramApiCommitUnknownError(error),
);
assert.equal(calls, 1);
assert.deepEqual(contentTypes, ["blob"]);
} finally {
restoreFetch();
}
});
test("Telegram file downloads use unique sanitized temp file names", async () => {
const tempDir = await mkdtemp(join(tmpdir(), "pi-telegram-download-"));
const restoreFetch = setApiTestFetch(async (input) => {
const url = getApiTestFetchUrl(input);
if (url.includes("/getFile")) {
return createApiJsonResponse({ file_path: "files/demo" });
}
return new Response("hello", { status: 200 });
});
try {
const path = await downloadTelegramFile(
"123:abc",
"file-id",
"bad name?.txt",
tempDir,
);
assert.match(path, /[0-9a-f-]{36}-bad_name_\.txt$/);
assert.equal(await readFile(path, "utf8"), "hello");
assert.equal((await stat(tempDir)).mode & 0o777, 0o700);
assert.equal((await stat(path)).mode & 0o777, 0o600);
} finally {
restoreFetch();
}
});
test("Telegram file downloads reject files above configured limits", async () => {
const tempDir = await mkdtemp(join(tmpdir(), "pi-telegram-download-limit-"));
let calls = 0;
const restoreFetch = setApiTestFetch(async (input) => {
calls += 1;
const url = getApiTestFetchUrl(input);
if (url.includes("/getFile")) {
return createApiJsonResponse({ file_path: "files/demo", file_size: 10 });
}
return new Response("too large", { status: 200 });
});
try {
await assert.rejects(
() =>
downloadTelegramFile("123:abc", "file-id", "demo.txt", tempDir, {
maxFileSizeBytes: 5,
}),
{ message: "Telegram file exceeds size limit (10 bytes > 5 bytes)" },
);
assert.equal(calls, 1);
assert.deepEqual(await readdir(tempDir), []);
} finally {
restoreFetch();
}
});
test("Telegram streaming downloads remove partial files after limit failures", async () => {
const tempDir = await mkdtemp(
join(tmpdir(), "pi-telegram-download-partial-"),
);
const restoreFetch = setApiTestFetch(async (input) => {
const url = getApiTestFetchUrl(input);
if (url.includes("/getFile")) {
return createApiJsonResponse({ file_path: "files/demo" });
}
return new Response("too large", { status: 200 });
});
try {
await assert.rejects(
() =>
downloadTelegramFile("123:abc", "file-id", "demo.txt", tempDir, {
maxFileSizeBytes: 5,
}),
{ message: "Telegram file exceeds size limit (9 bytes > 5 bytes)" },
);
assert.deepEqual(await readdir(tempDir), []);
} finally {
restoreFetch();
}
});
test("Telegram API helpers reject malformed successful responses", async () => {
const restoreFetch = setApiTestFetch(async () => {
return createMalformedApiTextResponse("not json");
});
try {
await assert.rejects(() => callTelegram("123:abc", "getMe", {}), {
message: "Telegram API getMe returned invalid JSON",
});
} finally {
restoreFetch();
}
});
test("Non-idempotent malformed success becomes commit-unknown", async () => {
const restoreFetch = setApiTestFetch(async () =>
createMalformedApiTextResponse("not json"),
);
try {
await assert.rejects(
() =>
callTelegram("123:abc", "createForumTopic", {}, {
maxAttempts: 1,
}),
isTelegramApiCommitUnknownError,
);
} finally {
restoreFetch();
}
});
test("answerTelegramCallbackQuery records Telegram API failures without throwing", async () => {
const events: Array<Record<string, unknown>> = [];
const restoreFetch = setApiTestFetch(async () => {
throw new Error("network down");
});
try {
await assert.doesNotReject(() =>
answerTelegramCallbackQuery("123:abc", "callback-id", "ok", {
recordRuntimeEvent: (kind, error, details) => {
events.push({
kind,
message: error instanceof Error ? error.message : String(error),
details,
});
},
}),
);
assert.deepEqual(events, [
{
kind: "api",
message: "network down",
details: { method: "answerCallbackQuery" },
},
]);
} finally {
restoreFetch();
}
});
test("Default Telegram bridge API runtime binds lazy token client and defaults", async () => {
const calls: string[] = [];
const restoreFetch = setApiTestFetch(async (input) => {
calls.push(getApiTestFetchUrl(input));
return createApiJsonResponse(true);
});
try {
const runtime = createDefaultTelegramBridgeApiRuntime({
getBotToken: () => "123:abc",
recordRuntimeEvent: () => {},
});
assert.equal(await runtime.sendTypingAction(7), true);
assert.match(calls[0] ?? "", /bot123:abc\/sendChatAction$/);
} finally {
restoreFetch();
}
});
test("Default Telegram bridge API runtime honors PI_CODING_AGENT_DIR for temp files", async () => {
const previousAgentDir = process.env.PI_CODING_AGENT_DIR;
const agentDir = await mkdtemp(join(tmpdir(), "pi-telegram-agent-dir-"));
const tempDir = resolve(agentDir, "tmp", "telegram");
process.env.PI_CODING_AGENT_DIR = agentDir;
try {
const runtime = createDefaultTelegramBridgeApiRuntime({
getBotToken: () => "123:abc",
recordRuntimeEvent: () => {},
});
assert.equal(await runtime.prepareTempDir(), 0);
assert.deepEqual(await readdir(tempDir), []);
} finally {
if (previousAgentDir === undefined) delete process.env.PI_CODING_AGENT_DIR;
else process.env.PI_CODING_AGENT_DIR = previousAgentDir;
}
});
test("Telegram bridge API runtime prepares its configured temp directory", async () => {
const tempDir = await mkdtemp(join(tmpdir(), "telegram-runtime-temp-"));
const staleFile = join(tempDir, "stale.txt");
await writeFile(staleFile, "old");
const oldDate = new Date(Date.now() - 2_000);
await utimes(staleFile, oldDate, oldDate);
const runtime = createTelegramBridgeApiRuntime({
tempDir,
maxFileSizeBytes: 123,
tempFileMaxAgeMs: 1,
recordRuntimeEvent: () => {},
client: createApiRuntimeClient({
downloadFile: async () => staleFile,
}),
});
assert.equal(await runtime.prepareTempDir(), 1);
assert.deepEqual(await readdir(tempDir), []);
});
test("Telegram bridge API runtime records structured failures", async () => {
const events: Array<Record<string, unknown>> = [];
const runtime = createTelegramBridgeApiRuntime({
tempDir: "/tmp/telegram",
maxFileSizeBytes: 123,
tempFileMaxAgeMs: 60_000,
recordRuntimeEvent: (kind, error, details) => {
events.push({
kind,
message: error instanceof Error ? error.message : String(error),
details,
});
},
client: createApiRuntimeClient({
call: async () => {
throw new Error("api failed");
},
callMultipart: async () => {
throw new Error("multipart failed");
},
downloadFile: async (_fileId, _suggestedName, tempDir, options) => {
events.push({ tempDir, maxFileSizeBytes: options?.maxFileSizeBytes });
throw new Error("download failed");
},
answerCallbackQuery: async () => {
throw new Error("answer failed");
},
}),
});
await assert.rejects(() => runtime.call("sendMessage", {}), {
message: "api failed",
});
await assert.rejects(
() =>
runtime.callMultipart("sendDocument", {}, "document", "/tmp/a", "a.txt"),
{ message: "multipart failed" },
);
await assert.rejects(() => runtime.downloadFile("file-id", "demo.txt"), {
message: "download failed",
});
await runtime.answerCallbackQuery("cb-1", "ok");
assert.deepEqual(events, [
{
kind: "api",
message: "api failed",
details: { method: "sendMessage" },
},
{
kind: "multipart",
message: "multipart failed",
details: { method: "sendDocument", fileName: "a.txt" },
},
{ tempDir: "/tmp/telegram", maxFileSizeBytes: 123 },
{
kind: "download",
message: "download failed",
details: { suggestedName: "demo.txt" },
},
{
kind: "api",
message: "answer failed",
details: { method: "answerCallbackQuery" },
},
]);
});
test("Telegram bridge API runtime exposes typed Bot API helpers", async () => {
const calls: Array<{ method: string; body: Record<string, unknown> }> = [];
const runtime = createTelegramBridgeApiRuntime({
tempDir: "/tmp/telegram",
maxFileSizeBytes: 123,
tempFileMaxAgeMs: 60_000,
recordRuntimeEvent: () => {},
client: createApiRuntimeClient({
call: async <TResponse>(
method: string,
body: Record<string, unknown>,
) => {
calls.push({ method, body });
if (method === "sendMessage" || method === "sendRichMessage") {
return { message_id: 9 } as TResponse;
}
if (method === "getUpdates") return [{ update_id: 10 }] as TResponse;
return true as TResponse;
},
}),
});
assert.equal(await runtime.deleteWebhook(), true);
assert.deepEqual(await runtime.getUpdates({ offset: 1 }), [
{ update_id: 10 },
]);
assert.equal(
await runtime.setMyCommands([{ command: "start", description: "Start" }]),
true,
);
assert.equal(await runtime.sendChatAction(1, "typing"), true);
assert.equal(await runtime.sendTypingAction(2), true);
await runtime.answerGuestQuery("guest-1", "hello");
await runtime.answerGuestQuery("guest-rich", undefined, {
richMessage: { markdown: "**hello**", skip_entity_detection: true },
});
await runtime.answerGuestQuery("guest-voice", undefined, {
result: {
type: "voice",
id: "voice-1",
voice_file_id: "cached-voice",
title: "Response",
caption: "hello",
},
});
await runtime.answerGuestQuery("guest-2");
assert.equal(await runtime.sendMessageDraft(1, 2, "draft"), true);
assert.equal(await runtime.sendMessageDraft(1, 2, ""), true);
assert.equal(await runtime.sendMessageDraft(1, 2, undefined), true);
assert.equal(
await runtime.sendMessageDraft(1, 2, "rich", {
parse_mode: "HTML",
entities: [{ type: "bold", offset: 0, length: 4 }],
}),
true,
);
assert.deepEqual(await runtime.sendMessage({ chat_id: 1, text: "hello" }), {
message_id: 9,
});
assert.deepEqual(
await runtime.sendRichMessage({
chat_id: 1,
rich_message: { markdown: "# hello" },
}),
{ message_id: 9 },
);
assert.equal(
await runtime.sendRichMessageDraft({
chat_id: 1,
draft_id: 3,
rich_message: { markdown: "**draft**" },
}),
true,
);
assert.deepEqual(calls, [
{ method: "deleteWebhook", body: { drop_pending_updates: false } },
{ method: "getUpdates", body: { offset: 1 } },
{
method: "setMyCommands",
body: { commands: [{ command: "start", description: "Start" }] },
},
{ method: "sendChatAction", body: { chat_id: 1, action: "typing" } },
{ method: "sendChatAction", body: { chat_id: 2, action: "typing" } },
{
method: "answerGuestQuery",
body: {
guest_query_id: "guest-1",
result: {
type: "article",
id: "1",
title: "Response",
input_message_content: { message_text: "hello" },
},
},
},
{
method: "answerGuestQuery",
body: {
guest_query_id: "guest-rich",
result: {
type: "article",
id: "1",
title: "Response",
input_message_content: {
rich_message: {
markdown: "**hello**",
skip_entity_detection: true,
},
},
},
},
},
{
method: "answerGuestQuery",
body: {
guest_query_id: "guest-voice",
result: {
type: "voice",
id: "voice-1",
voice_file_id: "cached-voice",
title: "Response",
caption: "hello",
},
},
},
{ method: "answerGuestQuery", body: { guest_query_id: "guest-2" } },
{
method: "sendMessageDraft",
body: { chat_id: 1, draft_id: 2, text: "draft" },
},
{
method: "sendMessageDraft",
body: { chat_id: 1, draft_id: 2, text: "" },
},
{
method: "sendMessageDraft",
body: { chat_id: 1, draft_id: 2 },
},
{
method: "sendMessageDraft",
body: {
chat_id: 1,
draft_id: 2,
text: "rich",
parse_mode: "HTML",
entities: [{ type: "bold", offset: 0, length: 4 }],
},
},
{ method: "sendMessage", body: { chat_id: 1, text: "hello" } },
{
method: "sendRichMessage",
body: { chat_id: 1, rich_message: { markdown: "# hello" } },
},
{
method: "sendRichMessageDraft",
body: {
chat_id: 1,
draft_id: 3,
rich_message: { markdown: "**draft**" },
},
},
]);
});
test("Telegram bridge API runtime edits messages and tolerates unchanged text", async () => {
const events: Array<Record<string, unknown>> = [];
const runtime = createTelegramBridgeApiRuntime({
tempDir: "/tmp/telegram",
maxFileSizeBytes: 123,
tempFileMaxAgeMs: 60_000,
recordRuntimeEvent: (kind, error, details) => {
events.push({
kind,
message: error instanceof Error ? error.message : String(error),
details,
});
},
client: createApiRuntimeClient({
call: async <TResponse>(
_method: string,
body: Record<string, unknown>,
) => {
if (body.text === "same") {
throw new Error("Bad Request: message is not modified");
}
return true as TResponse;
},
}),
});
assert.equal(
await runtime.editMessageText({ chat_id: 1, message_id: 2, text: "next" }),
"edited",
);
assert.equal(
await runtime.editMessageText({ chat_id: 1, message_id: 2, text: "same" }),
"unchanged",
);
assert.deepEqual(events, []);
});
test("Telegram API client resolves bot tokens lazily for wrapped calls", async () => {
const calls: string[] = [];
let botToken = "123:abc";
const restoreFetch = setApiTestFetch(async (input) => {
calls.push(getApiTestFetchUrl(input));
return createApiJsonResponse(true);
});
try {
const client = createTelegramApiClient(() => botToken);
await client.call("sendChatAction", { chat_id: 1, action: "typing" });
botToken = "456:def";
await client.answerCallbackQuery("cb-1", "ok");
assert.match(calls[0] ?? "", /bot123:abc\/sendChatAction$/);
assert.match(calls[1] ?? "", /bot456:def\/answerCallbackQuery$/);
} finally {
restoreFetch();
}
});