@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.
@@ -0,0 +1,441 @@
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&gt; 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&gt; Quote:HTML",
335
+ expectedText: "markdown:Para\n\n\n&gt; 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
+ });
@@ -1154,6 +1154,7 @@ test("Extension runtime finalizes a drafted preview into the final Telegram repl
1154
1154
  });
1155
1155
  const draftTexts: string[] = [];
1156
1156
  const sentTexts: string[] = [];
1157
+ const editedTexts: string[] = [];
1157
1158
  const pi = {
1158
1159
  on: (
1159
1160
  event: string,
@@ -1225,6 +1226,7 @@ test("Extension runtime finalizes a drafted preview into the final Telegram repl
1225
1226
  return { json: async () => ({ ok: true, result: true }) } as Response;
1226
1227
  }
1227
1228
  if (method === "editMessageText") {
1229
+ editedTexts.push(String(body?.text ?? ""));
1228
1230
  return { json: async () => ({ ok: true, result: true }) } as Response;
1229
1231
  }
1230
1232
  throw new Error(`Unexpected Telegram API method: ${method}`);
@@ -1284,6 +1286,7 @@ test("Extension runtime finalizes a drafted preview into the final Telegram repl
1284
1286
  assert.deepEqual(draftTexts, ["Draft preview", "Final answer", ""]);
1285
1287
  assert.equal(sentTexts.length, 1);
1286
1288
  assert.match(sentTexts[0] ?? "", /Final <b>answer<\/b>/);
1289
+ assert.deepEqual(editedTexts, []);
1287
1290
  await handlers.get("session_shutdown")?.({}, ctx);
1288
1291
  } finally {
1289
1292
  globalThis.fetch = originalFetch;
@@ -7,7 +7,10 @@ import assert from "node:assert/strict";
7
7
  import test from "node:test";
8
8
 
9
9
  import { __telegramTestUtils } from "../index.ts";
10
- import { renderMarkdownPreviewText } from "../lib/rendering.ts";
10
+ import {
11
+ buildTelegramPreviewSnapshot,
12
+ renderMarkdownPreviewText,
13
+ } from "../lib/rendering.ts";
11
14
 
12
15
  test("Nested lists stay out of code blocks", () => {
13
16
  const chunks = __telegramTestUtils.renderTelegramMessage(
@@ -93,13 +96,88 @@ test("Leading indentation on the first markdown line stays intact", () => {
93
96
  mode: "markdown",
94
97
  });
95
98
  assert.equal(chunks.length, 1);
96
- assert.match(chunks[0]?.text ?? "", /^\u00A0\u00A0<code>-<\/code> nested bullet/m);
99
+ assert.match(
100
+ chunks[0]?.text ?? "",
101
+ /^\u00A0\u00A0<code>-<\/code> nested bullet/m,
102
+ );
97
103
  assert.match(
98
104
  chunks[0]?.text ?? "",
99
105
  /^\u00A0\u00A0\u00A0\u00A0<code>-<\/code> nested child/m,
100
106
  );
101
107
  });
102
108
 
109
+ test("Preview and final rendering preserve multiple blank lines between blocks", () => {
110
+ const markdown = "# Title\n\n\nParagraph\n\n\n> Quote";
111
+ assert.equal(
112
+ renderMarkdownPreviewText(markdown),
113
+ "Title\n\n\nParagraph\n\n\n> Quote",
114
+ );
115
+ const chunks = __telegramTestUtils.renderTelegramMessage(markdown, {
116
+ mode: "markdown",
117
+ });
118
+ assert.equal(chunks.length, 1);
119
+ assert.match(
120
+ chunks[0]?.text ?? "",
121
+ /<b>Title<\/b>\n\n\nParagraph\n\n\n<blockquote>Quote<\/blockquote>/,
122
+ );
123
+ });
124
+
125
+ test("Rendering preserves original blank-line spacing across block transitions", () => {
126
+ const cases = [
127
+ {
128
+ markdown: "Para\n\n\n```ts\nconst x = 1\n```",
129
+ finalText:
130
+ 'Para\n\n\n<pre><code class="language-ts">const x = 1</code></pre>',
131
+ previewText:
132
+ 'Para\n\n\n<pre><code class="language-ts">const x = 1</code></pre>',
133
+ },
134
+ {
135
+ markdown: "```ts\nconst x = 1\n```\n\n\nPara",
136
+ finalText:
137
+ '<pre><code class="language-ts">const x = 1</code></pre>\n\n\nPara',
138
+ previewText:
139
+ '<pre><code class="language-ts">const x = 1</code></pre>\n\n\nPara',
140
+ },
141
+ {
142
+ markdown: "Para\n\n\n- item",
143
+ finalText: "Para\n\n\n<code>-</code> item",
144
+ previewText: "Para\n\n\n- item",
145
+ },
146
+ {
147
+ markdown: "Para\n\n\n> Quote",
148
+ finalText: "Para\n\n\n<blockquote>Quote</blockquote>",
149
+ previewText: "Para\n\n\n&gt; Quote",
150
+ },
151
+ ];
152
+ for (const testCase of cases) {
153
+ const finalChunks = __telegramTestUtils.renderTelegramMessage(
154
+ testCase.markdown,
155
+ { mode: "markdown" },
156
+ );
157
+ assert.equal(finalChunks.length, 1);
158
+ assert.equal(finalChunks[0]?.text ?? "", testCase.finalText);
159
+ const preview = buildTelegramPreviewSnapshot({
160
+ state: { pendingText: testCase.markdown, lastSentText: "" },
161
+ maxMessageLength: __telegramTestUtils.MAX_MESSAGE_LENGTH,
162
+ renderPreviewText: renderMarkdownPreviewText,
163
+ renderTelegramMessage: __telegramTestUtils.renderTelegramMessage,
164
+ });
165
+ assert.equal(preview?.text ?? "", testCase.previewText);
166
+ }
167
+ });
168
+
169
+ test("Headings keep visible spacing before following code blocks even without source blank lines", () => {
170
+ const markdown = "### Title\n```ts\nconst x = 1\n```";
171
+ const chunks = __telegramTestUtils.renderTelegramMessage(markdown, {
172
+ mode: "markdown",
173
+ });
174
+ assert.equal(chunks.length, 1);
175
+ assert.match(
176
+ chunks[0]?.text ?? "",
177
+ /<b>Title<\/b>\n\n<pre><code class="language-ts">const x = 1<\/code><\/pre>/,
178
+ );
179
+ });
180
+
103
181
  test("Standalone checkbox-looking prose stays literal outside task lists", () => {
104
182
  const markdown = "Use [ ] as a placeholder and keep [x] literal";
105
183
  assert.equal(renderMarkdownPreviewText(markdown), markdown);
@@ -154,6 +232,50 @@ test("Links, code spans, and underscore-heavy text coexist safely", () => {
154
232
  assert.equal((chunks[0]?.text ?? "").includes("<i>bar</i>"), false);
155
233
  });
156
234
 
235
+ test("Links degrade or normalize safely across supported and unsupported markdown forms", () => {
236
+ const markdown = [
237
+ "[**Bold** label](https://example.com/path)",
238
+ "[Docs](https://example.com/a_(b))",
239
+ '[Title](https://example.com/path "Tooltip")',
240
+ "[Relative](./docs/README.md)",
241
+ "[Ref][docs]",
242
+ "",
243
+ "[docs]: https://example.com/ref",
244
+ "",
245
+ "Footnote[^1]",
246
+ "",
247
+ "[^1]: Footnote body",
248
+ ].join("\n");
249
+ const chunks = __telegramTestUtils.renderTelegramMessage(markdown, {
250
+ mode: "markdown",
251
+ });
252
+ assert.equal(chunks.length, 1);
253
+ assert.match(
254
+ chunks[0]?.text ?? "",
255
+ /<a href="https:\/\/example.com\/path">Bold label<\/a>/,
256
+ );
257
+ assert.match(
258
+ chunks[0]?.text ?? "",
259
+ /<a href="https:\/\/example.com\/a_\(b\)">Docs<\/a>/,
260
+ );
261
+ assert.match(
262
+ chunks[0]?.text ?? "",
263
+ /<a href="https:\/\/example.com\/path">Title<\/a>/,
264
+ );
265
+ assert.equal(
266
+ (chunks[0]?.text ?? "").includes('<a href="./docs/README.md">'),
267
+ false,
268
+ );
269
+ assert.match(chunks[0]?.text ?? "", /Relative/);
270
+ assert.equal(
271
+ (chunks[0]?.text ?? "").includes('<a href="https://example.com/ref">'),
272
+ false,
273
+ );
274
+ assert.match(chunks[0]?.text ?? "", /\[Ref\]\[docs\]/);
275
+ assert.match(chunks[0]?.text ?? "", /Footnote\[\^1\]/);
276
+ assert.match(chunks[0]?.text ?? "", /\[\^1\]: Footnote body/);
277
+ });
278
+
157
279
  test("Long quoted blocks stay chunked with balanced blockquote tags", () => {
158
280
  const markdown = Array.from(
159
281
  { length: 500 },