import assert from "node:assert/strict"; import test from "node:test"; import { type Api, Context, callbackData, createBot, InlineKeyboard, richContext, session, } from "./index.js"; const api = {} as never; const messageUpdate = { update_id: 1, message: { message_id: 5, date: 0, chat: { id: 42, type: "private" }, from: { id: 7, is_bot: false, first_name: "u" }, text: "hi", }, } as never; test("richContext is a real Context with payload fields and shortcut methods", () => { const ctx = richContext(api, messageUpdate, "message"); assert.ok(ctx instanceof Context); // satisfies the middleware's Context contract assert.equal(ctx.text, "hi"); // core getter still works assert.equal((ctx as Context & { message_id: number }).message_id, 5); // payload field grafted on assert.equal(typeof (ctx as Context & { react: unknown }).react, "function"); // autogen shortcut grafted on assert.equal(typeof ctx.send, "function"); // core method preserved }); test("createBot returns a Bot wired with the rich factory", () => { const bot = createBot("123:abc"); assert.equal(typeof bot.handleUpdate, "function"); assert.equal(typeof bot.start, "function"); }); // compile-time proof: the typed routers narrow ctx to the rich per-update context. // (this block only builds if the types actually flow — it's the type test.) test("typed routers expose the generated shortcuts + narrowed fields", () => { const bot = createBot("123:abc"); bot.on("message:text", (ctx) => { const t: string = ctx.text; // narrowed to string by the filter query void t; void ctx.react; // MessageContext shortcut — typed, not just runtime void ctx.editText; }); bot.on("callback_query:data", (ctx) => { void ctx.answer; // CallbackQueryContext shortcut }); bot.command("start", (ctx) => { const args: string[] = ctx.args; void args; void ctx.react; // command handlers get the message context }); bot.on("guest_message", (ctx) => { void ctx.answer; // GuestMessageContext shortcut — answerGuestQuery, not sendMessage void ctx.guest_bot_caller_user; // who summoned the guest bot }); assert.ok(bot); }); test("typed enrichment keeps generated shortcuts", () => { const vibe = callbackData("vibe", { score: Number }); const bot = createBot("123:abc") .install(session({ initial: () => ({ fire: 0 }) })) .derive((ctx) => ({ who: ctx.from?.first_name ?? "friend" })); bot.on("message:text", (ctx) => { const text: string = ctx.text; const who: string = ctx.who; const fire: number = ++ctx.session.fire; void text; void who; void fire; void ctx.react; }); bot.callbackQuery(vibe.pattern, (ctx) => { const who: string = ctx.who; const fire: number = ctx.session.fire; void who; void fire; void ctx.answer; }); assert.ok(bot); }); test("guest_message answer() posts a real message via answerGuestQuery", async () => { const guestUpdate = { update_id: 1, guest_message: { message_id: 5, date: 0, chat: { id: 42, type: "private" }, guest_query_id: "gq1", guest_bot_caller_user: { id: 7, is_bot: false, first_name: "u" }, }, } as never; const calls: { method: string; params: unknown }[] = []; const stubApi = { call: (method: string, params: unknown) => { calls.push({ method, params }); return Promise.resolve({ inline_message_id: "im1" }); }, } as never; const ctx = richContext(stubApi, guestUpdate, "guest_message") as Context & { answer: (result: unknown) => Promise; guest_bot_caller_user?: { first_name: string }; }; assert.equal(ctx.guest_bot_caller_user?.first_name, "u"); await ctx.answer({ type: "article", id: "1", title: "hi", input_message_content: { message_text: "hi" }, }); assert.deepEqual(calls[0], { method: "answerGuestQuery", params: { guest_query_id: "gq1", result: { type: "article", id: "1", title: "hi", input_message_content: { message_text: "hi" }, }, }, }); }); test("new Bot() (without createBot) wires the rich contexts it promises in its types", async () => { const { Bot } = await import("./index.js"); let reactType = ""; const bot = new Bot("123:abc").on("message:text", (ctx) => { reactType = typeof ctx.react; // typed by RichFor — must also exist at runtime }); await bot.handleUpdate(messageUpdate as never); assert.equal(reactType, "function"); }); test("rich context: object-form send() keeps every param (no silent drops)", async () => { const calls: unknown[] = []; const stubApi = { call: (_method: string, p: unknown) => { calls.push(p); return Promise.resolve({}); }, } as never; const ctx = richContext(stubApi, messageUpdate, "message") as Context & { send: (p: Record) => Promise; }; await ctx.send({ text: "yo", reply_markup: { inline_keyboard: [] } }); assert.deepEqual(calls[0], { chat_id: 42, text: "yo", reply_markup: { inline_keyboard: [] }, }); }); test("keyboard builders satisfy typed reply_markup; guard narrows through the meta Bot", () => { // compile-time proof: the builder is assignable to the typed reply_markup param const markup: NonNullable[0]["reply_markup"]> = new InlineKeyboard().text("ok", "cb"); void markup; const bot = createBot("123:abc") .guard((ctx): ctx is typeof ctx & { admin: true } => Boolean(ctx.from)) .on("message:text", (ctx) => { const admin: true = ctx.admin; // the type-guard narrowing survives the chain void admin; void ctx.react; // rich typing intact after guard }); bot.hears(/hi/, (ctx) => { void ctx.react; // hears handlers get the rich message context too void ctx.match; }); assert.ok(bot); }); test("the batteries are reachable from the single import and install on a bot", async () => { const mod = await import("./index.js"); // one entry per plugin the meta package promises — a missing re-export fails here // instead of at a user's `import { … } from "yaebal"`. for (const name of [ "autoRetry", "autoAnswer", "hydrate", "typing", "files", "splitter", "FileId", "redisStorage", ]) { assert.equal(typeof (mod as Record)[name], "function", `${name} missing`); } // InlineQueryResult/InputMessageContent are namespaces of builders, not plugins assert.equal(typeof mod.InlineQueryResult.article, "function"); assert.equal(typeof mod.InputMessageContent.text, "function"); // and they compose on a real bot without fighting over the context type const bot = createBot("123:abc") .install(mod.autoRetry()) .install(mod.autoAnswer()) .install(mod.hydrate()) .install(mod.typing()) .install(mod.files()) .install(mod.splitter()); const ctx = richContext(api, messageUpdate, "message") as Context & Record; await bot.handleUpdate(messageUpdate); assert.ok(ctx instanceof Context); });