@xbibzlibrary/telebibz 0.4.4 → 0.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,147 @@
1
+ # 从 Telegraf 迁移(简体中文)
2
+
3
+ telebibz 有意实现了 Telegraf 的 context 表面与启动选项,因此大多数 handler 只需极少改动即可移植。本指南把 Telegraf bot 的每个部分映射到 telebibz 对应物。
4
+
5
+ ## 目录
6
+
7
+ 1. [并排对照:完整 bot](#1-并排对照完整-bot)
8
+ 2. [概念映射](#2-概念映射)
9
+ 3. [Context 方法](#3-context-方法)
10
+ 4. [启动选项](#4-启动选项)
11
+ 5. [Scenes → Wizards](#5-scenes--wizards)
12
+ 6. [会话存储](#6-会话存储)
13
+ 7. [Webhook](#7-webhook)
14
+ 8. [没有直接对应物的部分](#8-没有直接对应物的部分)
15
+
16
+ ## 1. 并排对照:完整 bot
17
+
18
+ **Telegraf**
19
+
20
+ ```ts
21
+ import { Telegraf } from "telegraf";
22
+
23
+ const bot = new Telegraf(process.env.TELEGRAM_BOT_TOKEN!);
24
+
25
+ bot.use(async (ctx, next) => { console.time("update"); await next(); console.timeEnd("update"); });
26
+ bot.start((ctx) => ctx.reply("Welcome!"));
27
+ bot.command("help", (ctx) => ctx.reply("Help"));
28
+ bot.action("menu:open", async (ctx) => { await ctx.answerCbQuery(); await ctx.reply("Menu"); });
29
+ bot.on("message", (ctx) => ctx.reply("got it"));
30
+ bot.catch((error) => console.error(error));
31
+
32
+ bot.launch({ dropPendingUpdates: true });
33
+ ```
34
+
35
+ **telebibz**
36
+
37
+ ```ts
38
+ import { Bot } from "@xbibzlibrary/telebibz";
39
+
40
+ const bot = new Bot(process.env.TELEGRAM_BOT_TOKEN!);
41
+
42
+ bot.use(async (ctx, next) => { console.time("update"); await next(); console.timeEnd("update"); });
43
+ bot.command("start", async (ctx) => { await ctx.reply("Welcome!"); }); // 具名命令,而非 bot.start()
44
+ bot.command("help", async (ctx) => { await ctx.reply("Help"); });
45
+ bot.action("menu:open", async (ctx) => { await ctx.answerCallbackQuery(); await ctx.reply("Menu"); });
46
+ bot.on("message", async (ctx) => { await ctx.reply("got it"); });
47
+ bot.catch(async (error) => { console.error(error); });
48
+
49
+ await bot.launch({ dropPendingUpdates: true }); // 选项名相同
50
+ ```
51
+
52
+ 只有两处机械差异:`bot.start(handler)` 变为 `bot.command("start", handler)`;`answerCbQuery()` 变为 `answerCallbackQuery()`。
53
+
54
+ ## 2. 概念映射
55
+
56
+ | Telegraf | telebibz | 说明 |
57
+ |---|---|---|
58
+ | `new Telegraf(token)` | `new Bot(token)` 或 `new Bot({ token, ... })` | |
59
+ | `bot.launch()` | `bot.launch()` / `bot.start()` | `launch` 上显式声明 `mode: "polling"` |
60
+ | `bot.stop()` | `bot.stop()` | telebibz 先排空在途 handler |
61
+ | `bot.use(mw)` | `bot.use(mw)` | 中间件签名相同 `(ctx, next)` |
62
+ | `bot.command(name, h)` | `bot.command(name, h)` | |
63
+ | `bot.on(filter, h)` | `bot.on(filter, h)` | 过滤语法相同(`message:photo`、数组) |
64
+ | `bot.hears(trigger, h)` | `bot.hears(trigger, h)` | 字符串与 RegExp |
65
+ | `bot.action(pattern, h)` | `bot.action(pattern, h)` | `bot.callback` 的直接别名 |
66
+ | `bot.catch(handler)` | `bot.catch(handler)` | 接收 `(error, ctx)` |
67
+
68
+ ## 3. Context 方法
69
+
70
+ Telegraf context 的每个快捷方法都在 —— 包括 Telegraf 留给插件的那部分:
71
+
72
+ - **回复**:`reply`、`replyWithPhoto`、`replyWithDocument`、`replyWithVideo`、`replyWithAudio`、`replyWithVoice`、`replyWithAnimation`、`replyWithVideoNote`、`replyWithSticker`、`replyWithMediaGroup`、`replyWithLocation`、`replyWithVenue`、`replyWithContact`、`replyWithPoll`、`replyWithQuiz`、`replyWithDice`、`replyWithGame`、`replyWithInvoice`、`replyWithHTML`、`replyWithMarkdown`(+V2)
73
+ - **管理/群管**:`banChatMember`、`unbanChatMember`、`restrictChatMember`、`promoteChatMember`、`banChatSenderChat`、`unbanChatSenderChat`
74
+ - **聊天**:`setChatTitle`、`setChatDescription`、`setChatPhoto`、`deleteChatPhoto`、`setChatPermissions`、`leaveChat`、`unpinAllChatMessages`、`setChatStickerSet`、`deleteChatStickerSet`
75
+ - **信息**:`getChat`、`getChatAdministrators`、`getChatMemberCount`、`getChatMember`
76
+ - **邀请链接/加群申请**:`exportChatInviteLink`、`createChatInviteLink`、`editChatInviteLink`、`revokeChatInviteLink`、`approveChatJoinRequest`、`declineChatJoinRequest`
77
+ - **实时位置/投票/游戏**:`editMessageLiveLocation`、`stopMessageLiveLocation`、`stopPoll`、`setGameScore`、`getGameHighScores`
78
+ - **论坛**:完整话题方法集(`createForumTopic` … `unhideGeneralForumTopic`)
79
+ - **超越 Telegraf 核心的新增**:`getFile`(带类型)、`downloadFile`、`edit`(改写当前消息文本),以及从包根导出的独立助手 —— `validateUpload`/`assertValidUpload` —— 它们不是 context 方法
80
+
81
+ 移植时需修正的命名差异:`answerCbQuery` → `answerCallbackQuery`;`ctx.telegram` → `ctx.api`;键盘助手来自包根(`InlineKeyboard`、`ReplyKeyboard`、`removeKeyboard`、`forceReply`)而非 `Markup`。
82
+
83
+ ## 4. 启动选项
84
+
85
+ | Telegraf | telebibz |
86
+ |---|---|
87
+ | `launch({ dropPendingUpdates })` | `launch({ dropPendingUpdates })` —— 完全相同 |
88
+ | `handlerTimeout`(默认 90 000) | `handlerTimeout`(默认 90 000;`0` 禁用) |
89
+ | `contextType` 选项 | `contextType` 选项 —— 你的 `Context` 子类会为每个更新实例化 |
90
+ | `webhookReply`(按更新) | 处理器 / `handleUpdate` 选项上的 `webhookReply` |
91
+ | `telegraf.use(session(...))` | `new Bot({ session: new MemoryStorage() })`(或 JSON/Redis/SQL/Mongo) |
92
+
93
+ ## 5. Scenes → Wizards
94
+
95
+ Telegraf 的 `WizardScene` + `Stage` 变成单个 `Wizard`:显式步骤、无全局 session 键:
96
+
97
+ ```ts
98
+ import { Bot, Wizard } from "@xbibzlibrary/telebibz";
99
+
100
+ const wizard = new Wizard()
101
+ .step({ id: "ask-name", run: async (flow) => { flow.next(); await flow.ctx.reply("名字?"); } })
102
+ .step({ id: "save", run: async (flow) => { await flow.ctx.reply(`你好 ${flow.ctx.message?.text}!`); } });
103
+
104
+ const bot = new Bot(process.env.TELEGRAM_BOT_TOKEN!);
105
+ bot.useWizard(wizard); // 取代 Stage 中间件
106
+ bot.command("start", async (ctx) => { await wizard.run(ctx); }); // 取代 scene.enter()
107
+ ```
108
+
109
+ - 向导键自动由 chat + 发送者派生 —— 无需手工管理键。
110
+ - `flow.set(key, value)` / `flow.get(key)` 取代 `ctx.scene.session`。
111
+ - `/cancel` 取消;最后一步完成后对话自动收尾。
112
+ - 非线性流程图请用 `ConversationManager` 配合 router 组合(telebibz 刻意把场景编排留给应用;见 FEATURE_MATRIX 的 "Design decisions")。
113
+
114
+ ## 6. 会话存储
115
+
116
+ Telegraf 默认把会话放在内存,持久化需要 store 插件。telebibz 在构造函数上接收 storage —— 换适配器,不换代码:
117
+
118
+ ```ts
119
+ import { Bot, MemoryStorage, JsonFileStorage, RedisStorage } from "@xbibzlibrary/telebibz";
120
+
121
+ const bot = new Bot({
122
+ token: process.env.TELEGRAM_BOT_TOKEN!,
123
+ session: new JsonFileStorage("state/sessions.json"), // 或 MemoryStorage / RedisStorage / SqlStorage / MongoStorage
124
+ });
125
+ ```
126
+
127
+ 每个适配器的完整接线配方:[STORAGE.zh-CN.md](STORAGE.zh-CN.md)。
128
+
129
+ ## 7. Webhook
130
+
131
+ ```ts
132
+ // Telegraf: webhookCallback(bot, app)
133
+ // telebibz: 框架需显式指定
134
+ import { webhookCallback } from "@xbibzlibrary/telebibz";
135
+ app.post("/telegram", webhookCallback(bot, "express", { secretToken: process.env.TELEGRAM_WEBHOOK_SECRET }));
136
+ ```
137
+
138
+ `createWebhookHandler()` 另外提供 Web 标准 `Request → Response` 处理器,适配 Bun/Deno/edge。完整部署指南:[WEBHOOK.zh-CN.md](WEBHOOK.zh-CN.md)。
139
+
140
+ ## 8. 没有直接对应物的部分
141
+
142
+ - **`bot.telegram` 底层客户端** —— 使用 `bot.api`(`call`、`raw`、`methods`、`downloadFile`);flood gate 与重试内置于传输层,不能按调用配置。
143
+ - **Telegraf 插件生态** —— 把插件移植为带显式生命周期(`install`、`onStop`、`dispose`)的 `Plugin` 对象;插件管理器可干净重启。
144
+ - **`Composer.mount`/动态 scene** —— 改用 `Router` 嵌套与 `matchMode: "all"` 组合。
145
+ - **Markup 链式助手**(`Markup.keyboard(...).resize()`) —— 使用 `new ReplyKeyboard().text("A").resized().build()`;负载相同,builder 风格。
146
+
147
+ English: [MIGRATION_TELEGRAF.md](MIGRATION_TELEGRAF.md) · Bahasa Indonesia: [MIGRATION_TELEGRAF.id.md](MIGRATION_TELEGRAF.id.md)
package/docs/README.md CHANGED
@@ -1,39 +1,58 @@
1
1
  # telebibz Documentation
2
2
 
3
- The default documentation language is **English**. Translated README and API references are available below.
3
+ The default documentation language is **English**. Every guide below is available in English, Bahasa Indonesia, and 简体中文.
4
4
 
5
- | Language | README | Complete API reference |
6
- |---|---|---|
7
- | English (default) | [`README.md`](../README.md) | [`API.md`](API.md) |
8
- | Bahasa Indonesia | [`README.id.md`](../README.id.md) | [`API.id.md`](API.id.md) |
9
- | 简体中文 | [`README.zh-CN.md`](../README.zh-CN.md) | [`API.zh-CN.md`](API.zh-CN.md) |
5
+ | Language | README | Getting started | Complete API reference |
6
+ |---|---|---|---|
7
+ | English (default) | [`README.md`](../README.md) | [`GETTING_STARTED.md`](GETTING_STARTED.md) | [`API.md`](API.md) |
8
+ | Bahasa Indonesia | [`README.id.md`](../README.id.md) | [`GETTING_STARTED.id.md`](GETTING_STARTED.id.md) | [`API.id.md`](API.id.md) |
9
+ | 简体中文 | [`README.zh-CN.md`](../README.zh-CN.md) | [`GETTING_STARTED.zh-CN.md`](GETTING_STARTED.zh-CN.md) | [`API.zh-CN.md`](API.zh-CN.md) |
10
10
 
11
11
  ![telebibz overview](https://cdn.jsdelivr.net/npm/@xbibzlibrary/telebibz@latest/assets/telebibz-readme-preview.png)
12
12
 
13
- The documentation covers the complete lifecycle: onboarding, bot startup and shutdown, API client and transport, update routing, middleware and context, state/session, interaction UI, background work, deployment, testing, and migration boundaries.
13
+ The documentation covers the complete lifecycle: onboarding, bot startup and shutdown, API client and transport, update routing, middleware and context, state/session, interaction UI, background work, files, errors, webhook deployment, testing, migration, and production recipes.
14
14
 
15
- | Resource | Purpose |
16
- |---|---|
17
- | [`GETTING_STARTED.md`](GETTING_STARTED.md) | Five-minute English onboarding from installation to a working bot. |
18
- | [`GETTING_STARTED.id.md`](GETTING_STARTED.id.md) | Indonesian onboarding guide. |
19
- | [`GETTING_STARTED.zh-CN.md`](GETTING_STARTED.zh-CN.md) | Simplified Chinese onboarding guide. |
20
- | [`../examples/README.md`](../examples/README.md) | Runnable minimal, wizard, and webhook starters. |
21
- | [`../SHOWCASE.md`](../SHOWCASE.md) | Community project showcase and submission format. |
15
+ ## Complete guide catalog
16
+
17
+ Every topic guide ships in three languages (EN · ID · ZH). Pick a topic, pick a language:
18
+
19
+ | Guide | English | Bahasa Indonesia | 简体中文 | Purpose |
20
+ |---|---|---|---|---|
21
+ | Getting started | [`GETTING_STARTED.md`](GETTING_STARTED.md) | [`GETTING_STARTED.id.md`](GETTING_STARTED.id.md) | [`GETTING_STARTED.zh-CN.md`](GETTING_STARTED.zh-CN.md) | Five-minute onboarding from installation to a working bot. |
22
+ | Complete API reference | [`API.md`](API.md) | [`API.id.md`](API.id.md) | [`API.zh-CN.md`](API.zh-CN.md) | Every exported class, method, type, error, and generated Telegram method. |
23
+ | Files: upload & download | [`FILES.md`](FILES.md) | [`FILES.id.md`](FILES.id.md) | [`FILES.zh-CN.md`](FILES.zh-CN.md) | `downloadFile()`, manual `getFile()`, `file_path` vs `filePath`, upload sources, validation, limits, troubleshooting. |
24
+ | Errors | [`ERRORS.md`](ERRORS.md) | [`ERRORS.id.md`](ERRORS.id.md) | [`ERRORS.zh-CN.md`](ERRORS.zh-CN.md) | Error taxonomy, 429 and the flood gate, `bot.catch()`, `handlerTimeout`, retries, graceful shutdown. |
25
+ | Webhook | [`WEBHOOK.md`](WEBHOOK.md) | [`WEBHOOK.id.md`](WEBHOOK.id.md) | [`WEBHOOK.zh-CN.md`](WEBHOOK.zh-CN.md) | Polling vs webhook, `createWebhookHandler()`, Express/http/Fastify/Koa, `setWebhook`, secret tokens, tunnels, production checklist. |
26
+ | Testing | [`TESTING.md`](TESTING.md) | [`TESTING.id.md`](TESTING.id.md) | [`TESTING.zh-CN.md`](TESTING.zh-CN.md) | Fully offline testing with `MockTransport`, driving updates, wizards end to end, webhooks, error paths, Vitest patterns. |
27
+ | Migration from Telegraf | [`MIGRATION_TELEGRAF.md`](MIGRATION_TELEGRAF.md) | [`MIGRATION_TELEGRAF.id.md`](MIGRATION_TELEGRAF.id.md) | [`MIGRATION_TELEGRAF.zh-CN.md`](MIGRATION_TELEGRAF.zh-CN.md) | Side-by-side port, concept map, context methods, scenes → wizards, webhooks. |
28
+ | Storage | [`STORAGE.md`](STORAGE.md) | [`STORAGE.id.md`](STORAGE.id.md) | [`STORAGE.zh-CN.md`](STORAGE.zh-CN.md) | Memory/JSON/Redis/SQL/Mongo adapters, TTL, atomic `update()`. |
29
+ | Production cookbook | [`COOKBOOK.md`](COOKBOOK.md) | [`COOKBOOK.id.md`](COOKBOOK.id.md) | [`COOKBOOK.zh-CN.md`](COOKBOOK.zh-CN.md) | Thirteen verified recipes: rate limiting, auth, broadcast, scheduling, queues, menus, forms, caching, Mini Apps, payments, metrics. |
30
+ | GitHub Packages install | [`GITHUB_PACKAGES.md`](GITHUB_PACKAGES.md) | [`GITHUB_PACKAGES.id.md`](GITHUB_PACKAGES.id.md) | [`GITHUB_PACKAGES.zh-CN.md`](GITHUB_PACKAGES.zh-CN.md) | Installing via GitHub Packages with a personal access token. |
31
+
32
+ Also available: [`../examples/README.md`](../examples/README.md) — runnable minimal, wizard, file, and webhook starters, and [`../SHOWCASE.md`](../SHOWCASE.md) — community project showcase.
33
+
34
+ ## Coverage status
22
35
 
23
36
  | Area | Status |
24
37
  |---|---|
25
- | Getting started | Dedicated guides are available in English, Indonesian, and Simplified Chinese, with runnable examples. |
26
- | Complete API reference | Available in English, Indonesian, and Simplified Chinese |
38
+ | Getting started | Dedicated guides in EN/ID/ZH with runnable examples |
39
+ | Complete API reference | Available in EN/ID/ZH |
27
40
  | Bot lifecycle, polling, webhook | Core implementation, per-update error isolation, reconnect backoff, and tests available |
28
- | API client and generated method list | Available; full vendored Telegram declarations are exposed through `TelegramTypes` |
41
+ | API client and generated method list | Available; full vendored Telegram declarations exposed through `TelegramTypes` |
29
42
  | Router, middleware, context | Available and tested; first-match is default, all-match is explicit |
30
43
  | Keyboard, callback, menus, pagination | Keyboard/callback core, permission menus, MenuController, and pagination available |
31
44
  | Sessions, conversations, wizards, forms | Storage-backed session/conversation primitives and forms available; scene orchestration remains application-owned |
32
45
  | Storage, cache, queue, scheduler | Memory, JSON file, Redis, SQL, Mongo driver adapters, cache, queue, and full five-field cron available |
46
+ | Files | Dedicated guide: one-call `downloadFile()`, upload sources, validation, limits, troubleshooting (EN/ID/ZH) |
47
+ | Errors | Dedicated guide: taxonomy, 429/flood gate, boundaries, timeouts, retries (EN/ID/ZH) |
48
+ | Webhook deployment | Dedicated guide: handler, four frameworks, registration, secrets, tunnels, checklist (EN/ID/ZH) |
49
+ | Testing | Dedicated guide: MockTransport, update drivers, wizards, webhooks, error paths (EN/ID/ZH) |
50
+ | Migration | Dedicated Telegraf migration guide (EN/ID/ZH) |
51
+ | Production recipes | Cookbook with thirteen verified recipes (EN/ID/ZH) |
33
52
  | Plugins, services, observability | Lifecycle/plugin/service hooks available |
34
53
  | Mini Apps, payments, business features | Web App signature validation and PaymentsClient wrappers available; UI is application-owned |
35
54
  | Testing and security | Unit, integration, type-level, gated E2E, CI, and security policy available |
36
- | Deployment and migration | Release automation is documented in `RELEASE_AUTOMATION.md`; webhook and deployment onboarding is in `GETTING_STARTED.md` |
55
+ | Deployment and migration | Release automation is documented in `RELEASE_AUTOMATION.md`; webhook deployment is in `WEBHOOK.md` |
37
56
  | Governance and community | `CODE_OF_CONDUCT.md`, `GOVERNANCE.md`, `CONTRIBUTING.md`, `CONTRIBUTION_RULES.md`, and `SHOWCASE.md` |
38
57
  | Security and support | `SECURITY.md` and `SUPPORT.md` |
39
58
  | Third-party notices | `NOTICE.md` and `LICENSE` |
@@ -45,4 +64,4 @@ Issue forms are available for bug reports, feature requests, documentation probl
45
64
 
46
65
  ## Documentation principle
47
66
 
48
- The documentation describes only capabilities that are implemented and tested in the current package. Telegram-native API access, Mini App/Web App behavior, external persistence, and distributed adapters are described separately so the documentation does not promise features that are not included.
67
+ The documentation describes only capabilities that are implemented and tested in the current package, and every code snippet in the guides is verified against the actual implementation. Telegram-native API access, Mini App/Web App behavior, external persistence, and distributed adapters are described separately so the documentation does not promise features that are not included.
@@ -0,0 +1,203 @@
1
+ # Panduan testing (Bahasa Indonesia)
2
+
3
+ Uji bot telebibz sepenuhnya offline: palsukan transport, rakit update, asersikan panggilan keluar, dan jalankan conversation utuh end-to-end.
4
+
5
+ ## Daftar isi
6
+
7
+ 1. [Subpath testing](#1-subpath-testing)
8
+ 2. [MockTransport](#2-mocktransport)
9
+ 3. [Men-drive update ke dalam bot](#3-men-drive-update-ke-dalam-bot)
10
+ 4. [Asersi panggilan keluar](#4-asersi-panggilan-keluar)
11
+ 5. [Menguji wizard end-to-end](#5-menguji-wizard-end-to-end)
12
+ 6. [Menguji unduhan file](#6-menguji-unduhan-file)
13
+ 7. [Menguji webhook](#7-menguji-webhook)
14
+ 8. [Menguji jalur error](#8-menguji-jalur-error)
15
+ 9. [Pola Vitest / Jest](#9-pola-vitest--jest)
16
+
17
+ ## 1. Subpath testing
18
+
19
+ Semuanya tersedia dari `@xbibzlibrary/telebibz/testing`:
20
+
21
+ ```ts
22
+ import { MockTransport, createTestBot, createMockUpdate, createMockCallbackUpdate, createMockContext } from "@xbibzlibrary/telebibz/testing";
23
+ ```
24
+
25
+ - `createTestBot()` → `{ bot, transport }` — sebuah `Bot` yang terhubung ke `MockTransport`, branding dimatikan.
26
+ - `createMockUpdate(overrides?)` → update pesan teks `/start` sederhana.
27
+ - `createMockCallbackUpdate(overrides?)` → update callback-query.
28
+ - `createMockContext(bot, update?)` → sebuah `Context` tanpa me-routing apa pun.
29
+
30
+ ## 2. MockTransport
31
+
32
+ `MockTransport` merekam setiap request keluar dan menjawab dari response yang dikonfigurasi:
33
+
34
+ ```ts
35
+ const { bot, transport } = createTestBot();
36
+
37
+ transport.respond("getMe", { ok: true, result: { id: 99, is_bot: true, first_name: "TestBot", username: "test_bot" } });
38
+ transport.respond("sendMessage", { ok: true, result: { message_id: 1, date: 0, chat: { id: 1, type: "private" } } });
39
+ transport.respond("getFile", (payload) => ({ // jawaban dinamis
40
+ ok: true,
41
+ result: { file_id: (payload as { file_id: string }).file_id, file_unique_id: "u", file_path: "documents/a.pdf" },
42
+ }));
43
+ ```
44
+
45
+ Method yang tidak dikonfigurasi menjawab `{ ok: true, result: true }` — cukup untuk panggilan yang hasilnya Anda abaikan.
46
+
47
+ Ia juga mengimplementasikan member download:
48
+
49
+ ```ts
50
+ transport.downloadBytes = new TextEncoder().encode("file-content");
51
+ transport.downloads; // setiap file_path yang diberikan ke download(), berurutan
52
+ transport.fileUrl("a.pdf"); // "mock://files/a.pdf"
53
+ ```
54
+
55
+ ## 3. Men-drive update ke dalam bot
56
+
57
+ Daftarkan handler, lalu kirim update — tanpa jaringan, tanpa token:
58
+
59
+ ```ts
60
+ bot.on("message", async (ctx) => { await ctx.reply("hi"); });
61
+ await bot.init(); // tepat satu getMe, seperti produksi
62
+
63
+ await bot.handleUpdate(createMockUpdate({ message: { ...createMockUpdate().message!, text: "hello" } }));
64
+ await bot.handleUpdates([updateA, updateB, updateC]); // satu batch penuh, paralel antar chat
65
+ ```
66
+
67
+ Untuk mensimulasikan beberapa chat, variasikan chat id:
68
+
69
+ ```ts
70
+ function textUpdate(updateId: number, chatId: number, text: string) {
71
+ const base = createMockUpdate();
72
+ return { ...base, update_id: updateId, message: { ...base.message!, chat: { id: chatId, type: "private" as const }, text } };
73
+ }
74
+ ```
75
+
76
+ ## 4. Asersi panggilan keluar
77
+
78
+ Setiap request tercatat di `transport.calls` lengkap dengan method dan payload-nya:
79
+
80
+ ```ts
81
+ const replies = transport.calls
82
+ .filter((call) => call.method === "sendMessage")
83
+ .map((call) => (call.payload as { text?: string }).text);
84
+
85
+ expect(replies).toEqual(["Siapa nama Anda?", "Berapa usia Anda?"]);
86
+
87
+ // Keyboard callback:
88
+ const markup = (transport.calls.at(-1)?.payload as { reply_markup?: { inline_keyboard: unknown[][] } }).reply_markup;
89
+ expect(markup?.inline_keyboard).toHaveLength(2);
90
+ ```
91
+
92
+ ## 5. Menguji wizard end-to-end
93
+
94
+ Berikan conversation satu pesan demi satu pesan dan asersikan balasan yang akan dilihat user:
95
+
96
+ ```ts
97
+ import { Bot, Wizard } from "@xbibzlibrary/telebibz";
98
+
99
+ const wizard = new Wizard()
100
+ .step({ id: "ask", run: async (flow) => { flow.next(); await flow.ctx.reply("Siapa nama Anda?"); } })
101
+ .step({ id: "save", run: async (flow) => { await flow.ctx.reply(`Halo, ${flow.ctx.message?.text}!`); } });
102
+ bot.useWizard(wizard);
103
+ bot.command("start", async (ctx) => { await wizard.run(ctx); });
104
+
105
+ await bot.handleUpdate(textUpdate(1, 7, "/start"));
106
+ await bot.handleUpdate(textUpdate(2, 7, "Dewi"));
107
+
108
+ expect(sentTexts()).toEqual(["Siapa nama Anda?", "Halo, Dewi!"]);
109
+ ```
110
+
111
+ Dua chat, dua wizard independen — update yang berselang-seling tetap terisolasi karena update dari chat yang sama diserialisasi:
112
+
113
+ ```ts
114
+ await bot.handleUpdate(textUpdate(1, 7, "/start"));
115
+ await bot.handleUpdate(textUpdate(2, 8, "/start"));
116
+ await bot.handleUpdate(textUpdate(3, 7, "Dewi"));
117
+ await bot.handleUpdate(textUpdate(4, 8, "Budi"));
118
+ expect(sentTexts()).toEqual(["Siapa nama Anda?", "Siapa nama Anda?", "Halo, Dewi!", "Halo, Budi!"]);
119
+ ```
120
+
121
+ ## 6. Menguji unduhan file
122
+
123
+ ```ts
124
+ transport.respond("getFile", { ok: true, result: { file_id: "F1", file_unique_id: "U1", file_path: "photos/pic.jpg" } });
125
+ transport.downloadBytes = new TextEncoder().encode("jpeg-bytes");
126
+
127
+ const file = await bot.downloadFile("F1", { destination: tmpFile });
128
+ expect(file.fileName).toBe("pic.jpg");
129
+ expect(transport.downloads).toEqual(["photos/pic.jpg"]);
130
+ expect(await readFile(tmpFile, "utf8")).toBe("jpeg-bytes");
131
+ ```
132
+
133
+ ## 7. Menguji webhook
134
+
135
+ `createWebhookHandler` menerima `Request` Web sungguhan:
136
+
137
+ ```ts
138
+ const handler = createWebhookHandler(bot, { secretToken: "s3cret" });
139
+
140
+ const response = await handler(new Request("https://example.com/telegram", {
141
+ method: "POST",
142
+ headers: { "content-type": "application/json", "x-telegram-bot-api-secret-token": "s3cret" },
143
+ body: JSON.stringify(createMockUpdate()),
144
+ }));
145
+ expect(response.status).toBe(200);
146
+
147
+ // Secret salah → ditolak sebelum handler mana pun berjalan
148
+ const rejected = await handler(new Request("https://example.com/telegram", {
149
+ method: "POST",
150
+ headers: { "content-type": "application/json", "x-telegram-bot-api-secret-token": "salah" },
151
+ body: JSON.stringify(createMockUpdate()),
152
+ }));
153
+ expect(rejected.status).toBe(401);
154
+ ```
155
+
156
+ ## 8. Menguji jalur error
157
+
158
+ ```ts
159
+ // Telegram menolak panggilan
160
+ transport.respond("sendMessage", { ok: false, error_code: 400, description: "Bad Request: chat not found" });
161
+ await expect(ctx.reply("hi")).rejects.toThrow(/chat not found/);
162
+
163
+ // Handler melempar — asersikan error boundary melihatnya
164
+ const errors: unknown[] = [];
165
+ bot.events.on("update:error", ({ error }) => errors.push(error));
166
+ bot.onText("boom", async () => { throw new Error("boom"); });
167
+ await bot.handleUpdate(textUpdate(1, 7, "boom"));
168
+ expect(errors).toHaveLength(1);
169
+
170
+ // handlerTimeout: update yang menggantung melempar UpdateTimeoutError sementara handler tetap berjalan
171
+ const slow = new Bot({ token: "123456:TEST", transport, handlerTimeout: 50, branding: false, logger: { level: "silent" } });
172
+ let finished = false;
173
+ slow.on("message", async () => { await sleep(200); finished = true; });
174
+ await expect(slow.handleUpdate(createMockUpdate())).rejects.toBeInstanceOf(UpdateTimeoutError);
175
+ // nanti:
176
+ expect(finished).toBe(true);
177
+ ```
178
+
179
+ ## 9. Pola Vitest / Jest
180
+
181
+ **Senyapkan logger di test** — `logger: { level: "silent" }` tidak memancarkan apa pun (diuji regresi):
182
+
183
+ ```ts
184
+ const bot = new Bot({ token: "123456:TEST", transport, branding: false, logger: { level: "silent" } });
185
+ ```
186
+
187
+ **Transport segar per test** — jangan pernah berbagi state antar kasus:
188
+
189
+ ```ts
190
+ beforeEach(() => { ({ bot, transport } = createTestBot()); });
191
+ ```
192
+
193
+ **Mempercepat waktu** — untuk test `Scheduler`/TTL pakai fake timer atau interval kecil (`1ms`); parser cron murni dan bisa diuji tanpa timer:
194
+
195
+ ```ts
196
+ import { nextCronOccurrence, parseCronExpression } from "@xbibzlibrary/telebibz";
197
+ expect(parseCronExpression("*/15 * * * *")).toBeDefined();
198
+ expect(nextCronOccurrence("0 9 * * 1", new Date("2026-08-29T00:00:00Z")).toISOString()).toContain("T09:00");
199
+ ```
200
+
201
+ **Jalankan suite**: `npm test` — test E2E yang membutuhkan token asli otomatis di-skip ketika `TELEGRAM_BOT_TOKEN` tidak ada.
202
+
203
+ English: [TESTING.md](TESTING.md) · 简体中文: [TESTING.zh-CN.md](TESTING.zh-CN.md)
@@ -0,0 +1,203 @@
1
+ # Testing guide (English)
2
+
3
+ Test telebibz bots fully offline: fake the transport, craft updates, assert outgoing calls, and drive whole conversations end to end.
4
+
5
+ ## Contents
6
+
7
+ 1. [The testing subpath](#1-the-testing-subpath)
8
+ 2. [MockTransport](#2-mocktransport)
9
+ 3. [Driving updates through the bot](#3-driving-updates-through-the-bot)
10
+ 4. [Asserting outgoing calls](#4-asserting-outgoing-calls)
11
+ 5. [Testing wizards end to end](#5-testing-wizards-end-to-end)
12
+ 6. [Testing file downloads](#6-testing-file-downloads)
13
+ 7. [Testing webhooks](#7-testing-webhooks)
14
+ 8. [Testing error paths](#8-testing-error-paths)
15
+ 9. [Vitest / Jest patterns](#9-vitest--jest-patterns)
16
+
17
+ ## 1. The testing subpath
18
+
19
+ Everything ships from `@xbibzlibrary/telebibz/testing`:
20
+
21
+ ```ts
22
+ import { MockTransport, createTestBot, createMockUpdate, createMockCallbackUpdate, createMockContext } from "@xbibzlibrary/telebibz/testing";
23
+ ```
24
+
25
+ - `createTestBot()` → `{ bot, transport }` — a `Bot` wired to a `MockTransport`, branding off.
26
+ - `createMockUpdate(overrides?)` → a plain text `/start` message update.
27
+ - `createMockCallbackUpdate(overrides?)` → a callback-query update.
28
+ - `createMockContext(bot, update?)` → a `Context` without routing anything.
29
+
30
+ ## 2. MockTransport
31
+
32
+ `MockTransport` records every outgoing request and answers from configured responses:
33
+
34
+ ```ts
35
+ const { bot, transport } = createTestBot();
36
+
37
+ transport.respond("getMe", { ok: true, result: { id: 99, is_bot: true, first_name: "TestBot", username: "test_bot" } });
38
+ transport.respond("sendMessage", { ok: true, result: { message_id: 1, date: 0, chat: { id: 1, type: "private" } } });
39
+ transport.respond("getFile", (payload) => ({ // dynamic answers
40
+ ok: true,
41
+ result: { file_id: (payload as { file_id: string }).file_id, file_unique_id: "u", file_path: "documents/a.pdf" },
42
+ }));
43
+ ```
44
+
45
+ Unconfigured methods answer `{ ok: true, result: true }` — enough for calls whose result you ignore.
46
+
47
+ It also implements the download members:
48
+
49
+ ```ts
50
+ transport.downloadBytes = new TextEncoder().encode("file-content");
51
+ transport.downloads; // every file_path passed to download(), in order
52
+ transport.fileUrl("a.pdf"); // "mock://files/a.pdf"
53
+ ```
54
+
55
+ ## 3. Driving updates through the bot
56
+
57
+ Register handlers, then feed updates — no network, no token:
58
+
59
+ ```ts
60
+ bot.on("message", async (ctx) => { await ctx.reply("hi"); });
61
+ await bot.init(); // exactly one getMe, like production
62
+
63
+ await bot.handleUpdate(createMockUpdate({ message: { ...createMockUpdate().message!, text: "hello" } }));
64
+ await bot.handleUpdates([updateA, updateB, updateC]); // whole batch, parallel across chats
65
+ ```
66
+
67
+ To simulate several chats, vary the chat id:
68
+
69
+ ```ts
70
+ function textUpdate(updateId: number, chatId: number, text: string) {
71
+ const base = createMockUpdate();
72
+ return { ...base, update_id: updateId, message: { ...base.message!, chat: { id: chatId, type: "private" as const }, text } };
73
+ }
74
+ ```
75
+
76
+ ## 4. Asserting outgoing calls
77
+
78
+ Every request is recorded in `transport.calls` with its method and payload:
79
+
80
+ ```ts
81
+ const replies = transport.calls
82
+ .filter((call) => call.method === "sendMessage")
83
+ .map((call) => (call.payload as { text?: string }).text);
84
+
85
+ expect(replies).toEqual(["What is your name?", "How old are you?"]);
86
+
87
+ // Callback keyboards:
88
+ const markup = (transport.calls.at(-1)?.payload as { reply_markup?: { inline_keyboard: unknown[][] } }).reply_markup;
89
+ expect(markup?.inline_keyboard).toHaveLength(2);
90
+ ```
91
+
92
+ ## 5. Testing wizards end to end
93
+
94
+ Feed the conversation one message at a time and assert the replies the user would see:
95
+
96
+ ```ts
97
+ import { Bot, Wizard } from "@xbibzlibrary/telebibz";
98
+
99
+ const wizard = new Wizard()
100
+ .step({ id: "ask", run: async (flow) => { flow.next(); await flow.ctx.reply("Name?"); } })
101
+ .step({ id: "save", run: async (flow) => { await flow.ctx.reply(`Hi ${flow.ctx.message?.text}!`); } });
102
+ bot.useWizard(wizard);
103
+ bot.command("start", async (ctx) => { await wizard.run(ctx); });
104
+
105
+ await bot.handleUpdate(textUpdate(1, 7, "/start"));
106
+ await bot.handleUpdate(textUpdate(2, 7, "Alice"));
107
+
108
+ expect(sentTexts()).toEqual(["Name?", "Hi Alice!"]);
109
+ ```
110
+
111
+ Two chats, two independent wizards — interleaved updates stay isolated because same-chat updates serialize:
112
+
113
+ ```ts
114
+ await bot.handleUpdate(textUpdate(1, 7, "/start"));
115
+ await bot.handleUpdate(textUpdate(2, 8, "/start"));
116
+ await bot.handleUpdate(textUpdate(3, 7, "Alice"));
117
+ await bot.handleUpdate(textUpdate(4, 8, "Bob"));
118
+ expect(sentTexts()).toEqual(["Name?", "Name?", "Hi Alice!", "Hi Bob!"]);
119
+ ```
120
+
121
+ ## 6. Testing file downloads
122
+
123
+ ```ts
124
+ transport.respond("getFile", { ok: true, result: { file_id: "F1", file_unique_id: "U1", file_path: "photos/pic.jpg" } });
125
+ transport.downloadBytes = new TextEncoder().encode("jpeg-bytes");
126
+
127
+ const file = await bot.downloadFile("F1", { destination: tmpFile });
128
+ expect(file.fileName).toBe("pic.jpg");
129
+ expect(transport.downloads).toEqual(["photos/pic.jpg"]);
130
+ expect(await readFile(tmpFile, "utf8")).toBe("jpeg-bytes");
131
+ ```
132
+
133
+ ## 7. Testing webhooks
134
+
135
+ `createWebhookHandler` accepts a real Web `Request`:
136
+
137
+ ```ts
138
+ const handler = createWebhookHandler(bot, { secretToken: "s3cret" });
139
+
140
+ const response = await handler(new Request("https://example.com/telegram", {
141
+ method: "POST",
142
+ headers: { "content-type": "application/json", "x-telegram-bot-api-secret-token": "s3cret" },
143
+ body: JSON.stringify(createMockUpdate()),
144
+ }));
145
+ expect(response.status).toBe(200);
146
+
147
+ // Wrong secret → rejected before any handler runs
148
+ const rejected = await handler(new Request("https://example.com/telegram", {
149
+ method: "POST",
150
+ headers: { "content-type": "application/json", "x-telegram-bot-api-secret-token": "wrong" },
151
+ body: JSON.stringify(createMockUpdate()),
152
+ }));
153
+ expect(rejected.status).toBe(401);
154
+ ```
155
+
156
+ ## 8. Testing error paths
157
+
158
+ ```ts
159
+ // Telegram rejects the call
160
+ transport.respond("sendMessage", { ok: false, error_code: 400, description: "Bad Request: chat not found" });
161
+ await expect(ctx.reply("hi")).rejects.toThrow(/chat not found/);
162
+
163
+ // A handler throws — assert the error boundary saw it
164
+ const errors: unknown[] = [];
165
+ bot.events.on("update:error", ({ error }) => errors.push(error));
166
+ bot.onText("boom", async () => { throw new Error("boom"); });
167
+ await bot.handleUpdate(textUpdate(1, 7, "boom"));
168
+ expect(errors).toHaveLength(1);
169
+
170
+ // handlerTimeout: hung updates reject with UpdateTimeoutError while the handler keeps running
171
+ const slow = new Bot({ token: "123456:TEST", transport, handlerTimeout: 50, branding: false, logger: { level: "silent" } });
172
+ let finished = false;
173
+ slow.on("message", async () => { await sleep(200); finished = true; });
174
+ await expect(slow.handleUpdate(createMockUpdate())).rejects.toBeInstanceOf(UpdateTimeoutError);
175
+ // later:
176
+ expect(finished).toBe(true);
177
+ ```
178
+
179
+ ## 9. Vitest / Jest patterns
180
+
181
+ **Silence the logger in tests** — `logger: { level: "silent" }` emits nothing (regression-tested):
182
+
183
+ ```ts
184
+ const bot = new Bot({ token: "123456:TEST", transport, branding: false, logger: { level: "silent" } });
185
+ ```
186
+
187
+ **Fresh transport per test** — never share state between cases:
188
+
189
+ ```ts
190
+ beforeEach(() => { ({ bot, transport } = createTestBot()); });
191
+ ```
192
+
193
+ **Fast-forwarding time** — for `Scheduler`/TTL tests use fake timers or small intervals (`1ms`); the cron parser is pure and testable without timers:
194
+
195
+ ```ts
196
+ import { nextCronOccurrence, parseCronExpression } from "@xbibzlibrary/telebibz";
197
+ expect(parseCronExpression("*/15 * * * *")).toBeDefined();
198
+ expect(nextCronOccurrence("0 9 * * 1", new Date("2026-08-29T00:00:00Z")).toISOString()).toContain("T09:00");
199
+ ```
200
+
201
+ **Run the suite**: `npm test` — E2E tests requiring a real token are skipped automatically when `TELEGRAM_BOT_TOKEN` is absent.
202
+
203
+ Bahasa Indonesia: [TESTING.id.md](TESTING.id.md) · 简体中文: [TESTING.zh-CN.md](TESTING.zh-CN.md)