mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-30 23:14:37 +00:00
test(e2e): expand agent-harness coverage for channels + prompt flows (#2518)
This commit is contained in:
@@ -12,12 +12,15 @@ import {
|
||||
getMockMessages,
|
||||
listMockLlmThreads,
|
||||
getMockWebhookTriggers,
|
||||
getMockTelegramSent,
|
||||
getRequestLog,
|
||||
pushMockTelegramUpdate,
|
||||
resetMockBehavior,
|
||||
resetMockConversations,
|
||||
resetMockCronJobs,
|
||||
resetMockMessages,
|
||||
resetMockLlmThreads,
|
||||
resetMockTelegram,
|
||||
resetSocketSessions,
|
||||
resetMockTunnels,
|
||||
resetMockWebhookTriggers,
|
||||
@@ -79,6 +82,7 @@ export function handleAdmin(ctx) {
|
||||
resetConversationFixturesState();
|
||||
resetCronFixturesState();
|
||||
resetSocketSessions();
|
||||
resetMockTelegram();
|
||||
json(res, 200, {
|
||||
success: true,
|
||||
data: {
|
||||
@@ -122,5 +126,31 @@ export function handleAdmin(ctx) {
|
||||
json(res, 200, { success: true, data: [] });
|
||||
return true;
|
||||
}
|
||||
|
||||
// ── Telegram admin endpoints ───────────────────────────────────────────
|
||||
if (method === "POST" && /^\/__admin\/telegram\/inject-update\/?$/.test(url)) {
|
||||
const updates = Array.isArray(parsedBody?.updates)
|
||||
? parsedBody.updates
|
||||
: parsedBody && typeof parsedBody === "object" && !Array.isArray(parsedBody)
|
||||
? [parsedBody]
|
||||
: [];
|
||||
for (const update of updates) {
|
||||
pushMockTelegramUpdate(update);
|
||||
}
|
||||
console.log(`[telegram-mock] inject-update: queued ${updates.length} update(s)`);
|
||||
json(res, 200, { ok: true, queued: updates.length });
|
||||
return true;
|
||||
}
|
||||
if (method === "GET" && /^\/__admin\/telegram\/sent\/?$/.test(url)) {
|
||||
json(res, 200, { ok: true, messages: getMockTelegramSent() });
|
||||
return true;
|
||||
}
|
||||
if (method === "POST" && /^\/__admin\/telegram\/reset\/?$/.test(url)) {
|
||||
resetMockTelegram();
|
||||
console.log("[telegram-mock] admin reset: telegram state cleared");
|
||||
json(res, 200, { ok: true });
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -19,11 +19,14 @@ export {
|
||||
clearRequestLog,
|
||||
getSocketEventLog,
|
||||
getMockBehavior,
|
||||
getMockTelegramSent,
|
||||
listMockLlmThreads,
|
||||
getRequestLog,
|
||||
listSocketSessions,
|
||||
pushMockTelegramUpdate,
|
||||
resetMockBehavior,
|
||||
resetMockLlmThreads,
|
||||
resetMockTelegram,
|
||||
setMockBehavior,
|
||||
setMockBehaviors,
|
||||
} from "./state.mjs";
|
||||
|
||||
@@ -0,0 +1,416 @@
|
||||
/**
|
||||
* Unit tests for the mock Telegram Bot API route handler.
|
||||
*
|
||||
* Pattern: construct a minimal `ctx` object (matching what server.mjs
|
||||
* provides), call the handler, and assert on the captured response.
|
||||
*
|
||||
* Run via:
|
||||
* node --test scripts/mock-api/routes/__tests__/telegram.test.mjs
|
||||
* or through the project test runner:
|
||||
* pnpm debug unit scripts/mock-api/routes/__tests__/telegram.test.mjs
|
||||
*/
|
||||
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
resetMockBehavior,
|
||||
resetMockTelegram,
|
||||
setMockBehavior,
|
||||
pushMockTelegramUpdate,
|
||||
getMockTelegramSent,
|
||||
startMockServer,
|
||||
stopMockServer,
|
||||
} from "../../index.mjs";
|
||||
import { handleTelegram } from "../telegram.mjs";
|
||||
|
||||
// ── Helpers ────────────────────────────────────────────────────────────────
|
||||
|
||||
function createRes() {
|
||||
return {
|
||||
statusCode: 0,
|
||||
headers: {},
|
||||
body: "",
|
||||
writeHead(status, headers = {}) {
|
||||
this.statusCode = status;
|
||||
this.headers = { ...this.headers, ...headers };
|
||||
},
|
||||
setHeader(name, value) {
|
||||
this.headers[name] = value;
|
||||
},
|
||||
end(chunk = "") {
|
||||
this.body += String(chunk);
|
||||
},
|
||||
json() {
|
||||
return JSON.parse(this.body);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function makeCtx(method, path, body = null) {
|
||||
return {
|
||||
method,
|
||||
url: path,
|
||||
body: body ? JSON.stringify(body) : "",
|
||||
parsedBody: body,
|
||||
res: createRes(),
|
||||
};
|
||||
}
|
||||
|
||||
// ── Setup / teardown ───────────────────────────────────────────────────────
|
||||
|
||||
test.beforeEach(() => {
|
||||
resetMockBehavior();
|
||||
resetMockTelegram();
|
||||
});
|
||||
|
||||
// ── getMe ──────────────────────────────────────────────────────────────────
|
||||
|
||||
test("getMe returns default bot info", async () => {
|
||||
const ctx = makeCtx("POST", "/bot12345:TOKEN/getMe");
|
||||
const handled = await handleTelegram(ctx);
|
||||
|
||||
assert.equal(handled, true);
|
||||
assert.equal(ctx.res.statusCode, 200);
|
||||
const payload = ctx.res.json();
|
||||
assert.equal(payload.ok, true);
|
||||
assert.equal(payload.result.is_bot, true);
|
||||
assert.equal(payload.result.username, "e2e_test_bot");
|
||||
assert.equal(payload.result.id, 123456789);
|
||||
});
|
||||
|
||||
test("getMe returns custom username from behavior", async () => {
|
||||
setMockBehavior("telegramBotUsername", "my_custom_bot");
|
||||
const ctx = makeCtx("GET", "/bot12345:TOKEN/getMe");
|
||||
const handled = await handleTelegram(ctx);
|
||||
|
||||
assert.equal(handled, true);
|
||||
const payload = ctx.res.json();
|
||||
assert.equal(payload.result.username, "my_custom_bot");
|
||||
});
|
||||
|
||||
test("getMe returns 401 when telegramGetMeFails=1", async () => {
|
||||
setMockBehavior("telegramGetMeFails", "1");
|
||||
const ctx = makeCtx("POST", "/botANYTOKEN/getMe");
|
||||
const handled = await handleTelegram(ctx);
|
||||
|
||||
assert.equal(handled, true);
|
||||
assert.equal(ctx.res.statusCode, 401);
|
||||
const payload = ctx.res.json();
|
||||
assert.equal(payload.ok, false);
|
||||
assert.equal(payload.error_code, 401);
|
||||
});
|
||||
|
||||
// ── getUpdates ─────────────────────────────────────────────────────────────
|
||||
|
||||
test("getUpdates returns empty array when queue is empty", async () => {
|
||||
setMockBehavior("telegramPollDelayMs", "0");
|
||||
const ctx = makeCtx("POST", "/botTOKEN/getUpdates");
|
||||
const handled = await handleTelegram(ctx);
|
||||
|
||||
assert.equal(handled, true);
|
||||
assert.equal(ctx.res.statusCode, 200);
|
||||
const payload = ctx.res.json();
|
||||
assert.equal(payload.ok, true);
|
||||
assert.deepEqual(payload.result, []);
|
||||
});
|
||||
|
||||
test("getUpdates returns injected update", async () => {
|
||||
setMockBehavior("telegramPollDelayMs", "0");
|
||||
const update = {
|
||||
update_id: 1,
|
||||
message: {
|
||||
message_id: 1,
|
||||
from: { id: 9999, first_name: "Alice" },
|
||||
chat: { id: 9999, type: "private" },
|
||||
text: "hello bot",
|
||||
},
|
||||
};
|
||||
pushMockTelegramUpdate(update);
|
||||
|
||||
const ctx = makeCtx("POST", "/botTOKEN/getUpdates");
|
||||
const handled = await handleTelegram(ctx);
|
||||
|
||||
assert.equal(handled, true);
|
||||
const payload = ctx.res.json();
|
||||
assert.equal(payload.result.length, 1);
|
||||
assert.deepEqual(payload.result[0], update);
|
||||
});
|
||||
|
||||
test("getUpdates drains queue on each call", async () => {
|
||||
setMockBehavior("telegramPollDelayMs", "0");
|
||||
pushMockTelegramUpdate({ update_id: 1, message: { text: "first" } });
|
||||
pushMockTelegramUpdate({ update_id: 2, message: { text: "second" } });
|
||||
|
||||
// First call — should return both
|
||||
const ctx1 = makeCtx("POST", "/botTOKEN/getUpdates");
|
||||
await handleTelegram(ctx1);
|
||||
const payload1 = ctx1.res.json();
|
||||
assert.equal(payload1.result.length, 2);
|
||||
|
||||
// Second call — queue is now empty
|
||||
const ctx2 = makeCtx("POST", "/botTOKEN/getUpdates");
|
||||
await handleTelegram(ctx2);
|
||||
const payload2 = ctx2.res.json();
|
||||
assert.deepEqual(payload2.result, []);
|
||||
});
|
||||
|
||||
// ── sendMessage ────────────────────────────────────────────────────────────
|
||||
|
||||
test("sendMessage records message and returns proper shape", async () => {
|
||||
const ctx = makeCtx("POST", "/botTOKEN/sendMessage", {
|
||||
chat_id: 42,
|
||||
text: "Hello from test!",
|
||||
parse_mode: "Markdown",
|
||||
});
|
||||
const handled = await handleTelegram(ctx);
|
||||
|
||||
assert.equal(handled, true);
|
||||
assert.equal(ctx.res.statusCode, 200);
|
||||
const payload = ctx.res.json();
|
||||
assert.equal(payload.ok, true);
|
||||
assert.equal(typeof payload.result.message_id, "number");
|
||||
assert.equal(payload.result.chat.id, 42);
|
||||
assert.equal(payload.result.text, "Hello from test!");
|
||||
|
||||
// Verify it was recorded
|
||||
const sent = getMockTelegramSent();
|
||||
assert.equal(sent.length, 1);
|
||||
assert.equal(sent[0].method, "sendMessage");
|
||||
assert.equal(sent[0].body.text, "Hello from test!");
|
||||
assert.equal(sent[0].message_id, payload.result.message_id);
|
||||
});
|
||||
|
||||
test("sendMessage returns 400 when telegramSendFails=1", async () => {
|
||||
setMockBehavior("telegramSendFails", "1");
|
||||
const ctx = makeCtx("POST", "/botTOKEN/sendMessage", {
|
||||
chat_id: 42,
|
||||
text: "will fail",
|
||||
});
|
||||
const handled = await handleTelegram(ctx);
|
||||
|
||||
assert.equal(handled, true);
|
||||
assert.equal(ctx.res.statusCode, 400);
|
||||
const payload = ctx.res.json();
|
||||
assert.equal(payload.ok, false);
|
||||
assert.equal(payload.error_code, 400);
|
||||
});
|
||||
|
||||
// ── Simple record methods ──────────────────────────────────────────────────
|
||||
|
||||
test("sendChatAction records and returns ok", async () => {
|
||||
const ctx = makeCtx("POST", "/botTOKEN/sendChatAction", {
|
||||
chat_id: 42,
|
||||
action: "typing",
|
||||
});
|
||||
const handled = await handleTelegram(ctx);
|
||||
|
||||
assert.equal(handled, true);
|
||||
const payload = ctx.res.json();
|
||||
assert.equal(payload.ok, true);
|
||||
assert.equal(payload.result, true);
|
||||
});
|
||||
|
||||
test("deleteWebhook returns ok", async () => {
|
||||
const ctx = makeCtx("POST", "/botTOKEN/deleteWebhook");
|
||||
const handled = await handleTelegram(ctx);
|
||||
|
||||
assert.equal(handled, true);
|
||||
const payload = ctx.res.json();
|
||||
assert.equal(payload.ok, true);
|
||||
});
|
||||
|
||||
// ── Media send methods ─────────────────────────────────────────────────────
|
||||
|
||||
test("sendPhoto records and returns message_id", async () => {
|
||||
const ctx = makeCtx("POST", "/botTOKEN/sendPhoto", { chat_id: 99 });
|
||||
const handled = await handleTelegram(ctx);
|
||||
|
||||
assert.equal(handled, true);
|
||||
const payload = ctx.res.json();
|
||||
assert.equal(payload.ok, true);
|
||||
assert.equal(typeof payload.result.message_id, "number");
|
||||
|
||||
const sent = getMockTelegramSent();
|
||||
assert.equal(sent.length, 1);
|
||||
assert.equal(sent[0].method, "sendPhoto");
|
||||
});
|
||||
|
||||
// ── Token validation ───────────────────────────────────────────────────────
|
||||
|
||||
test("rejects mismatched token when telegramBotToken is set", async () => {
|
||||
setMockBehavior("telegramBotToken", "correctToken");
|
||||
const ctx = makeCtx("POST", "/botwrongToken/getMe");
|
||||
const handled = await handleTelegram(ctx);
|
||||
|
||||
assert.equal(handled, true);
|
||||
assert.equal(ctx.res.statusCode, 401);
|
||||
const payload = ctx.res.json();
|
||||
assert.equal(payload.ok, false);
|
||||
});
|
||||
|
||||
test("accepts correct token when telegramBotToken is set", async () => {
|
||||
setMockBehavior("telegramBotToken", "correctToken");
|
||||
const ctx = makeCtx("POST", "/botcorrectToken/getMe");
|
||||
const handled = await handleTelegram(ctx);
|
||||
|
||||
assert.equal(handled, true);
|
||||
assert.equal(ctx.res.statusCode, 200);
|
||||
const payload = ctx.res.json();
|
||||
assert.equal(payload.ok, true);
|
||||
});
|
||||
|
||||
// ── Non-Telegram paths ─────────────────────────────────────────────────────
|
||||
|
||||
test("returns false for non-bot paths", async () => {
|
||||
const ctx = makeCtx("GET", "/api/something");
|
||||
const handled = await handleTelegram(ctx);
|
||||
assert.equal(handled, false);
|
||||
});
|
||||
|
||||
// ── Admin round-trip (stateful, uses full HTTP server) ─────────────────────
|
||||
|
||||
test("admin inject-update + sent-list round-trip", async () => {
|
||||
const { port } = await startMockServer(18562, { retryIfInUse: true });
|
||||
const base = `http://127.0.0.1:${port}`;
|
||||
|
||||
try {
|
||||
// Inject a single update
|
||||
const injectSingle = await fetch(`${base}/__admin/telegram/inject-update`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
update_id: 100,
|
||||
message: { text: "hello", chat: { id: 1 } },
|
||||
}),
|
||||
});
|
||||
assert.equal(injectSingle.status, 200);
|
||||
const injectBody = await injectSingle.json();
|
||||
assert.equal(injectBody.ok, true);
|
||||
assert.equal(injectBody.queued, 1);
|
||||
|
||||
// Drain via getUpdates
|
||||
const updatesRes = await fetch(`${base}/botTEST123/getUpdates`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ timeout: 0 }),
|
||||
});
|
||||
const updatesBody = await updatesRes.json();
|
||||
assert.equal(updatesBody.ok, true);
|
||||
assert.equal(updatesBody.result.length, 1);
|
||||
assert.equal(updatesBody.result[0].update_id, 100);
|
||||
|
||||
// Send a message
|
||||
await fetch(`${base}/botTEST123/sendMessage`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ chat_id: 1, text: "reply" }),
|
||||
});
|
||||
|
||||
// Check sent list
|
||||
const sentRes = await fetch(`${base}/__admin/telegram/sent`);
|
||||
const sentBody = await sentRes.json();
|
||||
assert.equal(sentBody.ok, true);
|
||||
assert.equal(sentBody.messages.length, 1);
|
||||
assert.equal(sentBody.messages[0].method, "sendMessage");
|
||||
assert.equal(sentBody.messages[0].body.text, "reply");
|
||||
} finally {
|
||||
await stopMockServer();
|
||||
}
|
||||
});
|
||||
|
||||
test("admin inject-update accepts { updates: [...] } batch form", async () => {
|
||||
const { port } = await startMockServer(18563, { retryIfInUse: true });
|
||||
const base = `http://127.0.0.1:${port}`;
|
||||
|
||||
try {
|
||||
const injectBatch = await fetch(`${base}/__admin/telegram/inject-update`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
updates: [
|
||||
{ update_id: 200, message: { text: "msg1" } },
|
||||
{ update_id: 201, message: { text: "msg2" } },
|
||||
],
|
||||
}),
|
||||
});
|
||||
const batchBody = await injectBatch.json();
|
||||
assert.equal(batchBody.queued, 2);
|
||||
|
||||
// Drain
|
||||
const updatesRes = await fetch(`${base}/botX/getUpdates`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
const updatesBody = await updatesRes.json();
|
||||
assert.equal(updatesBody.result.length, 2);
|
||||
} finally {
|
||||
await stopMockServer();
|
||||
}
|
||||
});
|
||||
|
||||
test("admin telegram reset clears state", async () => {
|
||||
const { port } = await startMockServer(18564, { retryIfInUse: true });
|
||||
const base = `http://127.0.0.1:${port}`;
|
||||
|
||||
try {
|
||||
// Inject an update and a sent message
|
||||
await fetch(`${base}/__admin/telegram/inject-update`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ update_id: 999, message: { text: "pre-reset" } }),
|
||||
});
|
||||
await fetch(`${base}/botX/sendMessage`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ chat_id: 1, text: "pre-reset" }),
|
||||
});
|
||||
|
||||
// Reset telegram state only
|
||||
const resetRes = await fetch(`${base}/__admin/telegram/reset`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
const resetBody = await resetRes.json();
|
||||
assert.equal(resetBody.ok, true);
|
||||
|
||||
// Sent should be empty now
|
||||
const sentRes = await fetch(`${base}/__admin/telegram/sent`);
|
||||
const sentBody = await sentRes.json();
|
||||
assert.deepEqual(sentBody.messages, []);
|
||||
|
||||
// Queue should be empty (getUpdates returns [])
|
||||
const updatesRes = await fetch(`${base}/botX/getUpdates`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
const updatesBody = await updatesRes.json();
|
||||
assert.deepEqual(updatesBody.result, []);
|
||||
} finally {
|
||||
await stopMockServer();
|
||||
}
|
||||
});
|
||||
|
||||
test("global admin reset also clears telegram state", async () => {
|
||||
const { port } = await startMockServer(18565, { retryIfInUse: true });
|
||||
const base = `http://127.0.0.1:${port}`;
|
||||
|
||||
try {
|
||||
await fetch(`${base}/botX/sendMessage`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ chat_id: 1, text: "before global reset" }),
|
||||
});
|
||||
|
||||
// Global reset
|
||||
await fetch(`${base}/__admin/reset`, { method: "POST" });
|
||||
|
||||
const sentRes = await fetch(`${base}/__admin/telegram/sent`);
|
||||
const sentBody = await sentRes.json();
|
||||
assert.deepEqual(sentBody.messages, []);
|
||||
} finally {
|
||||
await stopMockServer();
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,211 @@
|
||||
/**
|
||||
* Mock Telegram Bot API route handler.
|
||||
*
|
||||
* Intercepts all requests matching `/bot<token>/<method>` and responds with
|
||||
* realistic Telegram Bot API shapes. Behaviour is driven by the mock behavior
|
||||
* keys documented below so E2E specs can configure failure modes without
|
||||
* changing code.
|
||||
*
|
||||
* Behavior keys:
|
||||
* telegramBotUsername - bot username returned by getMe (default: "e2e_test_bot")
|
||||
* telegramBotToken - expected bot token; if set, mismatches return 401
|
||||
* telegramPollDelayMs - delay before getUpdates returns (simulates long-poll)
|
||||
* telegramGetMeFails - if "1", getMe returns HTTP 401 Unauthorized
|
||||
* telegramSendFails - if "1", sendMessage returns HTTP 400 Bad Request
|
||||
*/
|
||||
|
||||
import { json } from "../http.mjs";
|
||||
import {
|
||||
behavior,
|
||||
drainMockTelegramUpdates,
|
||||
nextMockTelegramMessageId,
|
||||
recordMockTelegramSent,
|
||||
sleep,
|
||||
} from "../state.mjs";
|
||||
|
||||
/** Extract the bot token and method name from a Telegram Bot API path. */
|
||||
const BOT_PATH_RE = /^\/bot([^/]+)\/([^/?]+)/;
|
||||
|
||||
/**
|
||||
* Attempt to parse a Telegram Bot API path.
|
||||
* Returns `{ token, method }` or `null` if the path does not match.
|
||||
*/
|
||||
function parseBotPath(url) {
|
||||
const m = BOT_PATH_RE.exec(url);
|
||||
if (!m) return null;
|
||||
return { token: m[1], method: m[2] };
|
||||
}
|
||||
|
||||
/**
|
||||
* Main route handler. Returns `true` when the request was handled, `false`
|
||||
* when it should fall through to the next handler.
|
||||
*/
|
||||
export async function handleTelegram(ctx) {
|
||||
const { url, parsedBody, res } = ctx;
|
||||
|
||||
const parsed = parseBotPath(url);
|
||||
if (!parsed) return false;
|
||||
|
||||
const { token, method } = parsed;
|
||||
const b = behavior();
|
||||
|
||||
console.log(`[telegram-mock] ${method} token=${token.slice(0, 8)}...`);
|
||||
|
||||
// Optional token validation — only enforced when behavior key is set.
|
||||
if (b.telegramBotToken && token !== b.telegramBotToken) {
|
||||
console.warn(
|
||||
`[telegram-mock] token mismatch: got ${token.slice(0, 8)}... expected ${b.telegramBotToken.slice(0, 8)}...`,
|
||||
);
|
||||
json(res, 401, {
|
||||
ok: false,
|
||||
error_code: 401,
|
||||
description: "Unauthorized",
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (method) {
|
||||
case "getMe":
|
||||
return handleGetMe(res, b);
|
||||
|
||||
case "getUpdates":
|
||||
return handleGetUpdates(res, b);
|
||||
|
||||
case "sendMessage":
|
||||
return handleSendMessage(res, b, parsedBody);
|
||||
|
||||
case "sendChatAction":
|
||||
return handleSimpleRecord(res, "sendChatAction", parsedBody);
|
||||
|
||||
case "deleteWebhook":
|
||||
return handleSimpleRecord(res, "deleteWebhook", parsedBody);
|
||||
|
||||
case "setMessageReaction":
|
||||
return handleSimpleRecord(res, "setMessageReaction", parsedBody);
|
||||
|
||||
case "editMessageText":
|
||||
return handleSimpleRecord(res, "editMessageText", parsedBody);
|
||||
|
||||
case "editMessageReplyMarkup":
|
||||
return handleSimpleRecord(res, "editMessageReplyMarkup", parsedBody);
|
||||
|
||||
case "answerCallbackQuery":
|
||||
return handleSimpleRecord(res, "answerCallbackQuery", parsedBody);
|
||||
|
||||
case "sendPhoto":
|
||||
case "sendDocument":
|
||||
case "sendVideo":
|
||||
case "sendAudio":
|
||||
case "sendVoice":
|
||||
case "sendAnimation":
|
||||
case "sendSticker": {
|
||||
// Multipart bodies are not parsed — just record the method and any JSON
|
||||
// keys we received (body may be null for multipart).
|
||||
const messageId = nextMockTelegramMessageId();
|
||||
recordMockTelegramSent({
|
||||
method,
|
||||
body: parsedBody ?? {},
|
||||
message_id: messageId,
|
||||
});
|
||||
console.log(
|
||||
`[telegram-mock] ${method} recorded message_id=${messageId}`,
|
||||
);
|
||||
json(res, 200, { ok: true, result: { message_id: messageId } });
|
||||
return true;
|
||||
}
|
||||
|
||||
default:
|
||||
console.log(
|
||||
`[telegram-mock] unhandled method="${method}" — returning ok:true, result:null`,
|
||||
);
|
||||
json(res, 200, { ok: true, result: null });
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Individual method handlers ─────────────────────────────────────────────
|
||||
|
||||
function handleGetMe(res, b) {
|
||||
if (b.telegramGetMeFails === "1") {
|
||||
console.warn("[telegram-mock] getMe failing per behavior.telegramGetMeFails");
|
||||
json(res, 401, {
|
||||
ok: false,
|
||||
error_code: 401,
|
||||
description: "Unauthorized",
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
const username = b.telegramBotUsername || "e2e_test_bot";
|
||||
console.log(`[telegram-mock] getMe -> username=${username}`);
|
||||
json(res, 200, {
|
||||
ok: true,
|
||||
result: {
|
||||
id: 123456789,
|
||||
is_bot: true,
|
||||
first_name: "E2E Bot",
|
||||
username,
|
||||
},
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
async function handleGetUpdates(res, b) {
|
||||
const delayMs = Math.min(
|
||||
Number(b.telegramPollDelayMs) || 50,
|
||||
30_000,
|
||||
);
|
||||
if (Number.isFinite(delayMs) && delayMs > 0) {
|
||||
console.log(`[telegram-mock] getUpdates: waiting ${delayMs}ms before reply`);
|
||||
await sleep(delayMs);
|
||||
}
|
||||
|
||||
const updates = drainMockTelegramUpdates();
|
||||
console.log(`[telegram-mock] getUpdates: returning ${updates.length} update(s)`);
|
||||
json(res, 200, { ok: true, result: updates });
|
||||
return true;
|
||||
}
|
||||
|
||||
function handleSendMessage(res, b, body) {
|
||||
if (b.telegramSendFails === "1") {
|
||||
console.warn("[telegram-mock] sendMessage failing per behavior.telegramSendFails");
|
||||
json(res, 400, {
|
||||
ok: false,
|
||||
error_code: 400,
|
||||
description: "Bad Request",
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
const messageId = nextMockTelegramMessageId();
|
||||
const chatId = body?.chat_id ?? 0;
|
||||
const text = body?.text ?? "";
|
||||
|
||||
recordMockTelegramSent({
|
||||
method: "sendMessage",
|
||||
body: body ?? {},
|
||||
message_id: messageId,
|
||||
});
|
||||
|
||||
console.log(
|
||||
`[telegram-mock] sendMessage chat_id=${chatId} message_id=${messageId} text="${String(text).slice(0, 80)}"`,
|
||||
);
|
||||
|
||||
json(res, 200, {
|
||||
ok: true,
|
||||
result: {
|
||||
message_id: messageId,
|
||||
date: Math.floor(Date.now() / 1000),
|
||||
chat: { id: chatId },
|
||||
text,
|
||||
},
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
function handleSimpleRecord(res, method, body) {
|
||||
recordMockTelegramSent({ method, body: body ?? {} });
|
||||
console.log(`[telegram-mock] ${method} recorded`);
|
||||
json(res, 200, { ok: true, result: true });
|
||||
return true;
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import { handleInvites } from "./routes/invites.mjs";
|
||||
import { handleLlmCompletions } from "./routes/llm.mjs";
|
||||
import { handleOAuth } from "./routes/oauth.mjs";
|
||||
import { handlePayments } from "./routes/payments.mjs";
|
||||
import { handleTelegram } from "./routes/telegram.mjs";
|
||||
import { handleUser } from "./routes/user.mjs";
|
||||
import { handleVersion } from "./routes/version.mjs";
|
||||
import { handleWebhooks } from "./routes/webhooks.mjs";
|
||||
@@ -38,6 +39,9 @@ let server = null;
|
||||
// Order matters: admin & socket.io short-circuit early; the rest fall through
|
||||
// in domain order so the cheapest predicates run first.
|
||||
const ROUTE_HANDLERS = [
|
||||
// Telegram Bot API paths start with /bot<token>/… — check before the
|
||||
// general-purpose handlers so the distinctive prefix routes cleanly.
|
||||
handleTelegram,
|
||||
handleOAuth,
|
||||
handleAuth,
|
||||
handleUser,
|
||||
|
||||
@@ -22,6 +22,11 @@ let socketEventLog = [];
|
||||
let mockLlmThreads = new Map();
|
||||
let nextSequence = 1;
|
||||
|
||||
// ── Telegram Bot API mock state ────────────────────────────────────────────
|
||||
let mockTelegramUpdates = [];
|
||||
let mockTelegramSentMessages = [];
|
||||
let mockTelegramMessageIdSeq = 1000;
|
||||
|
||||
const socketSessions = new Map();
|
||||
|
||||
export const openSockets = new Set();
|
||||
@@ -498,3 +503,53 @@ export function getMockTeam() {
|
||||
role: "ADMIN",
|
||||
};
|
||||
}
|
||||
|
||||
// ── Telegram Bot API mock state helpers ────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Push a single Telegram Update object into the pending queue.
|
||||
* The queue is drained by each `getUpdates` poll.
|
||||
*/
|
||||
export function pushMockTelegramUpdate(update) {
|
||||
mockTelegramUpdates.push(update);
|
||||
}
|
||||
|
||||
/**
|
||||
* Drain and return all pending Telegram updates. Each `getUpdates` call
|
||||
* should call this so each update is delivered exactly once.
|
||||
*/
|
||||
export function drainMockTelegramUpdates() {
|
||||
const updates = [...mockTelegramUpdates];
|
||||
mockTelegramUpdates = [];
|
||||
return updates;
|
||||
}
|
||||
|
||||
/**
|
||||
* Record a message sent by the bot (sendMessage, sendPhoto, etc.).
|
||||
* @param {object} entry - { method, body, message_id, ... }
|
||||
*/
|
||||
export function recordMockTelegramSent(entry) {
|
||||
mockTelegramSentMessages.push({
|
||||
timestamp: new Date().toISOString(),
|
||||
...entry,
|
||||
});
|
||||
}
|
||||
|
||||
/** Return all recorded outbound Telegram API calls (non-destructive). */
|
||||
export function getMockTelegramSent() {
|
||||
return [...mockTelegramSentMessages];
|
||||
}
|
||||
|
||||
/** Increment and return the next mock message_id. */
|
||||
export function nextMockTelegramMessageId() {
|
||||
const id = mockTelegramMessageIdSeq;
|
||||
mockTelegramMessageIdSeq += 1;
|
||||
return id;
|
||||
}
|
||||
|
||||
/** Reset all Telegram mock state (updates queue, sent log, id counter). */
|
||||
export function resetMockTelegram() {
|
||||
mockTelegramUpdates = [];
|
||||
mockTelegramSentMessages = [];
|
||||
mockTelegramMessageIdSeq = 1000;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user