@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.
- package/AGENTS.md +2 -1
- package/CHANGELOG.md +5 -0
- package/README.md +36 -120
- package/docs/architecture.md +13 -6
- package/index.ts +13 -12
- package/lib/polling.ts +2 -0
- package/lib/preview.ts +212 -0
- package/lib/rendering.ts +616 -59
- package/lib/replies.ts +2 -181
- package/lib/updates.ts +0 -8
- package/package.json +1 -1
- package/tests/menu.test.ts +46 -15
- package/tests/preview.test.ts +441 -0
- package/tests/queue.test.ts +3 -0
- package/tests/rendering.test.ts +167 -0
- package/tests/replies.test.ts +2 -222
- package/tests/updates.test.ts +15 -24
package/tests/replies.test.ts
CHANGED
|
@@ -1,239 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Regression tests for
|
|
3
|
-
* Covers
|
|
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({
|
package/tests/updates.test.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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"
|
|
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, [
|
|
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 () => {
|