@llblab/pi-telegram 0.2.8 → 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 +4 -0
- package/README.md +6 -6
- package/docs/architecture.md +11 -5
- package/index.ts +13 -10
- package/lib/preview.ts +212 -0
- package/lib/rendering.ts +583 -46
- package/lib/replies.ts +2 -181
- 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 +124 -2
- package/tests/replies.test.ts +2 -222
- package/tests/updates.test.ts +10 -4
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
|
@@ -119,7 +119,9 @@ test("Update flow returns authorized callback and message actions", () => {
|
|
|
119
119
|
);
|
|
120
120
|
assert.equal(callbackAction.kind, "callback");
|
|
121
121
|
assert.deepEqual(
|
|
122
|
-
callbackAction.kind === "callback"
|
|
122
|
+
callbackAction.kind === "callback"
|
|
123
|
+
? callbackAction.authorization
|
|
124
|
+
: undefined,
|
|
123
125
|
{ kind: "allow" },
|
|
124
126
|
);
|
|
125
127
|
const messageAction = buildTelegramUpdateFlowAction({
|
|
@@ -223,10 +225,10 @@ test("Update runtime executes delete and reaction plans through the right side e
|
|
|
223
225
|
{
|
|
224
226
|
ctx: {} as never,
|
|
225
227
|
removePendingMediaGroupMessages: (ids) => {
|
|
226
|
-
events.push(`media:${ids.join(
|
|
228
|
+
events.push(`media:${ids.join(",")}`);
|
|
227
229
|
},
|
|
228
230
|
removeQueuedTelegramTurnsByMessageIds: (ids) => {
|
|
229
|
-
events.push(`queue:${ids.join(
|
|
231
|
+
events.push(`queue:${ids.join(",")}`);
|
|
230
232
|
return ids.length;
|
|
231
233
|
},
|
|
232
234
|
handleAuthorizedTelegramReactionUpdate: async () => {
|
|
@@ -273,7 +275,11 @@ test("Update runtime can execute directly from raw updates", async () => {
|
|
|
273
275
|
},
|
|
274
276
|
},
|
|
275
277
|
);
|
|
276
|
-
assert.deepEqual(events, [
|
|
278
|
+
assert.deepEqual(events, [
|
|
279
|
+
"pair",
|
|
280
|
+
"reply:Telegram bridge paired with this account.",
|
|
281
|
+
"message",
|
|
282
|
+
]);
|
|
277
283
|
});
|
|
278
284
|
|
|
279
285
|
test("Update runtime handles callback deny and message pair flows", async () => {
|