/** * Regression tests for Telegram UI/compat rendering helpers * Covers nested lists, code blocks, tables, links, quotes, chunking, and other Telegram-specific render edge cases */ import assert from "node:assert/strict"; import test from "node:test"; import { chunkHtmlPreservingTags, escapeHtml, escapeHtmlAttribute, MAX_MESSAGE_LENGTH, renderTelegramMessage, } from "../lib/rendering.ts"; function countMatches(text: string, pattern: RegExp): number { return text.match(pattern)?.length ?? 0; } test("HTML helpers escape text and attribute contexts", () => { assert.equal(escapeHtml("&value"), "<tag>&value"); assert.equal( escapeHtmlAttribute(`"quoted" 'value' & `), ""quoted" 'value' & <tag>", ); }); test("HTML helpers chunk long text while preserving open tags", () => { const chunks = chunkHtmlPreservingTags(`${"x".repeat(10)}`, 12); assert.deepEqual(chunks, ["xxxxx", "xxxxx"]); }); test("HTML helpers do not reopen void tags across chunks", () => { const chunks = chunkHtmlPreservingTags(`a
${"b".repeat(10)}`, 8); assert.deepEqual(chunks, ["a
bbb", "bbbbbbbb".slice(0, 7)]); }); test("Malformed and boundary markdown renders safely", () => { const cases = [ "", "```ts\nconst raw = '**not bold**'", `${"x".repeat(MAX_MESSAGE_LENGTH + 250)} end`, `surrogate-boundary: ${"🙂".repeat(120)}\uD83D`, [ "> level 1", "> > level 2", "> > > level 3", "> > > > level 4", "> **bold quote", ].join("\n"), ]; for (const markdown of cases) { let chunks: ReturnType = []; assert.doesNotThrow(() => { chunks = renderTelegramMessage(markdown, { mode: "markdown" }); }); for (const chunk of chunks) { assert.ok(chunk.text.length <= MAX_MESSAGE_LENGTH); assert.equal( countMatches(chunk.text, /
/g), countMatches(chunk.text, /<\/blockquote>/g), ); assert.equal( countMatches(chunk.text, //g), countMatches(chunk.text, /<\/b>/g), ); assert.equal( countMatches(chunk.text, //g), countMatches(chunk.text, /<\/i>/g), ); assert.equal( countMatches(chunk.text, /
/g),
        countMatches(chunk.text, /<\/pre>/g),
      );
    }
  }
});

test("Nested lists stay out of code blocks", () => {
  const chunks = renderTelegramMessage(
    "- Level 1\n  - Level 2\n    - Level 3 with **bold** text",
    { mode: "markdown" },
  );
  assert.ok(chunks.length > 0);
  assert.equal(
    chunks.some((chunk) => chunk.text.includes("
")),
    false,
  );
  assert.equal(
    chunks.some((chunk) =>
      chunk.text.includes("- Level 3 with bold text"),
    ),
    true,
  );
});

test("Fenced code blocks preserve literal markdown", () => {
  const chunks = renderTelegramMessage('~~~ts\nconst value = "**raw**";\n~~~', {
    mode: "markdown",
  });
  assert.equal(chunks.length, 1);
  assert.match(chunks[0]?.text ?? "", /
/);
  assert.match(chunks[0]?.text ?? "", /\*\*raw\*\*/);
});

test("Underscores inside words do not become italic", () => {
  const chunks = renderTelegramMessage("Path: foo_bar_baz.txt and **bold**", {
    mode: "markdown",
  });
  assert.equal(chunks.length, 1);
  assert.equal((chunks[0]?.text ?? "").includes("bar"), false);
  assert.match(chunks[0]?.text ?? "", /bold<\/b>/);
});

test("Bold markdown can span soft line breaks", () => {
  const chunks = renderTelegramMessage(
    "Скорее: **архитектурно — да,\nпрактически — почти**.",
    { mode: "markdown" },
  );
  assert.equal(chunks.length, 1);
  assert.match(
    chunks[0]?.text ?? "",
    /Скорее: архитектурно — да,\nпрактически — почти<\/b>\./,
  );
});

test("Quoted nested lists stay in blockquote rendering", () => {
  const chunks = renderTelegramMessage(
    "> Quoted intro\n> - nested item\n>   - deeper item",
    { mode: "markdown" },
  );
  assert.equal(chunks.length, 1);
  assert.match(chunks[0]?.text ?? "", /
/); assert.match(chunks[0]?.text ?? "", /nested item/); assert.match(chunks[0]?.text ?? "", /-<\/code> nested item/); assert.equal((chunks[0]?.text ?? "").includes("
"), false);
});

test("Numbered lists use monospace numeric markers", () => {
  const chunks = renderTelegramMessage("1. first\n  2. second", {
    mode: "markdown",
  });
  assert.equal(chunks.length, 1);
  assert.match(chunks[0]?.text ?? "", /1\.<\/code> first/);
  assert.match(chunks[0]?.text ?? "", /2\.<\/code> second/);
});

test("Ordered task lists preserve numeric markers in rendering", () => {
  const markdown = "1. [x] first\n2. [ ] second";
  const chunks = renderTelegramMessage(markdown, {
    mode: "markdown",
  });
  assert.equal(chunks.length, 1);
  assert.match(
    chunks[0]?.text ?? "",
    /1\.<\/code> \[x\]<\/code> first/,
  );
  assert.match(
    chunks[0]?.text ?? "",
    /2\.<\/code> \[ \]<\/code> second/,
  );
});

test("Leading indentation on the first markdown line stays intact", () => {
  const markdown = "  - nested bullet\n    - nested child";
  const chunks = renderTelegramMessage(markdown, {
    mode: "markdown",
  });
  assert.equal(chunks.length, 1);
  assert.match(
    chunks[0]?.text ?? "",
    /^\u00A0\u00A0-<\/code> nested bullet/m,
  );
  assert.match(
    chunks[0]?.text ?? "",
    /^\u00A0\u00A0\u00A0\u00A0-<\/code> nested child/m,
  );
});

test("UI/compat rendering preserves multiple blank lines between blocks", () => {
  const markdown = "# Title\n\n\nParagraph\n\n\n> Quote";
  const chunks = renderTelegramMessage(markdown, {
    mode: "markdown",
  });
  assert.equal(chunks.length, 1);
  assert.match(
    chunks[0]?.text ?? "",
    /Title<\/b>\n\n\nParagraph\n\n\n
Quote<\/blockquote>/, ); }); test("UI/compat rendering preserves original blank-line spacing across block transitions", () => { const cases = [ { markdown: "Para\n\n\n```ts\nconst x = 1\n```", finalText: 'Para\n\n\n
const x = 1
', }, { markdown: "```ts\nconst x = 1\n```\n\n\nPara", finalText: '
const x = 1
\n\n\nPara', }, { markdown: "Para\n\n\n- item", finalText: "Para\n\n\n- item", }, { markdown: "Para\n\n\n> Quote", finalText: "Para\n\n\n
Quote
", }, ]; for (const testCase of cases) { const finalChunks = renderTelegramMessage(testCase.markdown, { mode: "markdown", }); assert.equal(finalChunks.length, 1); assert.equal(finalChunks[0]?.text ?? "", testCase.finalText); } }); test("Headings keep visible spacing before following code blocks even without source blank lines", () => { const markdown = "### Title\n```ts\nconst x = 1\n```"; const chunks = renderTelegramMessage(markdown, { mode: "markdown", }); assert.equal(chunks.length, 1); assert.match( chunks[0]?.text ?? "", /Title<\/b>\n\n
const x = 1<\/code><\/pre>/,
  );
});

test("Standalone checkbox-looking prose stays literal outside task lists", () => {
  const markdown = "Use [ ] as a placeholder and keep [x] literal";
  const chunks = renderTelegramMessage(markdown, {
    mode: "markdown",
  });
  assert.equal(chunks.length, 1);
  assert.equal((chunks[0]?.text ?? "").includes("[ ]"), false);
  assert.equal((chunks[0]?.text ?? "").includes("[x]"), false);
  assert.match(chunks[0]?.text ?? "", /Use \[ \] as a placeholder/);
  assert.match(chunks[0]?.text ?? "", /keep \[x\] literal/);
});

test("Nested blockquotes flatten into one Telegram blockquote with indentation", () => {
  const chunks = renderTelegramMessage("> outer\n>> inner\n>>> deepest", {
    mode: "markdown",
  });
  assert.equal(chunks.length, 1);
  assert.equal((chunks[0]?.text.match(/
/g) ?? []).length, 1); assert.equal((chunks[0]?.text.match(/<\/blockquote>/g) ?? []).length, 1); assert.match(chunks[0]?.text ?? "", /outer/); assert.match(chunks[0]?.text ?? "", /\u00A0\u00A0inner/); assert.match(chunks[0]?.text ?? "", /\u00A0\u00A0\u00A0\u00A0deepest/); }); test("UI/compat Markdown tables render as literal monospace blocks without outer side borders", () => { const chunks = renderTelegramMessage( "| Name | Value |\n| --- | --- |\n| **x** | `y` |", { mode: "markdown" }, ); assert.equal(chunks.length, 1); assert.match(chunks[0]?.text ?? "", /
/);
  assert.equal((chunks[0]?.text ?? "").includes("x"), false);
  assert.match(chunks[0]?.text ?? "", /Name\s+\|\s+Value/);
  assert.match(chunks[0]?.text ?? "", /x\s+\|\s+y/);
  assert.equal((chunks[0]?.text ?? "").includes("| Name |"), false);
  assert.equal((chunks[0]?.text ?? "").includes("| x |"), false);
});

test("Markdown table padding uses emoji grapheme display width", () => {
  const chunks = renderTelegramMessage(
    "| Icon | Name |\n| --- | --- |\n| 👩‍💻 | dev |\n| ✅ | done |\n| 1️⃣ | key |",
    { mode: "markdown" },
  );
  assert.equal(chunks.length, 1);
  assert.match(chunks[0]?.text ?? "", /Icon \| Name/);
  assert.match(chunks[0]?.text ?? "", /👩‍💻 {3}\| dev /u);
  assert.match(chunks[0]?.text ?? "", /✅ {3}\| done/u);
  assert.match(chunks[0]?.text ?? "", /1️⃣ {3}\| key /u);
});

test("Markdown table padding handles CJK and combining marks", () => {
  const chunks = renderTelegramMessage(
    "| Word | Note |\n| --- | --- |\n| é | acute |\n| 東京 | city |",
    { mode: "markdown" },
  );
  assert.equal(chunks.length, 1);
  assert.match(chunks[0]?.text ?? "", /Word \| Note /);
  assert.match(chunks[0]?.text ?? "", /é {4}\| acute/u);
  assert.match(chunks[0]?.text ?? "", /東京 \| city /u);
});

test("Links, code spans, and underscore-heavy text coexist safely", () => {
  const chunks = renderTelegramMessage(
    "See [docs](https://example.com), run `foo_bar()` and keep foo_bar.txt literal",
    { mode: "markdown" },
  );
  assert.equal(chunks.length, 1);
  assert.match(
    chunks[0]?.text ?? "",
    /docs<\/a>/,
  );
  assert.match(chunks[0]?.text ?? "", /foo_bar\(\)<\/code>/);
  assert.equal((chunks[0]?.text ?? "").includes("bar"), false);
});

test("HTML attributes are escaped or sanitized in generated Telegram markup", () => {
  const linkChunks = renderTelegramMessage(
    '[quoted]()',
    { mode: "markdown" },
  );
  assert.match(
    linkChunks[0]?.text ?? "",
    /quoted<\/a>/,
  );
  const codeChunks = renderTelegramMessage(
    '```ts"onclick=bad\nconst value = 1;\n```',
    { mode: "markdown" },
  );
  assert.match(
    codeChunks[0]?.text ?? "",
    /
/,
  );
  assert.equal((codeChunks[0]?.text ?? "").includes('"onclick'), false);
});

test("HTML mode chunks long messages below Telegram limits", () => {
  const chunks = renderTelegramMessage("x".repeat(5000), {
    mode: "html",
  });
  assert.ok(chunks.length > 1);
  for (const chunk of chunks) {
    assert.equal(chunk.parseMode, "HTML");
    assert.ok(chunk.text.length <= MAX_MESSAGE_LENGTH);
  }
});

test("HTML mode keeps tags balanced across long chunk boundaries", () => {
  const chunks = renderTelegramMessage(
    `
${"x".repeat(5000)}
`, { mode: "html" }, ); assert.ok(chunks.length > 1); for (const chunk of chunks) { assert.equal(chunk.parseMode, "HTML"); assert.ok(chunk.text.length <= MAX_MESSAGE_LENGTH); assert.equal( (chunk.text.match(/
/g) ?? []).length, (chunk.text.match(/<\/blockquote>/g) ?? []).length, ); assert.equal( (chunk.text.match(//g) ?? []).length, (chunk.text.match(/<\/b>/g) ?? []).length, ); } }); test("Links degrade or normalize safely across supported and unsupported markdown forms", () => { const markdown = [ "[**Bold** label](https://example.com/path)", "[Docs](https://example.com/a_(b))", '[Title](https://example.com/path "Tooltip")', "[Relative](./docs/README.md)", "[Ref][docs]", "", "[docs]: https://example.com/ref", "", "Footnote[^1]", "", "[^1]: Footnote body", ].join("\n"); const chunks = renderTelegramMessage(markdown, { mode: "markdown", }); assert.equal(chunks.length, 1); assert.match( chunks[0]?.text ?? "", /Bold label<\/a>/, ); assert.match( chunks[0]?.text ?? "", /Docs<\/a>/, ); assert.match( chunks[0]?.text ?? "", /Title<\/a>/, ); assert.equal( (chunks[0]?.text ?? "").includes(''), false, ); assert.match(chunks[0]?.text ?? "", /Relative/); assert.equal( (chunks[0]?.text ?? "").includes(''), false, ); assert.match(chunks[0]?.text ?? "", /\[Ref\]\[docs\]/); assert.match(chunks[0]?.text ?? "", /Footnote\[\^1\]/); assert.match(chunks[0]?.text ?? "", /\[\^1\]: Footnote body/); }); test("Long quoted blocks stay chunked with balanced blockquote tags", () => { const markdown = Array.from( { length: 500 }, (_, index) => `> quoted **${index}** line`, ).join("\n"); const chunks = renderTelegramMessage(markdown, { mode: "markdown", }); assert.ok(chunks.length > 1); for (const chunk of chunks) { assert.ok(chunk.text.length <= MAX_MESSAGE_LENGTH); assert.equal( (chunk.text.match(/
/g) ?? []).length, (chunk.text.match(/<\/blockquote>/g) ?? []).length, ); } }); test("Long UI/compat Markdown messages stay chunked below Telegram limits", () => { const markdown = Array.from( { length: 600 }, (_, index) => `- item **${index}**`, ).join("\n"); const chunks = renderTelegramMessage(markdown, { mode: "markdown", }); assert.ok(chunks.length > 1); for (const chunk of chunks) { assert.ok(chunk.text.length <= MAX_MESSAGE_LENGTH); assert.equal( (chunk.text.match(//g) ?? []).length, (chunk.text.match(/<\/b>/g) ?? []).length, ); } }); test("Long mixed links and code spans stay chunked with balanced inline tags", () => { const markdown = Array.from( { length: 450 }, (_, index) => `Paragraph ${index}: see [docs ${index}](https://example.com/${index}), run \`code_${index}()\`, and keep foo_bar_${index}.txt literal`, ).join("\n\n"); const chunks = renderTelegramMessage(markdown, { mode: "markdown", }); assert.ok(chunks.length > 1); for (const chunk of chunks) { assert.ok(chunk.text.length <= MAX_MESSAGE_LENGTH); assert.equal( (chunk.text.match(//g) ?? []).length, ); assert.equal( (chunk.text.match(//g) ?? []).length, (chunk.text.match(/<\/code>/g) ?? []).length, ); assert.equal((chunk.text ?? "").includes("bar"), false); } }); test("Long multi-block markdown keeps quotes and code fences structurally balanced", () => { const markdown = Array.from({ length: 120 }, (_, index) => { return [ `## Section ${index}`, `> quoted **${index}** line`, `- item ${index}`, "```ts", `const value_${index} = "**raw**";`, "```", ].join("\n"); }).join("\n\n"); const chunks = renderTelegramMessage(markdown, { mode: "markdown", }); assert.ok(chunks.length > 1); for (const chunk of chunks) { assert.ok(chunk.text.length <= MAX_MESSAGE_LENGTH); assert.equal( (chunk.text.match(/
/g) ?? []).length, (chunk.text.match(/<\/blockquote>/g) ?? []).length, ); assert.equal( (chunk.text.match(/
<\/pre>/g) ?? []).length,
    );
  }
});

test("Chunked mixed block transitions keep quote and list structure balanced", () => {
  const markdown = Array.from({ length: 260 }, (_, index) => {
    return [
      `> quoted **${index}** intro`,
      `> continuation ${index}`,
      `- item ${index}`,
      `plain paragraph ${index} with [link](https://example.com/${index})`,
    ].join("\n");
  }).join("\n\n");
  const chunks = renderTelegramMessage(markdown, {
    mode: "markdown",
  });
  assert.ok(chunks.length > 1);
  for (const chunk of chunks) {
    assert.ok(chunk.text.length <= MAX_MESSAGE_LENGTH);
    assert.equal(
      (chunk.text.match(/
/g) ?? []).length, (chunk.text.match(/<\/blockquote>/g) ?? []).length, ); assert.equal( (chunk.text.match(//g) ?? []).length, ); } }); test("Chunked code fence transitions keep code blocks closed before following prose", () => { const markdown = Array.from({ length: 220 }, (_, index) => { return [ "```ts", `const block_${index} = "value_${index}";`, "```", `After code **${index}** and \`inline_${index}()\``, ].join("\n"); }).join("\n\n"); const chunks = renderTelegramMessage(markdown, { mode: "markdown", }); assert.ok(chunks.length > 1); for (const chunk of chunks) { assert.ok(chunk.text.length <= MAX_MESSAGE_LENGTH); assert.equal( (chunk.text.match(/
<\/pre>/g) ?? []).length,
    );
    assert.equal(
      (chunk.text.match(//g) ?? []).length,
      (chunk.text.match(/<\/code>/g) ?? []).length,
    );
  }
});

test("Long inline formatting paragraphs stay balanced across chunk boundaries", () => {
  const markdown = Array.from({ length: 500 }, (_, index) => {
    return `Segment ${index} keeps **bold_${index}** with \`code_${index}()\`, [link_${index}](https://example.com/${index}), and foo_bar_${index}.txt literal.`;
  }).join(" ");
  const chunks = renderTelegramMessage(markdown, {
    mode: "markdown",
  });
  assert.ok(chunks.length > 1);
  for (const chunk of chunks) {
    assert.ok(chunk.text.length <= MAX_MESSAGE_LENGTH);
    assert.equal(
      (chunk.text.match(//g) ?? []).length,
      (chunk.text.match(/<\/b>/g) ?? []).length,
    );
    assert.equal(
      (chunk.text.match(//g) ?? []).length,
    );
    assert.equal(
      (chunk.text.match(//g) ?? []).length,
      (chunk.text.match(/<\/code>/g) ?? []).length,
    );
    assert.equal(chunk.text.includes("bar"), false);
  }
});

test("Realistic Markdown fixtures render safely for UI/compat surfaces", () => {
  const fixtures = [
    [
      "Here is the UI action plan:",
      "",
      "1. [x] Snapshot the database",
      "2. [ ] Apply the patch with `pnpm db:migrate`",
      "3. [ ] Verify [Grafana](https://example.com/dashboards/db)",
      "",
      "> Note: keep `user_id` and `team_id` untouched.",
      "",
      "```sql",
      "ALTER TABLE users ADD COLUMN team_id uuid; -- **literal**",
      "```",
    ].join("\n"),
    [
      "### Comparison",
      "| Option | Pros | Cons |",
      "| --- | --- | --- |",
      "| A | Fast | More foo_bar flags |",
      "| B | Safer | Requires rollback notes |",
      "",
      "Unsupported relative link: [local](./notes.md)",
      "Supported link: [docs](https://example.com/path?q=a_b)",
    ].join("\n"),
    [
      "The JSON payload should stay literal:",
      "",
      "```json",
      '{ "markdown": "**not bold**", "path": "foo_bar/baz" }',
      "```",
      "",
      "Nested quote from interactive compatibility message:",
      "> outer",
      ">> inner with **bold**",
    ].join("\n"),
  ];
  for (const markdown of fixtures) {
    const chunks = renderTelegramMessage(markdown, {
      mode: "markdown",
    });
    assert.ok(chunks.length > 0);
    for (const chunk of chunks) {
      assert.equal(chunk.parseMode, "HTML");
      assert.ok(chunk.text.length <= MAX_MESSAGE_LENGTH);
      assert.equal(
        countMatches(chunk.text, //g),
      );
      assert.equal(
        countMatches(chunk.text, /
/g), countMatches(chunk.text, /<\/blockquote>/g), ); assert.equal( countMatches(chunk.text, /
<\/pre>/g),
      );
      assert.equal(chunk.text.includes("bar"), false);
    }
  }
});

test("Chunked list, code, quote, and prose cycles stay balanced across transitions", () => {
  const markdown = Array.from({ length: 180 }, (_, index) => {
    return [
      `- list item **${index}**`,
      "```ts",
      `const cycle_${index} = "value_${index}";`,
      "```",
      `> quoted ${index} with [link](https://example.com/${index})`,
      `Plain paragraph ${index} with \`inline_${index}()\``,
    ].join("\n");
  }).join("\n\n");
  const chunks = renderTelegramMessage(markdown, {
    mode: "markdown",
  });
  assert.ok(chunks.length > 1);
  for (const chunk of chunks) {
    assert.ok(chunk.text.length <= MAX_MESSAGE_LENGTH);
    assert.equal(
      (chunk.text.match(/
<\/pre>/g) ?? []).length,
    );
    assert.equal(
      (chunk.text.match(/
/g) ?? []).length, (chunk.text.match(/<\/blockquote>/g) ?? []).length, ); assert.equal( (chunk.text.match(//g) ?? []).length, ); } });