@llblab/pi-telegram 0.2.7 → 0.2.9

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.
@@ -1,239 +1,19 @@
1
1
  /**
2
- * Regression tests for the Telegram replies domain
3
- * Covers preview decisions, rendered-message delivery, and plain or markdown reply sending in one suite
2
+ * Regression tests for Telegram reply delivery helpers
3
+ * Covers rendered-message transport, chunk delivery, and plain or markdown final reply sending
4
4
  */
5
5
 
6
6
  import assert from "node:assert/strict";
7
7
  import test from "node:test";
8
8
 
9
9
  import {
10
- buildTelegramPreviewFinalText,
11
- buildTelegramPreviewFlushText,
12
10
  buildTelegramReplyTransport,
13
- clearTelegramPreview,
14
11
  editTelegramRenderedMessage,
15
- finalizeTelegramMarkdownPreview,
16
- finalizeTelegramPreview,
17
- flushTelegramPreview,
18
12
  sendTelegramMarkdownReply,
19
13
  sendTelegramPlainReply,
20
14
  sendTelegramRenderedChunks,
21
- shouldUseTelegramDraftPreview,
22
15
  } from "../lib/replies.ts";
23
16
 
24
- function createPreviewRuntimeHarness(state?: {
25
- mode: "draft" | "message";
26
- draftId?: number;
27
- messageId?: number;
28
- pendingText: string;
29
- lastSentText: string;
30
- flushTimer?: ReturnType<typeof setTimeout>;
31
- }) {
32
- let previewState = state;
33
- let draftSupport: "unknown" | "supported" | "unsupported" = "unknown";
34
- let nextDraftId = 10;
35
- const events: string[] = [];
36
- return {
37
- events,
38
- getState: () => previewState,
39
- getDraftSupport: () => draftSupport,
40
- setDraftSupport: (support: "unknown" | "supported" | "unsupported") => {
41
- draftSupport = support;
42
- },
43
- deps: {
44
- getState: () => previewState,
45
- setState: (nextState: typeof previewState) => {
46
- previewState = nextState;
47
- },
48
- clearScheduledFlush: (nextState: NonNullable<typeof previewState>) => {
49
- if (!nextState.flushTimer) return;
50
- clearTimeout(nextState.flushTimer);
51
- nextState.flushTimer = undefined;
52
- events.push("clear-timer");
53
- },
54
- maxMessageLength: 5,
55
- renderPreviewText: (markdown: string) => markdown.replaceAll("*", ""),
56
- getDraftSupport: () => draftSupport,
57
- setDraftSupport: (support: "unknown" | "supported" | "unsupported") => {
58
- draftSupport = support;
59
- },
60
- allocateDraftId: () => nextDraftId++,
61
- sendDraft: async (chatId: number, draftId: number, text: string) => {
62
- events.push(`draft:${chatId}:${draftId}:${text}`);
63
- },
64
- sendMessage: async (chatId: number, text: string) => {
65
- events.push(`send:${chatId}:${text}`);
66
- return { message_id: 77 };
67
- },
68
- editMessageText: async (
69
- chatId: number,
70
- messageId: number,
71
- text: string,
72
- ) => {
73
- events.push(`edit:${chatId}:${messageId}:${text}`);
74
- },
75
- renderTelegramMessage: (text: string, options?: { mode?: string }) => [
76
- { text: `${options?.mode ?? "plain"}:${text}` },
77
- ],
78
- sendRenderedChunks: async (
79
- chatId: number,
80
- chunks: Array<{ text: string }>,
81
- ) => {
82
- events.push(
83
- `render-send:${chatId}:${chunks.map((chunk) => chunk.text).join("|")}`,
84
- );
85
- return 88;
86
- },
87
- editRenderedMessage: async (
88
- chatId: number,
89
- messageId: number,
90
- chunks: Array<{ text: string }>,
91
- ) => {
92
- events.push(
93
- `render-edit:${chatId}:${messageId}:${chunks.map((chunk) => chunk.text).join("|")}`,
94
- );
95
- return messageId;
96
- },
97
- },
98
- };
99
- }
100
-
101
- test("Reply previews build flush text only when the preview changed", () => {
102
- assert.equal(
103
- buildTelegramPreviewFlushText({
104
- state: {
105
- mode: "draft",
106
- pendingText: "**hello**",
107
- lastSentText: "",
108
- },
109
- maxMessageLength: 4096,
110
- renderPreviewText: (markdown) => markdown.replaceAll("*", ""),
111
- }),
112
- "hello",
113
- );
114
- assert.equal(
115
- buildTelegramPreviewFlushText({
116
- state: {
117
- mode: "draft",
118
- pendingText: "**hello**",
119
- lastSentText: "hello",
120
- },
121
- maxMessageLength: 4096,
122
- renderPreviewText: (markdown) => markdown.replaceAll("*", ""),
123
- }),
124
- undefined,
125
- );
126
- });
127
-
128
- test("Reply previews truncate long flush text and compute final text fallback", () => {
129
- assert.equal(
130
- buildTelegramPreviewFlushText({
131
- state: {
132
- mode: "message",
133
- pendingText: "abcdef",
134
- lastSentText: "",
135
- },
136
- maxMessageLength: 3,
137
- renderPreviewText: (markdown) => markdown,
138
- }),
139
- "abc",
140
- );
141
- assert.equal(
142
- buildTelegramPreviewFinalText({
143
- mode: "message",
144
- pendingText: " ",
145
- lastSentText: "saved",
146
- }),
147
- "saved",
148
- );
149
- assert.equal(
150
- buildTelegramPreviewFinalText({
151
- mode: "message",
152
- pendingText: " ",
153
- lastSentText: " ",
154
- }),
155
- undefined,
156
- );
157
- });
158
-
159
- test("Reply previews use drafts unless support is explicitly disabled", () => {
160
- assert.equal(
161
- shouldUseTelegramDraftPreview({ draftSupport: "unknown" }),
162
- true,
163
- );
164
- assert.equal(
165
- shouldUseTelegramDraftPreview({ draftSupport: "supported" }),
166
- true,
167
- );
168
- assert.equal(
169
- shouldUseTelegramDraftPreview({ draftSupport: "unsupported" }),
170
- false,
171
- );
172
- });
173
-
174
- test("Reply preview runtime prefers draft updates and can clear draft previews", async () => {
175
- const harness = createPreviewRuntimeHarness({
176
- mode: "draft",
177
- pendingText: "**hello**",
178
- lastSentText: "",
179
- flushTimer: setTimeout(() => {}, 1000),
180
- });
181
- await flushTelegramPreview(7, harness.deps);
182
- assert.deepEqual(harness.events, ["draft:7:10:hello"]);
183
- assert.equal(harness.getState()?.mode, "draft");
184
- assert.equal(harness.getState()?.draftId, 10);
185
- assert.equal(harness.getState()?.lastSentText, "hello");
186
- assert.equal(harness.getDraftSupport(), "supported");
187
- await clearTelegramPreview(7, harness.deps);
188
- assert.deepEqual(harness.events, ["draft:7:10:hello", "draft:7:10:"]);
189
- assert.equal(harness.getState(), undefined);
190
- });
191
-
192
- test("Reply preview runtime falls back to editable messages when draft delivery fails", async () => {
193
- const harness = createPreviewRuntimeHarness({
194
- mode: "draft",
195
- pendingText: "abcdef",
196
- lastSentText: "",
197
- });
198
- harness.deps.sendDraft = async () => {
199
- throw new Error("draft unsupported");
200
- };
201
- await flushTelegramPreview(7, harness.deps);
202
- assert.deepEqual(harness.events, ["send:7:abcde"]);
203
- assert.equal(harness.getState()?.mode, "message");
204
- assert.equal(harness.getState()?.messageId, 77);
205
- assert.equal(harness.getDraftSupport(), "unsupported");
206
- });
207
-
208
- test("Reply preview runtime finalizes plain and markdown previews", async () => {
209
- const plainHarness = createPreviewRuntimeHarness({
210
- mode: "message",
211
- messageId: 44,
212
- pendingText: "done",
213
- lastSentText: "",
214
- });
215
- plainHarness.setDraftSupport("unsupported");
216
- assert.equal(await finalizeTelegramPreview(7, plainHarness.deps), true);
217
- assert.deepEqual(plainHarness.events, ["edit:7:44:done"]);
218
- assert.equal(plainHarness.getState(), undefined);
219
- const markdownHarness = createPreviewRuntimeHarness({
220
- mode: "message",
221
- messageId: 55,
222
- pendingText: "done",
223
- lastSentText: "",
224
- });
225
- markdownHarness.setDraftSupport("unsupported");
226
- assert.equal(
227
- await finalizeTelegramMarkdownPreview(7, "**done**", markdownHarness.deps),
228
- true,
229
- );
230
- assert.deepEqual(markdownHarness.events, [
231
- "edit:7:55:done",
232
- "render-edit:7:55:markdown:**done**",
233
- ]);
234
- assert.equal(markdownHarness.getState(), undefined);
235
- });
236
-
237
17
  test("Reply transport forwards send and edit operations through delivery helpers", async () => {
238
18
  const events: string[] = [];
239
19
  const transport = buildTelegramReplyTransport({
@@ -30,28 +30,20 @@ test("Update helpers normalize emoji reactions and collect emoji-only entries",
30
30
  assert.deepEqual([...emojis], ["👍", "👎"]);
31
31
  });
32
32
 
33
- test("Update helpers extract deleted message ids from Telegram update variants", () => {
33
+ test("Update helpers extract deleted business-message ids only from Bot API shapes", () => {
34
34
  assert.deepEqual(
35
35
  extractDeletedTelegramMessageIds({
36
- _: "other",
37
36
  deleted_business_messages: { message_ids: [1, 2] },
38
37
  }),
39
38
  [1, 2],
40
39
  );
41
40
  assert.deepEqual(
42
41
  extractDeletedTelegramMessageIds({
43
- _: "updateDeleteMessages",
44
- messages: [3, 4],
45
- }),
46
- [3, 4],
47
- );
48
- assert.deepEqual(
49
- extractDeletedTelegramMessageIds({
50
- _: "updateDeleteMessages",
51
- messages: [3, "bad"],
42
+ deleted_business_messages: { message_ids: [3, "bad"] },
52
43
  }),
53
44
  [],
54
45
  );
46
+ assert.deepEqual(extractDeletedTelegramMessageIds({}), []);
55
47
  });
56
48
 
57
49
  test("Update routing classifies authorization state for pair, allow, and deny", () => {
@@ -101,11 +93,10 @@ test("Update routing extracts private human messages from message or edited_mess
101
93
  assert.ok(directMessage);
102
94
  });
103
95
 
104
- test("Update flow prioritizes deleted-message handling over other update kinds", () => {
96
+ test("Update flow prioritizes deleted business-message handling over other update kinds", () => {
105
97
  const action = buildTelegramUpdateFlowAction(
106
98
  {
107
- _: "updateDeleteMessages",
108
- messages: [1, 2],
99
+ deleted_business_messages: { message_ids: [1, 2] },
109
100
  message_reaction: {
110
101
  chat: { type: "private" },
111
102
  user: { id: 1, is_bot: false },
@@ -119,7 +110,6 @@ test("Update flow prioritizes deleted-message handling over other update kinds",
119
110
  test("Update flow returns authorized callback and message actions", () => {
120
111
  const callbackAction = buildTelegramUpdateFlowAction(
121
112
  {
122
- _: "other",
123
113
  callback_query: {
124
114
  from: { id: 7, is_bot: false },
125
115
  message: { chat: { type: "private" } },
@@ -129,11 +119,12 @@ test("Update flow returns authorized callback and message actions", () => {
129
119
  );
130
120
  assert.equal(callbackAction.kind, "callback");
131
121
  assert.deepEqual(
132
- callbackAction.kind === "callback" ? callbackAction.authorization : undefined,
122
+ callbackAction.kind === "callback"
123
+ ? callbackAction.authorization
124
+ : undefined,
133
125
  { kind: "allow" },
134
126
  );
135
127
  const messageAction = buildTelegramUpdateFlowAction({
136
- _: "other",
137
128
  message: {
138
129
  chat: { type: "private" },
139
130
  from: { id: 9, is_bot: false },
@@ -148,7 +139,6 @@ test("Update flow returns authorized callback and message actions", () => {
148
139
 
149
140
  test("Update flow ignores unauthorized transport shapes and preserves reaction events", () => {
150
141
  const reactionAction = buildTelegramUpdateFlowAction({
151
- _: "other",
152
142
  message_reaction: {
153
143
  chat: { type: "private" },
154
144
  user: { id: 1, is_bot: false },
@@ -156,7 +146,6 @@ test("Update flow ignores unauthorized transport shapes and preserves reaction e
156
146
  });
157
147
  assert.equal(reactionAction.kind, "reaction");
158
148
  const ignored = buildTelegramUpdateFlowAction({
159
- _: "other",
160
149
  callback_query: {
161
150
  from: { id: 1, is_bot: true },
162
151
  message: { chat: { type: "private" } },
@@ -218,7 +207,6 @@ test("Update execution plan preserves deleted and reaction actions", () => {
218
207
  test("Update execution plan can be built directly from updates", () => {
219
208
  const plan = buildTelegramUpdateExecutionPlanFromUpdate(
220
209
  {
221
- _: "other",
222
210
  callback_query: {
223
211
  from: { id: 4, is_bot: false },
224
212
  message: { chat: { type: "private" } },
@@ -237,10 +225,10 @@ test("Update runtime executes delete and reaction plans through the right side e
237
225
  {
238
226
  ctx: {} as never,
239
227
  removePendingMediaGroupMessages: (ids) => {
240
- events.push(`media:${ids.join(',')}`);
228
+ events.push(`media:${ids.join(",")}`);
241
229
  },
242
230
  removeQueuedTelegramTurnsByMessageIds: (ids) => {
243
- events.push(`queue:${ids.join(',')}`);
231
+ events.push(`queue:${ids.join(",")}`);
244
232
  return ids.length;
245
233
  },
246
234
  handleAuthorizedTelegramReactionUpdate: async () => {
@@ -260,7 +248,6 @@ test("Update runtime can execute directly from raw updates", async () => {
260
248
  const events: string[] = [];
261
249
  await executeTelegramUpdate(
262
250
  {
263
- _: "other",
264
251
  message: {
265
252
  chat: { id: 10, type: "private" },
266
253
  message_id: 20,
@@ -288,7 +275,11 @@ test("Update runtime can execute directly from raw updates", async () => {
288
275
  },
289
276
  },
290
277
  );
291
- assert.deepEqual(events, ["pair", "reply:Telegram bridge paired with this account.", "message"]);
278
+ assert.deepEqual(events, [
279
+ "pair",
280
+ "reply:Telegram bridge paired with this account.",
281
+ "message",
282
+ ]);
292
283
  });
293
284
 
294
285
  test("Update runtime handles callback deny and message pair flows", async () => {