@llblab/pi-telegram 0.2.9 → 0.3.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.
- package/README.md +40 -26
- package/docs/architecture.md +62 -35
- package/index.ts +388 -1936
- package/lib/api.ts +647 -76
- package/lib/attachments.ts +128 -16
- package/lib/commands.ts +721 -0
- package/lib/config.ts +157 -0
- package/lib/media.ts +211 -36
- package/lib/menu.ts +920 -338
- package/lib/model.ts +647 -0
- package/lib/pi.ts +80 -0
- package/lib/polling.ts +264 -18
- package/lib/preview.ts +451 -29
- package/lib/queue.ts +1134 -110
- package/lib/registration.ts +127 -28
- package/lib/rendering.ts +575 -281
- package/lib/replies.ts +198 -8
- package/lib/runtime.ts +475 -0
- package/lib/setup.ts +129 -1
- package/lib/status.ts +428 -13
- package/lib/turns.ts +207 -17
- package/lib/updates.ts +392 -99
- package/package.json +18 -3
- package/AGENTS.md +0 -91
- package/BACKLOG.md +0 -5
- package/CHANGELOG.md +0 -23
- package/lib/model-switch.ts +0 -62
- package/tests/api.test.ts +0 -89
- package/tests/attachments.test.ts +0 -132
- package/tests/config.test.ts +0 -80
- package/tests/media.test.ts +0 -77
- package/tests/menu.test.ts +0 -676
- package/tests/polling.test.ts +0 -129
- package/tests/preview.test.ts +0 -441
- package/tests/queue.test.ts +0 -3245
- package/tests/registration.test.ts +0 -268
- package/tests/rendering.test.ts +0 -475
- package/tests/replies.test.ts +0 -142
- package/tests/turns.test.ts +0 -132
- package/tests/updates.test.ts +0 -357
package/tests/polling.test.ts
DELETED
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Regression tests for the Telegram polling domain
|
|
3
|
-
* Covers polling request helpers, stop conditions, and the long-poll loop runtime in one suite
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import assert from "node:assert/strict";
|
|
7
|
-
import test from "node:test";
|
|
8
|
-
|
|
9
|
-
import {
|
|
10
|
-
TELEGRAM_ALLOWED_UPDATES,
|
|
11
|
-
buildTelegramInitialSyncRequest,
|
|
12
|
-
buildTelegramLongPollRequest,
|
|
13
|
-
getLatestTelegramUpdateId,
|
|
14
|
-
runTelegramPollLoop,
|
|
15
|
-
shouldStopTelegramPolling,
|
|
16
|
-
} from "../lib/polling.ts";
|
|
17
|
-
|
|
18
|
-
test("Polling helpers build the initial sync request", () => {
|
|
19
|
-
assert.deepEqual(buildTelegramInitialSyncRequest(), {
|
|
20
|
-
offset: -1,
|
|
21
|
-
limit: 1,
|
|
22
|
-
timeout: 0,
|
|
23
|
-
});
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
test("Polling helpers build long-poll requests with and without lastUpdateId", () => {
|
|
27
|
-
assert.deepEqual(buildTelegramLongPollRequest(), {
|
|
28
|
-
offset: undefined,
|
|
29
|
-
limit: 10,
|
|
30
|
-
timeout: 30,
|
|
31
|
-
allowed_updates: TELEGRAM_ALLOWED_UPDATES,
|
|
32
|
-
});
|
|
33
|
-
assert.deepEqual(buildTelegramLongPollRequest(41), {
|
|
34
|
-
offset: 42,
|
|
35
|
-
limit: 10,
|
|
36
|
-
timeout: 30,
|
|
37
|
-
allowed_updates: TELEGRAM_ALLOWED_UPDATES,
|
|
38
|
-
});
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
test("Polling helpers extract the latest update id", () => {
|
|
42
|
-
assert.equal(getLatestTelegramUpdateId([]), undefined);
|
|
43
|
-
assert.equal(
|
|
44
|
-
getLatestTelegramUpdateId([{ update_id: 1 }, { update_id: 7 }]),
|
|
45
|
-
7,
|
|
46
|
-
);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
test("Polling helpers stop only for abort conditions", () => {
|
|
50
|
-
assert.equal(shouldStopTelegramPolling(true, new Error("ignored")), true);
|
|
51
|
-
assert.equal(
|
|
52
|
-
shouldStopTelegramPolling(false, new DOMException("aborted", "AbortError")),
|
|
53
|
-
true,
|
|
54
|
-
);
|
|
55
|
-
assert.equal(shouldStopTelegramPolling(false, new Error("network")), false);
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
test("Poll loop initializes lastUpdateId and processes updates", async () => {
|
|
59
|
-
const handled: number[] = [];
|
|
60
|
-
const config: { botToken: string; lastUpdateId?: number } = {
|
|
61
|
-
botToken: "123:abc",
|
|
62
|
-
};
|
|
63
|
-
let getUpdatesCalls = 0;
|
|
64
|
-
let persistCount = 0;
|
|
65
|
-
const signal = new AbortController().signal;
|
|
66
|
-
await runTelegramPollLoop({
|
|
67
|
-
ctx: {} as never,
|
|
68
|
-
signal,
|
|
69
|
-
config,
|
|
70
|
-
deleteWebhook: async () => {},
|
|
71
|
-
getUpdates: async () => {
|
|
72
|
-
getUpdatesCalls += 1;
|
|
73
|
-
if (getUpdatesCalls === 1) {
|
|
74
|
-
return [{ update_id: 5 }];
|
|
75
|
-
}
|
|
76
|
-
if (getUpdatesCalls === 2) {
|
|
77
|
-
return [{ update_id: 6 }, { update_id: 7 }];
|
|
78
|
-
}
|
|
79
|
-
throw new DOMException("stop", "AbortError");
|
|
80
|
-
},
|
|
81
|
-
persistConfig: async () => {
|
|
82
|
-
persistCount += 1;
|
|
83
|
-
},
|
|
84
|
-
handleUpdate: async (update) => {
|
|
85
|
-
handled.push(update.update_id);
|
|
86
|
-
},
|
|
87
|
-
onErrorStatus: () => {},
|
|
88
|
-
onStatusReset: () => {},
|
|
89
|
-
sleep: async () => {},
|
|
90
|
-
});
|
|
91
|
-
assert.equal(config.lastUpdateId, 7);
|
|
92
|
-
assert.deepEqual(handled, [6, 7]);
|
|
93
|
-
assert.equal(persistCount, 3);
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
test("Poll loop reports retryable errors and sleeps before retrying", async () => {
|
|
97
|
-
const config = { botToken: "123:abc", lastUpdateId: 1 };
|
|
98
|
-
const statusMessages: string[] = [];
|
|
99
|
-
let calls = 0;
|
|
100
|
-
await runTelegramPollLoop({
|
|
101
|
-
ctx: {} as never,
|
|
102
|
-
signal: new AbortController().signal,
|
|
103
|
-
config,
|
|
104
|
-
deleteWebhook: async () => {},
|
|
105
|
-
getUpdates: async () => {
|
|
106
|
-
calls += 1;
|
|
107
|
-
if (calls === 1) {
|
|
108
|
-
throw new Error("network down");
|
|
109
|
-
}
|
|
110
|
-
throw new DOMException("stop", "AbortError");
|
|
111
|
-
},
|
|
112
|
-
persistConfig: async () => {},
|
|
113
|
-
handleUpdate: async () => {},
|
|
114
|
-
onErrorStatus: (message) => {
|
|
115
|
-
statusMessages.push(`error:${message}`);
|
|
116
|
-
},
|
|
117
|
-
onStatusReset: () => {
|
|
118
|
-
statusMessages.push("reset");
|
|
119
|
-
},
|
|
120
|
-
sleep: async (ms) => {
|
|
121
|
-
statusMessages.push(`sleep:${ms}`);
|
|
122
|
-
},
|
|
123
|
-
});
|
|
124
|
-
assert.deepEqual(statusMessages, [
|
|
125
|
-
"error:network down",
|
|
126
|
-
"sleep:3000",
|
|
127
|
-
"reset",
|
|
128
|
-
]);
|
|
129
|
-
});
|
package/tests/preview.test.ts
DELETED
|
@@ -1,441 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Regression tests for the Telegram preview domain
|
|
3
|
-
* Covers preview snapshot decisions, transport selection, runtime flushing, and finalization behavior
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import assert from "node:assert/strict";
|
|
7
|
-
import test from "node:test";
|
|
8
|
-
|
|
9
|
-
import {
|
|
10
|
-
buildTelegramPreviewFlushText,
|
|
11
|
-
buildTelegramPreviewSnapshot,
|
|
12
|
-
renderTelegramMessage,
|
|
13
|
-
type TelegramRenderedChunk,
|
|
14
|
-
type TelegramRenderMode,
|
|
15
|
-
} from "../lib/rendering.ts";
|
|
16
|
-
import {
|
|
17
|
-
buildTelegramPreviewFinalText,
|
|
18
|
-
clearTelegramPreview,
|
|
19
|
-
finalizeTelegramMarkdownPreview,
|
|
20
|
-
finalizeTelegramPreview,
|
|
21
|
-
flushTelegramPreview,
|
|
22
|
-
shouldUseTelegramDraftPreview,
|
|
23
|
-
} from "../lib/preview.ts";
|
|
24
|
-
|
|
25
|
-
function createPreviewRuntimeHarness(state?: {
|
|
26
|
-
mode: "draft" | "message";
|
|
27
|
-
draftId?: number;
|
|
28
|
-
messageId?: number;
|
|
29
|
-
pendingText: string;
|
|
30
|
-
lastSentText: string;
|
|
31
|
-
lastSentParseMode?: "HTML";
|
|
32
|
-
lastSentStrategy?: "plain" | "rich-stable-blocks";
|
|
33
|
-
flushTimer?: ReturnType<typeof setTimeout>;
|
|
34
|
-
}) {
|
|
35
|
-
let previewState = state;
|
|
36
|
-
let draftSupport: "unknown" | "supported" | "unsupported" = "unknown";
|
|
37
|
-
let nextDraftId = 10;
|
|
38
|
-
const events: string[] = [];
|
|
39
|
-
return {
|
|
40
|
-
events,
|
|
41
|
-
getState: () => previewState,
|
|
42
|
-
getDraftSupport: () => draftSupport,
|
|
43
|
-
setDraftSupport: (support: "unknown" | "supported" | "unsupported") => {
|
|
44
|
-
draftSupport = support;
|
|
45
|
-
},
|
|
46
|
-
deps: {
|
|
47
|
-
getState: () => previewState,
|
|
48
|
-
setState: (nextState: typeof previewState) => {
|
|
49
|
-
previewState = nextState;
|
|
50
|
-
},
|
|
51
|
-
clearScheduledFlush: (nextState: NonNullable<typeof previewState>) => {
|
|
52
|
-
if (!nextState.flushTimer) return;
|
|
53
|
-
clearTimeout(nextState.flushTimer);
|
|
54
|
-
nextState.flushTimer = undefined;
|
|
55
|
-
events.push("clear-timer");
|
|
56
|
-
},
|
|
57
|
-
maxMessageLength: 50,
|
|
58
|
-
renderPreviewText: (markdown: string) => markdown.replaceAll("*", ""),
|
|
59
|
-
getDraftSupport: () => draftSupport,
|
|
60
|
-
setDraftSupport: (support: "unknown" | "supported" | "unsupported") => {
|
|
61
|
-
draftSupport = support;
|
|
62
|
-
},
|
|
63
|
-
allocateDraftId: () => nextDraftId++,
|
|
64
|
-
sendDraft: async (chatId: number, draftId: number, text: string) => {
|
|
65
|
-
events.push(`draft:${chatId}:${draftId}:${text}`);
|
|
66
|
-
},
|
|
67
|
-
sendMessage: async (
|
|
68
|
-
chatId: number,
|
|
69
|
-
text: string,
|
|
70
|
-
options?: { parseMode?: "HTML" },
|
|
71
|
-
) => {
|
|
72
|
-
events.push(`send:${chatId}:${text}:${options?.parseMode ?? "plain"}`);
|
|
73
|
-
return { message_id: 77 };
|
|
74
|
-
},
|
|
75
|
-
editMessageText: async (
|
|
76
|
-
chatId: number,
|
|
77
|
-
messageId: number,
|
|
78
|
-
text: string,
|
|
79
|
-
options?: { parseMode?: "HTML" },
|
|
80
|
-
) => {
|
|
81
|
-
events.push(
|
|
82
|
-
`edit:${chatId}:${messageId}:${text}:${options?.parseMode ?? "plain"}`,
|
|
83
|
-
);
|
|
84
|
-
},
|
|
85
|
-
renderTelegramMessage: (
|
|
86
|
-
text: string,
|
|
87
|
-
options?: { mode?: TelegramRenderMode },
|
|
88
|
-
): TelegramRenderedChunk[] =>
|
|
89
|
-
options?.mode === "markdown"
|
|
90
|
-
? [{ text: `markdown:${text}`, parseMode: "HTML" as const }]
|
|
91
|
-
: [{ text: `${options?.mode ?? "plain"}:${text}` }],
|
|
92
|
-
sendRenderedChunks: async (
|
|
93
|
-
chatId: number,
|
|
94
|
-
chunks: Array<{ text: string }>,
|
|
95
|
-
) => {
|
|
96
|
-
events.push(
|
|
97
|
-
`render-send:${chatId}:${chunks.map((chunk) => chunk.text).join("|")}`,
|
|
98
|
-
);
|
|
99
|
-
return 88;
|
|
100
|
-
},
|
|
101
|
-
editRenderedMessage: async (
|
|
102
|
-
chatId: number,
|
|
103
|
-
messageId: number,
|
|
104
|
-
chunks: Array<{ text: string }>,
|
|
105
|
-
) => {
|
|
106
|
-
events.push(
|
|
107
|
-
`render-edit:${chatId}:${messageId}:${chunks.map((chunk) => chunk.text).join("|")}`,
|
|
108
|
-
);
|
|
109
|
-
return messageId;
|
|
110
|
-
},
|
|
111
|
-
},
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
test("Preview helpers build flush text only when the preview changed", () => {
|
|
116
|
-
assert.equal(
|
|
117
|
-
buildTelegramPreviewFlushText({
|
|
118
|
-
state: {
|
|
119
|
-
pendingText: "**hello**",
|
|
120
|
-
lastSentText: "",
|
|
121
|
-
},
|
|
122
|
-
maxMessageLength: 4096,
|
|
123
|
-
renderPreviewText: (markdown) => markdown.replaceAll("*", ""),
|
|
124
|
-
}),
|
|
125
|
-
"hello",
|
|
126
|
-
);
|
|
127
|
-
assert.equal(
|
|
128
|
-
buildTelegramPreviewFlushText({
|
|
129
|
-
state: {
|
|
130
|
-
pendingText: "**hello**",
|
|
131
|
-
lastSentText: "hello",
|
|
132
|
-
},
|
|
133
|
-
maxMessageLength: 4096,
|
|
134
|
-
renderPreviewText: (markdown) => markdown.replaceAll("*", ""),
|
|
135
|
-
}),
|
|
136
|
-
undefined,
|
|
137
|
-
);
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
test("Preview snapshots prefer stable rich blocks and fall back to plain text", () => {
|
|
141
|
-
const richSnapshot = buildTelegramPreviewSnapshot({
|
|
142
|
-
state: {
|
|
143
|
-
pendingText: "```ts\nconst value = 1\n```",
|
|
144
|
-
lastSentText: "",
|
|
145
|
-
},
|
|
146
|
-
maxMessageLength: 100,
|
|
147
|
-
renderPreviewText: (markdown) => markdown.replaceAll("*", ""),
|
|
148
|
-
renderTelegramMessage: (text, options) =>
|
|
149
|
-
options?.mode === "markdown"
|
|
150
|
-
? [{ text: `<b>${text}</b>`, parseMode: "HTML" as const }]
|
|
151
|
-
: [{ text }],
|
|
152
|
-
});
|
|
153
|
-
assert.deepEqual(richSnapshot, {
|
|
154
|
-
text: "<b>```ts\nconst value = 1\n```</b>",
|
|
155
|
-
parseMode: "HTML",
|
|
156
|
-
sourceText: "```ts\nconst value = 1\n```",
|
|
157
|
-
strategy: "rich-stable-blocks",
|
|
158
|
-
});
|
|
159
|
-
const plainSnapshot = buildTelegramPreviewSnapshot({
|
|
160
|
-
state: {
|
|
161
|
-
pendingText: "**hello**",
|
|
162
|
-
lastSentText: "",
|
|
163
|
-
},
|
|
164
|
-
maxMessageLength: 5,
|
|
165
|
-
renderPreviewText: (markdown) => markdown.replaceAll("*", ""),
|
|
166
|
-
renderTelegramMessage: () => [
|
|
167
|
-
{ text: "markdown:too-long", parseMode: "HTML" as const },
|
|
168
|
-
],
|
|
169
|
-
});
|
|
170
|
-
assert.deepEqual(plainSnapshot, {
|
|
171
|
-
text: "hello",
|
|
172
|
-
sourceText: "**hello**",
|
|
173
|
-
strategy: "plain",
|
|
174
|
-
});
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
test("Preview snapshots append conservative plain tails for incomplete fences, quotes, and lists", () => {
|
|
178
|
-
const renderTelegramMessage = (
|
|
179
|
-
text: string,
|
|
180
|
-
options?: { mode?: TelegramRenderMode },
|
|
181
|
-
) =>
|
|
182
|
-
options?.mode === "markdown"
|
|
183
|
-
? [{ text: `<b>${text}</b>`, parseMode: "HTML" as const }]
|
|
184
|
-
: [{ text }];
|
|
185
|
-
const fenceSnapshot = buildTelegramPreviewSnapshot({
|
|
186
|
-
state: {
|
|
187
|
-
pendingText: "## Intro\n\n```ts\nconst value = 1",
|
|
188
|
-
lastSentText: "",
|
|
189
|
-
},
|
|
190
|
-
maxMessageLength: 200,
|
|
191
|
-
renderPreviewText: (markdown) => markdown,
|
|
192
|
-
renderTelegramMessage,
|
|
193
|
-
});
|
|
194
|
-
assert.deepEqual(fenceSnapshot, {
|
|
195
|
-
text: "<b>## Intro</b>\n\n```ts\nconst value = 1",
|
|
196
|
-
parseMode: "HTML",
|
|
197
|
-
sourceText: "## Intro\n\n```ts\nconst value = 1",
|
|
198
|
-
strategy: "rich-stable-blocks",
|
|
199
|
-
});
|
|
200
|
-
const quoteSnapshot = buildTelegramPreviewSnapshot({
|
|
201
|
-
state: {
|
|
202
|
-
pendingText: "## Intro\n\n\n> quoted line",
|
|
203
|
-
lastSentText: "",
|
|
204
|
-
},
|
|
205
|
-
maxMessageLength: 200,
|
|
206
|
-
renderPreviewText: (markdown) => markdown,
|
|
207
|
-
renderTelegramMessage,
|
|
208
|
-
});
|
|
209
|
-
assert.deepEqual(quoteSnapshot, {
|
|
210
|
-
text: "<b>## Intro</b>\n\n\n> quoted line",
|
|
211
|
-
parseMode: "HTML",
|
|
212
|
-
sourceText: "## Intro\n\n\n> quoted line",
|
|
213
|
-
strategy: "rich-stable-blocks",
|
|
214
|
-
});
|
|
215
|
-
const listSnapshot = buildTelegramPreviewSnapshot({
|
|
216
|
-
state: {
|
|
217
|
-
pendingText: "## Intro\n\n\n- first\n- second",
|
|
218
|
-
lastSentText: "",
|
|
219
|
-
},
|
|
220
|
-
maxMessageLength: 200,
|
|
221
|
-
renderPreviewText: (markdown) => markdown,
|
|
222
|
-
renderTelegramMessage,
|
|
223
|
-
});
|
|
224
|
-
assert.deepEqual(listSnapshot, {
|
|
225
|
-
text: "<b>## Intro</b>\n\n\n- first\n- second",
|
|
226
|
-
parseMode: "HTML",
|
|
227
|
-
sourceText: "## Intro\n\n\n- first\n- second",
|
|
228
|
-
strategy: "rich-stable-blocks",
|
|
229
|
-
});
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
test("Preview snapshots omit unstable tails when long-message limits leave no room", () => {
|
|
233
|
-
const snapshot = buildTelegramPreviewSnapshot({
|
|
234
|
-
state: {
|
|
235
|
-
pendingText: "## Intro\n\n- first\n- second\n- third",
|
|
236
|
-
lastSentText: "",
|
|
237
|
-
},
|
|
238
|
-
maxMessageLength: 18,
|
|
239
|
-
renderPreviewText: (markdown) => markdown,
|
|
240
|
-
renderTelegramMessage: (text, options) =>
|
|
241
|
-
options?.mode === "markdown"
|
|
242
|
-
? [{ text: `<b>${text}</b>`, parseMode: "HTML" as const }]
|
|
243
|
-
: [{ text }],
|
|
244
|
-
});
|
|
245
|
-
assert.deepEqual(snapshot, {
|
|
246
|
-
text: "<b>## Intro</b>",
|
|
247
|
-
parseMode: "HTML",
|
|
248
|
-
sourceText: "## Intro\n\n- first\n- second\n- third",
|
|
249
|
-
strategy: "rich-stable-blocks",
|
|
250
|
-
});
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
test("Preview helpers compute final text fallback without reusing rich HTML snapshots", () => {
|
|
254
|
-
assert.equal(
|
|
255
|
-
buildTelegramPreviewFinalText({
|
|
256
|
-
mode: "message",
|
|
257
|
-
pendingText: " ",
|
|
258
|
-
lastSentText: "saved",
|
|
259
|
-
lastSentStrategy: "plain",
|
|
260
|
-
}),
|
|
261
|
-
"saved",
|
|
262
|
-
);
|
|
263
|
-
assert.equal(
|
|
264
|
-
buildTelegramPreviewFinalText({
|
|
265
|
-
mode: "message",
|
|
266
|
-
pendingText: " ",
|
|
267
|
-
lastSentText: "<b>saved</b>",
|
|
268
|
-
lastSentParseMode: "HTML",
|
|
269
|
-
lastSentStrategy: "rich-stable-blocks",
|
|
270
|
-
}),
|
|
271
|
-
undefined,
|
|
272
|
-
);
|
|
273
|
-
assert.equal(
|
|
274
|
-
buildTelegramPreviewFinalText({
|
|
275
|
-
mode: "message",
|
|
276
|
-
pendingText: " ",
|
|
277
|
-
lastSentText: " ",
|
|
278
|
-
}),
|
|
279
|
-
undefined,
|
|
280
|
-
);
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
test("Preview helpers use drafts only for plain preview snapshots", () => {
|
|
284
|
-
assert.equal(
|
|
285
|
-
shouldUseTelegramDraftPreview({ draftSupport: "unknown" }),
|
|
286
|
-
true,
|
|
287
|
-
);
|
|
288
|
-
assert.equal(
|
|
289
|
-
shouldUseTelegramDraftPreview({
|
|
290
|
-
draftSupport: "supported",
|
|
291
|
-
snapshot: { text: "preview", sourceText: "preview", strategy: "plain" },
|
|
292
|
-
}),
|
|
293
|
-
true,
|
|
294
|
-
);
|
|
295
|
-
assert.equal(
|
|
296
|
-
shouldUseTelegramDraftPreview({
|
|
297
|
-
draftSupport: "supported",
|
|
298
|
-
snapshot: {
|
|
299
|
-
text: "<b>preview</b>",
|
|
300
|
-
parseMode: "HTML",
|
|
301
|
-
sourceText: "preview",
|
|
302
|
-
strategy: "rich-stable-blocks",
|
|
303
|
-
},
|
|
304
|
-
}),
|
|
305
|
-
false,
|
|
306
|
-
);
|
|
307
|
-
assert.equal(
|
|
308
|
-
shouldUseTelegramDraftPreview({ draftSupport: "unsupported" }),
|
|
309
|
-
false,
|
|
310
|
-
);
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
test("Preview runtime prefers editable rich previews when stable blocks are available", async () => {
|
|
314
|
-
const harness = createPreviewRuntimeHarness({
|
|
315
|
-
mode: "draft",
|
|
316
|
-
pendingText: "## Intro\n\nTail",
|
|
317
|
-
lastSentText: "",
|
|
318
|
-
flushTimer: setTimeout(() => {}, 1000),
|
|
319
|
-
});
|
|
320
|
-
await flushTelegramPreview(7, harness.deps);
|
|
321
|
-
assert.deepEqual(harness.events, ["send:7:markdown:## Intro\n\nTail:HTML"]);
|
|
322
|
-
assert.equal(harness.getState()?.mode, "message");
|
|
323
|
-
assert.equal(harness.getState()?.messageId, 77);
|
|
324
|
-
assert.equal(harness.getState()?.lastSentText, "markdown:## Intro\n\nTail");
|
|
325
|
-
assert.equal(harness.getState()?.lastSentParseMode, "HTML");
|
|
326
|
-
assert.equal(harness.getState()?.lastSentStrategy, "rich-stable-blocks");
|
|
327
|
-
assert.equal(harness.getDraftSupport(), "unknown");
|
|
328
|
-
});
|
|
329
|
-
|
|
330
|
-
test("Preview runtime preserves original blank-line spacing around conservative tails", async () => {
|
|
331
|
-
const cases = [
|
|
332
|
-
{
|
|
333
|
-
markdown: "Para\n\n\n> Quote",
|
|
334
|
-
expectedEvent: "send:7:markdown:Para\n\n\n> Quote:HTML",
|
|
335
|
-
expectedText: "markdown:Para\n\n\n> Quote",
|
|
336
|
-
},
|
|
337
|
-
{
|
|
338
|
-
markdown: "Para\n\n\n- item",
|
|
339
|
-
expectedEvent: "send:7:markdown:Para\n\n\n- item:HTML",
|
|
340
|
-
expectedText: "markdown:Para\n\n\n- item",
|
|
341
|
-
},
|
|
342
|
-
];
|
|
343
|
-
for (const testCase of cases) {
|
|
344
|
-
const harness = createPreviewRuntimeHarness({
|
|
345
|
-
mode: "draft",
|
|
346
|
-
pendingText: testCase.markdown,
|
|
347
|
-
lastSentText: "",
|
|
348
|
-
flushTimer: setTimeout(() => {}, 1000),
|
|
349
|
-
});
|
|
350
|
-
await flushTelegramPreview(7, harness.deps);
|
|
351
|
-
assert.deepEqual(harness.events, [testCase.expectedEvent]);
|
|
352
|
-
assert.equal(harness.getState()?.lastSentText, testCase.expectedText);
|
|
353
|
-
}
|
|
354
|
-
});
|
|
355
|
-
|
|
356
|
-
test("Preview runtime keeps heading-to-code spacing readable without source blank lines", async () => {
|
|
357
|
-
const harness = createPreviewRuntimeHarness({
|
|
358
|
-
mode: "draft",
|
|
359
|
-
pendingText: "### Title\n```ts\nconst x = 1\n```",
|
|
360
|
-
lastSentText: "",
|
|
361
|
-
flushTimer: setTimeout(() => {}, 1000),
|
|
362
|
-
});
|
|
363
|
-
harness.deps.renderTelegramMessage = renderTelegramMessage;
|
|
364
|
-
harness.deps.maxMessageLength = 4096;
|
|
365
|
-
await flushTelegramPreview(7, harness.deps);
|
|
366
|
-
assert.deepEqual(harness.events, [
|
|
367
|
-
'send:7:<b>Title</b>\n\n<pre><code class="language-ts">const x = 1</code></pre>:HTML',
|
|
368
|
-
]);
|
|
369
|
-
assert.equal(
|
|
370
|
-
harness.getState()?.lastSentText,
|
|
371
|
-
'<b>Title</b>\n\n<pre><code class="language-ts">const x = 1</code></pre>',
|
|
372
|
-
);
|
|
373
|
-
});
|
|
374
|
-
|
|
375
|
-
test("Preview runtime can still use and clear plain draft previews", async () => {
|
|
376
|
-
const harness = createPreviewRuntimeHarness({
|
|
377
|
-
mode: "draft",
|
|
378
|
-
pendingText: "**hello**",
|
|
379
|
-
lastSentText: "",
|
|
380
|
-
flushTimer: setTimeout(() => {}, 1000),
|
|
381
|
-
});
|
|
382
|
-
harness.deps.renderTelegramMessage = () => [];
|
|
383
|
-
await flushTelegramPreview(7, harness.deps);
|
|
384
|
-
assert.deepEqual(harness.events, ["draft:7:10:hello"]);
|
|
385
|
-
assert.equal(harness.getState()?.mode, "draft");
|
|
386
|
-
assert.equal(harness.getState()?.draftId, 10);
|
|
387
|
-
assert.equal(harness.getState()?.lastSentText, "hello");
|
|
388
|
-
assert.equal(harness.getState()?.lastSentStrategy, "plain");
|
|
389
|
-
assert.equal(harness.getDraftSupport(), "supported");
|
|
390
|
-
await clearTelegramPreview(7, harness.deps);
|
|
391
|
-
assert.deepEqual(harness.events, ["draft:7:10:hello", "draft:7:10:"]);
|
|
392
|
-
assert.equal(harness.getState(), undefined);
|
|
393
|
-
});
|
|
394
|
-
|
|
395
|
-
test("Preview runtime falls back to editable plain messages when draft delivery fails", async () => {
|
|
396
|
-
const harness = createPreviewRuntimeHarness({
|
|
397
|
-
mode: "draft",
|
|
398
|
-
pendingText: "abcdef",
|
|
399
|
-
lastSentText: "",
|
|
400
|
-
});
|
|
401
|
-
harness.deps.renderTelegramMessage = () => [];
|
|
402
|
-
harness.deps.sendDraft = async () => {
|
|
403
|
-
throw new Error("draft unsupported");
|
|
404
|
-
};
|
|
405
|
-
await flushTelegramPreview(7, harness.deps);
|
|
406
|
-
assert.deepEqual(harness.events, ["send:7:abcdef:plain"]);
|
|
407
|
-
assert.equal(harness.getState()?.mode, "message");
|
|
408
|
-
assert.equal(harness.getState()?.messageId, 77);
|
|
409
|
-
assert.equal(harness.getState()?.lastSentStrategy, "plain");
|
|
410
|
-
assert.equal(harness.getDraftSupport(), "unsupported");
|
|
411
|
-
});
|
|
412
|
-
|
|
413
|
-
test("Preview runtime finalizes plain and markdown previews", async () => {
|
|
414
|
-
const plainHarness = createPreviewRuntimeHarness({
|
|
415
|
-
mode: "message",
|
|
416
|
-
messageId: 44,
|
|
417
|
-
pendingText: "done",
|
|
418
|
-
lastSentText: "",
|
|
419
|
-
});
|
|
420
|
-
plainHarness.setDraftSupport("unsupported");
|
|
421
|
-
plainHarness.deps.renderTelegramMessage = () => [];
|
|
422
|
-
assert.equal(await finalizeTelegramPreview(7, plainHarness.deps), true);
|
|
423
|
-
assert.deepEqual(plainHarness.events, ["edit:7:44:done:plain"]);
|
|
424
|
-
assert.equal(plainHarness.getState(), undefined);
|
|
425
|
-
const markdownHarness = createPreviewRuntimeHarness({
|
|
426
|
-
mode: "message",
|
|
427
|
-
messageId: 55,
|
|
428
|
-
pendingText: "done",
|
|
429
|
-
lastSentText: "",
|
|
430
|
-
});
|
|
431
|
-
markdownHarness.setDraftSupport("unsupported");
|
|
432
|
-
assert.equal(
|
|
433
|
-
await finalizeTelegramMarkdownPreview(7, "**done**", markdownHarness.deps),
|
|
434
|
-
true,
|
|
435
|
-
);
|
|
436
|
-
assert.deepEqual(markdownHarness.events, [
|
|
437
|
-
"edit:7:55:done:plain",
|
|
438
|
-
"render-edit:7:55:markdown:**done**",
|
|
439
|
-
]);
|
|
440
|
-
assert.equal(markdownHarness.getState(), undefined);
|
|
441
|
-
});
|