@llblab/pi-telegram 0.2.8 → 0.2.10

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({
@@ -11,6 +11,7 @@ import {
11
11
  buildTelegramTurnPrompt,
12
12
  formatTelegramTurnStatusSummary,
13
13
  truncateTelegramQueueSummary,
14
+ updateTelegramPromptTurnText,
14
15
  } from "../lib/turns.ts";
15
16
 
16
17
  test("Turn helpers truncate queue summaries predictably", () => {
@@ -69,6 +70,120 @@ test("Turn helpers summarize text and attachment-only turns", () => {
69
70
  );
70
71
  });
71
72
 
73
+ test("Turn helpers update queued prompt text for edited Telegram messages", () => {
74
+ const turn = {
75
+ kind: "prompt" as const,
76
+ chatId: 99,
77
+ replyToMessageId: 10,
78
+ sourceMessageIds: [10],
79
+ queueOrder: 1,
80
+ queueLane: "default" as const,
81
+ laneOrder: 1,
82
+ queuedAttachments: [],
83
+ content: [{ type: "text" as const, text: "[telegram] old" }],
84
+ historyText: "old",
85
+ statusSummary: "old",
86
+ };
87
+ const updated = updateTelegramPromptTurnText({
88
+ turn,
89
+ telegramPrefix: "[telegram]",
90
+ rawText: "new edited message",
91
+ });
92
+ assert.equal(updated.content[0]?.type, "text");
93
+ assert.equal(
94
+ (updated.content[0] as { type: "text"; text: string }).text,
95
+ "[telegram] new edited message",
96
+ );
97
+ assert.equal(updated.historyText, "new edited message");
98
+ assert.equal(updated.statusSummary, "new edited message");
99
+ assert.notEqual(updated, turn);
100
+ });
101
+
102
+ test("Turn helpers preserve queued prompt attachments when captions are edited", () => {
103
+ const turn = {
104
+ kind: "prompt" as const,
105
+ chatId: 99,
106
+ replyToMessageId: 10,
107
+ sourceMessageIds: [10],
108
+ queueOrder: 1,
109
+ queueLane: "default" as const,
110
+ laneOrder: 1,
111
+ queuedAttachments: [],
112
+ content: [
113
+ {
114
+ type: "text" as const,
115
+ text:
116
+ "[telegram] old caption\n\n" +
117
+ "Telegram attachments were saved locally:\n" +
118
+ "- /tmp/demo.png\n" +
119
+ "- /tmp/report.txt",
120
+ },
121
+ { type: "image" as const, data: "abc", mimeType: "image/png" },
122
+ ],
123
+ historyText: "old caption\nAttachments:\n- /tmp/demo.png\n- /tmp/report.txt",
124
+ statusSummary: "old caption",
125
+ };
126
+ const updated = updateTelegramPromptTurnText({
127
+ turn,
128
+ telegramPrefix: "[telegram]",
129
+ rawText: "new caption",
130
+ });
131
+ assert.equal(
132
+ (updated.content[0] as { type: "text"; text: string }).text,
133
+ "[telegram] new caption\n\n" +
134
+ "Telegram attachments were saved locally:\n" +
135
+ "- /tmp/demo.png\n" +
136
+ "- /tmp/report.txt",
137
+ );
138
+ assert.deepEqual(updated.content[1], turn.content[1]);
139
+ assert.equal(
140
+ updated.historyText,
141
+ "new caption\nAttachments:\n- /tmp/demo.png\n- /tmp/report.txt",
142
+ );
143
+ assert.equal(updated.statusSummary, "new caption");
144
+ });
145
+
146
+ test("Turn helpers preserve abort-history prompt context when queued turns are edited", () => {
147
+ const turn = {
148
+ kind: "prompt" as const,
149
+ chatId: 99,
150
+ replyToMessageId: 10,
151
+ sourceMessageIds: [10],
152
+ queueOrder: 1,
153
+ queueLane: "default" as const,
154
+ laneOrder: 1,
155
+ queuedAttachments: [],
156
+ content: [
157
+ {
158
+ type: "text" as const,
159
+ text:
160
+ "[telegram]\n\n" +
161
+ "Earlier Telegram messages arrived after an aborted turn. " +
162
+ "Treat them as prior user messages, in order:\n\n" +
163
+ "1. older Current Telegram message: quote\n\n" +
164
+ "Current Telegram message:\nold current",
165
+ },
166
+ ],
167
+ historyText: "old current",
168
+ statusSummary: "old current",
169
+ };
170
+ const updated = updateTelegramPromptTurnText({
171
+ turn,
172
+ telegramPrefix: "[telegram]",
173
+ rawText: "new current",
174
+ });
175
+ assert.equal(
176
+ (updated.content[0] as { type: "text"; text: string }).text,
177
+ "[telegram]\n\n" +
178
+ "Earlier Telegram messages arrived after an aborted turn. " +
179
+ "Treat them as prior user messages, in order:\n\n" +
180
+ "1. older Current Telegram message: quote\n\n" +
181
+ "Current Telegram message:\nnew current",
182
+ );
183
+ assert.equal(updated.historyText, "new current");
184
+ assert.equal(updated.statusSummary, "new current");
185
+ });
186
+
72
187
  test("Turn helpers assemble prompt turns with text, ids, history, and image payloads", async () => {
73
188
  const turn = await buildTelegramPromptTurn({
74
189
  telegramPrefix: "[telegram]",
@@ -15,6 +15,7 @@ import {
15
15
  executeTelegramUpdatePlan,
16
16
  extractDeletedTelegramMessageIds,
17
17
  getAuthorizedTelegramCallbackQuery,
18
+ getAuthorizedTelegramEditedMessage,
18
19
  getAuthorizedTelegramMessage,
19
20
  getTelegramAuthorizationState,
20
21
  normalizeTelegramReactionEmoji,
@@ -74,7 +75,7 @@ test("Update routing extracts only private human callback queries", () => {
74
75
  assert.ok(query);
75
76
  });
76
77
 
77
- test("Update routing extracts private human messages from message or edited_message", () => {
78
+ test("Update routing extracts private human messages and edited messages separately", () => {
78
79
  assert.equal(
79
80
  getAuthorizedTelegramMessage({
80
81
  message: {
@@ -85,12 +86,19 @@ test("Update routing extracts private human messages from message or edited_mess
85
86
  undefined,
86
87
  );
87
88
  const directMessage = getAuthorizedTelegramMessage({
88
- edited_message: {
89
+ message: {
89
90
  chat: { type: "private" },
90
91
  from: { id: 1, is_bot: false },
91
92
  },
92
93
  });
93
94
  assert.ok(directMessage);
95
+ const editedMessage = getAuthorizedTelegramEditedMessage({
96
+ edited_message: {
97
+ chat: { type: "private" },
98
+ from: { id: 1, is_bot: false },
99
+ },
100
+ });
101
+ assert.ok(editedMessage);
94
102
  });
95
103
 
96
104
  test("Update flow prioritizes deleted business-message handling over other update kinds", () => {
@@ -107,7 +115,7 @@ test("Update flow prioritizes deleted business-message handling over other updat
107
115
  assert.deepEqual(action, { kind: "deleted", messageIds: [1, 2] });
108
116
  });
109
117
 
110
- test("Update flow returns authorized callback and message actions", () => {
118
+ test("Update flow returns authorized callback, message, and edit actions", () => {
111
119
  const callbackAction = buildTelegramUpdateFlowAction(
112
120
  {
113
121
  callback_query: {
@@ -119,7 +127,9 @@ test("Update flow returns authorized callback and message actions", () => {
119
127
  );
120
128
  assert.equal(callbackAction.kind, "callback");
121
129
  assert.deepEqual(
122
- callbackAction.kind === "callback" ? callbackAction.authorization : undefined,
130
+ callbackAction.kind === "callback"
131
+ ? callbackAction.authorization
132
+ : undefined,
123
133
  { kind: "allow" },
124
134
  );
125
135
  const messageAction = buildTelegramUpdateFlowAction({
@@ -133,6 +143,16 @@ test("Update flow returns authorized callback and message actions", () => {
133
143
  messageAction.kind === "message" ? messageAction.authorization : undefined,
134
144
  { kind: "pair", userId: 9 },
135
145
  );
146
+ const editAction = buildTelegramUpdateFlowAction(
147
+ {
148
+ edited_message: {
149
+ chat: { type: "private" },
150
+ from: { id: 9, is_bot: false },
151
+ },
152
+ },
153
+ 9,
154
+ );
155
+ assert.equal(editAction.kind, "edited-message");
136
156
  });
137
157
 
138
158
  test("Update flow ignores unauthorized transport shapes and preserves reaction events", () => {
@@ -223,10 +243,10 @@ test("Update runtime executes delete and reaction plans through the right side e
223
243
  {
224
244
  ctx: {} as never,
225
245
  removePendingMediaGroupMessages: (ids) => {
226
- events.push(`media:${ids.join(',')}`);
246
+ events.push(`media:${ids.join(",")}`);
227
247
  },
228
248
  removeQueuedTelegramTurnsByMessageIds: (ids) => {
229
- events.push(`queue:${ids.join(',')}`);
249
+ events.push(`queue:${ids.join(",")}`);
230
250
  return ids.length;
231
251
  },
232
252
  handleAuthorizedTelegramReactionUpdate: async () => {
@@ -237,6 +257,7 @@ test("Update runtime executes delete and reaction plans through the right side e
237
257
  handleAuthorizedTelegramCallbackQuery: async () => {},
238
258
  sendTextReply: async () => undefined,
239
259
  handleAuthorizedTelegramMessage: async () => {},
260
+ handleAuthorizedTelegramEditedMessage: async () => {},
240
261
  },
241
262
  );
242
263
  assert.deepEqual(events, ["media:1,2", "queue:1,2"]);
@@ -271,9 +292,47 @@ test("Update runtime can execute directly from raw updates", async () => {
271
292
  handleAuthorizedTelegramMessage: async () => {
272
293
  events.push("message");
273
294
  },
295
+ handleAuthorizedTelegramEditedMessage: async () => {
296
+ events.push("edited-message");
297
+ },
298
+ },
299
+ );
300
+ assert.deepEqual(events, [
301
+ "pair",
302
+ "reply:Telegram bridge paired with this account.",
303
+ "message",
304
+ ]);
305
+ });
306
+
307
+ test("Update runtime routes edited messages without creating normal message turns", async () => {
308
+ const events: string[] = [];
309
+ await executeTelegramUpdate(
310
+ {
311
+ edited_message: {
312
+ chat: { id: 10, type: "private" },
313
+ message_id: 20,
314
+ from: { id: 7, is_bot: false },
315
+ },
316
+ },
317
+ 7,
318
+ {
319
+ ctx: {} as never,
320
+ removePendingMediaGroupMessages: () => {},
321
+ removeQueuedTelegramTurnsByMessageIds: () => 0,
322
+ handleAuthorizedTelegramReactionUpdate: async () => {},
323
+ pairTelegramUserIfNeeded: async () => false,
324
+ answerCallbackQuery: async () => {},
325
+ handleAuthorizedTelegramCallbackQuery: async () => {},
326
+ sendTextReply: async () => undefined,
327
+ handleAuthorizedTelegramMessage: async () => {
328
+ events.push("message");
329
+ },
330
+ handleAuthorizedTelegramEditedMessage: async () => {
331
+ events.push("edited-message");
332
+ },
274
333
  },
275
334
  );
276
- assert.deepEqual(events, ["pair", "reply:Telegram bridge paired with this account.", "message"]);
335
+ assert.deepEqual(events, ["edited-message"]);
277
336
  });
278
337
 
279
338
  test("Update runtime handles callback deny and message pair flows", async () => {
@@ -311,6 +370,9 @@ test("Update runtime handles callback deny and message pair flows", async () =>
311
370
  handleAuthorizedTelegramMessage: async () => {
312
371
  events.push("message");
313
372
  },
373
+ handleAuthorizedTelegramEditedMessage: async () => {
374
+ events.push("edited-message");
375
+ },
314
376
  },
315
377
  );
316
378
  await executeTelegramUpdatePlan(
@@ -340,6 +402,9 @@ test("Update runtime handles callback deny and message pair flows", async () =>
340
402
  handleAuthorizedTelegramMessage: async () => {
341
403
  events.push("message");
342
404
  },
405
+ handleAuthorizedTelegramEditedMessage: async () => {
406
+ events.push("edited-message");
407
+ },
343
408
  },
344
409
  );
345
410
  assert.deepEqual(events, [