@llblab/pi-telegram 0.2.0

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,366 @@
1
+ /**
2
+ * Regression tests for the Telegram updates domain
3
+ * Covers extraction, authorization, flow classification, execution planning, and runtime execution in one suite
4
+ */
5
+
6
+ import test from "node:test";
7
+ import assert from "node:assert/strict";
8
+
9
+ import {
10
+ buildTelegramUpdateExecutionPlan,
11
+ buildTelegramUpdateExecutionPlanFromUpdate,
12
+ buildTelegramUpdateFlowAction,
13
+ collectTelegramReactionEmojis,
14
+ executeTelegramUpdate,
15
+ executeTelegramUpdatePlan,
16
+ extractDeletedTelegramMessageIds,
17
+ getAuthorizedTelegramCallbackQuery,
18
+ getAuthorizedTelegramMessage,
19
+ getTelegramAuthorizationState,
20
+ normalizeTelegramReactionEmoji,
21
+ } from "../lib/updates.ts";
22
+
23
+ test("Update helpers normalize emoji reactions and collect emoji-only entries", () => {
24
+ assert.equal(normalizeTelegramReactionEmoji("👍️"), "👍");
25
+ const emojis = collectTelegramReactionEmojis([
26
+ { type: "emoji", emoji: "👍️" },
27
+ { type: "emoji", emoji: "👎" },
28
+ { type: "custom_emoji" },
29
+ ]);
30
+ assert.deepEqual([...emojis], ["👍", "👎"]);
31
+ });
32
+
33
+ test("Update helpers extract deleted message ids from Telegram update variants", () => {
34
+ assert.deepEqual(
35
+ extractDeletedTelegramMessageIds({
36
+ _: "other",
37
+ deleted_business_messages: { message_ids: [1, 2] },
38
+ }),
39
+ [1, 2],
40
+ );
41
+ assert.deepEqual(
42
+ extractDeletedTelegramMessageIds({
43
+ _: "updateDeleteMessages",
44
+ messages: [3, 4],
45
+ }),
46
+ [3, 4],
47
+ );
48
+ assert.deepEqual(
49
+ extractDeletedTelegramMessageIds({
50
+ _: "updateDeleteMessages",
51
+ messages: [3, "bad"],
52
+ }),
53
+ [],
54
+ );
55
+ });
56
+
57
+ test("Update routing classifies authorization state for pair, allow, and deny", () => {
58
+ assert.deepEqual(getTelegramAuthorizationState(10), {
59
+ kind: "pair",
60
+ userId: 10,
61
+ });
62
+ assert.deepEqual(getTelegramAuthorizationState(10, 10), { kind: "allow" });
63
+ assert.deepEqual(getTelegramAuthorizationState(10, 11), { kind: "deny" });
64
+ });
65
+
66
+ test("Update routing extracts only private human callback queries", () => {
67
+ assert.equal(
68
+ getAuthorizedTelegramCallbackQuery({
69
+ callback_query: {
70
+ from: { id: 1, is_bot: true },
71
+ message: { chat: { type: "private" } },
72
+ },
73
+ }),
74
+ undefined,
75
+ );
76
+ const query = getAuthorizedTelegramCallbackQuery({
77
+ callback_query: {
78
+ from: { id: 1, is_bot: false },
79
+ message: { chat: { type: "private" } },
80
+ },
81
+ });
82
+ assert.ok(query);
83
+ });
84
+
85
+ test("Update routing extracts private human messages from message or edited_message", () => {
86
+ assert.equal(
87
+ getAuthorizedTelegramMessage({
88
+ message: {
89
+ chat: { type: "group" },
90
+ from: { id: 1, is_bot: false },
91
+ },
92
+ }),
93
+ undefined,
94
+ );
95
+ const directMessage = getAuthorizedTelegramMessage({
96
+ edited_message: {
97
+ chat: { type: "private" },
98
+ from: { id: 1, is_bot: false },
99
+ },
100
+ });
101
+ assert.ok(directMessage);
102
+ });
103
+
104
+ test("Update flow prioritizes deleted-message handling over other update kinds", () => {
105
+ const action = buildTelegramUpdateFlowAction(
106
+ {
107
+ _: "updateDeleteMessages",
108
+ messages: [1, 2],
109
+ message_reaction: {
110
+ chat: { type: "private" },
111
+ user: { id: 1, is_bot: false },
112
+ },
113
+ },
114
+ 1,
115
+ );
116
+ assert.deepEqual(action, { kind: "deleted", messageIds: [1, 2] });
117
+ });
118
+
119
+ test("Update flow returns authorized callback and message actions", () => {
120
+ const callbackAction = buildTelegramUpdateFlowAction(
121
+ {
122
+ _: "other",
123
+ callback_query: {
124
+ from: { id: 7, is_bot: false },
125
+ message: { chat: { type: "private" } },
126
+ },
127
+ },
128
+ 7,
129
+ );
130
+ assert.equal(callbackAction.kind, "callback");
131
+ assert.deepEqual(
132
+ callbackAction.kind === "callback" ? callbackAction.authorization : undefined,
133
+ { kind: "allow" },
134
+ );
135
+ const messageAction = buildTelegramUpdateFlowAction({
136
+ _: "other",
137
+ message: {
138
+ chat: { type: "private" },
139
+ from: { id: 9, is_bot: false },
140
+ },
141
+ });
142
+ assert.equal(messageAction.kind, "message");
143
+ assert.deepEqual(
144
+ messageAction.kind === "message" ? messageAction.authorization : undefined,
145
+ { kind: "pair", userId: 9 },
146
+ );
147
+ });
148
+
149
+ test("Update flow ignores unauthorized transport shapes and preserves reaction events", () => {
150
+ const reactionAction = buildTelegramUpdateFlowAction({
151
+ _: "other",
152
+ message_reaction: {
153
+ chat: { type: "private" },
154
+ user: { id: 1, is_bot: false },
155
+ },
156
+ });
157
+ assert.equal(reactionAction.kind, "reaction");
158
+ const ignored = buildTelegramUpdateFlowAction({
159
+ _: "other",
160
+ callback_query: {
161
+ from: { id: 1, is_bot: true },
162
+ message: { chat: { type: "private" } },
163
+ },
164
+ });
165
+ assert.deepEqual(ignored, { kind: "ignore" });
166
+ });
167
+
168
+ test("Update execution plan maps callback and message authorization to side-effect flags", () => {
169
+ const callbackPlan = buildTelegramUpdateExecutionPlan({
170
+ kind: "callback",
171
+ query: {
172
+ from: { id: 1, is_bot: false },
173
+ message: { chat: { type: "private" } },
174
+ },
175
+ authorization: { kind: "deny" },
176
+ });
177
+ assert.deepEqual(callbackPlan, {
178
+ kind: "callback",
179
+ query: {
180
+ from: { id: 1, is_bot: false },
181
+ message: { chat: { type: "private" } },
182
+ },
183
+ shouldPair: false,
184
+ shouldDeny: true,
185
+ });
186
+ const messagePlan = buildTelegramUpdateExecutionPlan({
187
+ kind: "message",
188
+ message: {
189
+ chat: { type: "private" },
190
+ from: { id: 2, is_bot: false },
191
+ },
192
+ authorization: { kind: "pair", userId: 2 },
193
+ });
194
+ assert.equal(messagePlan.kind, "message");
195
+ assert.equal(messagePlan.shouldPair, true);
196
+ assert.equal(messagePlan.shouldNotifyPaired, true);
197
+ assert.equal(messagePlan.shouldDeny, false);
198
+ });
199
+
200
+ test("Update execution plan preserves deleted and reaction actions", () => {
201
+ assert.deepEqual(
202
+ buildTelegramUpdateExecutionPlan({ kind: "deleted", messageIds: [1, 2] }),
203
+ { kind: "deleted", messageIds: [1, 2] },
204
+ );
205
+ const reactionUpdate = {
206
+ chat: { type: "private" },
207
+ user: { id: 1, is_bot: false },
208
+ };
209
+ assert.deepEqual(
210
+ buildTelegramUpdateExecutionPlan({
211
+ kind: "reaction",
212
+ reactionUpdate,
213
+ }),
214
+ { kind: "reaction", reactionUpdate },
215
+ );
216
+ });
217
+
218
+ test("Update execution plan can be built directly from updates", () => {
219
+ const plan = buildTelegramUpdateExecutionPlanFromUpdate(
220
+ {
221
+ _: "other",
222
+ callback_query: {
223
+ from: { id: 4, is_bot: false },
224
+ message: { chat: { type: "private" } },
225
+ },
226
+ },
227
+ 5,
228
+ );
229
+ assert.equal(plan.kind, "callback");
230
+ assert.equal(plan.kind === "callback" ? plan.shouldDeny : false, true);
231
+ });
232
+
233
+ test("Update runtime executes delete and reaction plans through the right side effects", async () => {
234
+ const events: string[] = [];
235
+ await executeTelegramUpdatePlan(
236
+ { kind: "deleted", messageIds: [1, 2] },
237
+ {
238
+ ctx: {} as never,
239
+ removePendingMediaGroupMessages: (ids) => {
240
+ events.push(`media:${ids.join(',')}`);
241
+ },
242
+ removeQueuedTelegramTurnsByMessageIds: (ids) => {
243
+ events.push(`queue:${ids.join(',')}`);
244
+ return ids.length;
245
+ },
246
+ handleAuthorizedTelegramReactionUpdate: async () => {
247
+ events.push("reaction");
248
+ },
249
+ pairTelegramUserIfNeeded: async () => false,
250
+ answerCallbackQuery: async () => {},
251
+ handleAuthorizedTelegramCallbackQuery: async () => {},
252
+ sendTextReply: async () => undefined,
253
+ handleAuthorizedTelegramMessage: async () => {},
254
+ },
255
+ );
256
+ assert.deepEqual(events, ["media:1,2", "queue:1,2"]);
257
+ });
258
+
259
+ test("Update runtime can execute directly from raw updates", async () => {
260
+ const events: string[] = [];
261
+ await executeTelegramUpdate(
262
+ {
263
+ _: "other",
264
+ message: {
265
+ chat: { id: 10, type: "private" },
266
+ message_id: 20,
267
+ from: { id: 7, is_bot: false },
268
+ },
269
+ },
270
+ undefined,
271
+ {
272
+ ctx: {} as never,
273
+ removePendingMediaGroupMessages: () => {},
274
+ removeQueuedTelegramTurnsByMessageIds: () => 0,
275
+ handleAuthorizedTelegramReactionUpdate: async () => {},
276
+ pairTelegramUserIfNeeded: async () => {
277
+ events.push("pair");
278
+ return true;
279
+ },
280
+ answerCallbackQuery: async () => {},
281
+ handleAuthorizedTelegramCallbackQuery: async () => {},
282
+ sendTextReply: async (_chatId, _replyToMessageId, text) => {
283
+ events.push(`reply:${text}`);
284
+ return undefined;
285
+ },
286
+ handleAuthorizedTelegramMessage: async () => {
287
+ events.push("message");
288
+ },
289
+ },
290
+ );
291
+ assert.deepEqual(events, ["pair", "reply:Telegram bridge paired with this account.", "message"]);
292
+ });
293
+
294
+ test("Update runtime handles callback deny and message pair flows", async () => {
295
+ const events: string[] = [];
296
+ await executeTelegramUpdatePlan(
297
+ {
298
+ kind: "callback",
299
+ query: {
300
+ id: "cb",
301
+ from: { id: 1, is_bot: false },
302
+ message: { chat: { type: "private" } },
303
+ },
304
+ shouldPair: true,
305
+ shouldDeny: true,
306
+ },
307
+ {
308
+ ctx: {} as never,
309
+ removePendingMediaGroupMessages: () => {},
310
+ removeQueuedTelegramTurnsByMessageIds: () => 0,
311
+ handleAuthorizedTelegramReactionUpdate: async () => {},
312
+ pairTelegramUserIfNeeded: async (userId) => {
313
+ events.push(`pair:${userId}`);
314
+ return true;
315
+ },
316
+ answerCallbackQuery: async (id, text) => {
317
+ events.push(`answer:${id}:${text}`);
318
+ },
319
+ handleAuthorizedTelegramCallbackQuery: async () => {
320
+ events.push("callback");
321
+ },
322
+ sendTextReply: async (chatId, replyToMessageId, text) => {
323
+ events.push(`reply:${chatId}:${replyToMessageId}:${text}`);
324
+ return undefined;
325
+ },
326
+ handleAuthorizedTelegramMessage: async () => {
327
+ events.push("message");
328
+ },
329
+ },
330
+ );
331
+ await executeTelegramUpdatePlan(
332
+ {
333
+ kind: "message",
334
+ message: {
335
+ chat: { id: 7, type: "private" },
336
+ from: { id: 2, is_bot: false },
337
+ message_id: 9,
338
+ },
339
+ shouldPair: true,
340
+ shouldNotifyPaired: true,
341
+ shouldDeny: false,
342
+ },
343
+ {
344
+ ctx: {} as never,
345
+ removePendingMediaGroupMessages: () => {},
346
+ removeQueuedTelegramTurnsByMessageIds: () => 0,
347
+ handleAuthorizedTelegramReactionUpdate: async () => {},
348
+ pairTelegramUserIfNeeded: async () => true,
349
+ answerCallbackQuery: async () => {},
350
+ handleAuthorizedTelegramCallbackQuery: async () => {},
351
+ sendTextReply: async (chatId, replyToMessageId, text) => {
352
+ events.push(`reply:${chatId}:${replyToMessageId}:${text}`);
353
+ return undefined;
354
+ },
355
+ handleAuthorizedTelegramMessage: async () => {
356
+ events.push("message");
357
+ },
358
+ },
359
+ );
360
+ assert.deepEqual(events, [
361
+ "pair:1",
362
+ "answer:cb:This bot is not authorized for your account.",
363
+ "reply:7:9:Telegram bridge paired with this account.",
364
+ "message",
365
+ ]);
366
+ });