/** * Unit tests for render.js partitioning, export, and bounded history helpers. */ import { test, describe } from 'node:test'; import assert from 'node:assert/strict'; import { partitionMarkdown, renderMarkdownSectionToFragment, SECTION_TARGET_BYTES, SECTION_HARD_LIMIT_BYTES, PATHOLOGICAL_BLOCK_LIMIT, getUtf8Bytes } from '../js/render.js'; import { serializeBounded } from '../js/history.js'; describe('Render module helpers and partition logic', () => { test('partitionMarkdown returns single section for normal sized text', () => { const text = '# Small Document\n\nThis is a short markdown text.'; const sections = partitionMarkdown(text); assert.equal(sections.length, 1); assert.equal(sections[0].text, text); }); test('partitionMarkdown splits large documents at heading/blank line boundaries', () => { // Generate text exceeding SECTION_TARGET_BYTES (~48 KiB) const part1 = '# Section One\n' + 'Content line.\n'.repeat(3500); const part2 = '# Section Two\n' + 'More content line.\n'.repeat(3500); const combined = `${part1}\n\n${part2}`; const sections = partitionMarkdown(combined); assert.ok(sections.length >= 2, 'Large text should be partitioned into at least 2 sections'); assert.ok(sections[0].text.includes('Section One')); assert.ok(sections[1].text.includes('Section Two')); }); test('partitionMarkdown forces split inside giant 12 MB code fence and re-opens fence', () => { // 12 MB code fence (runtime probe from H1) const line = 'const val = 1234567890;\n'; const linesCount = Math.ceil((12 * 1024 * 1024) / line.length); const codeBlock = '```typescript\n' + line.repeat(linesCount) + '```\n'; const sections = partitionMarkdown(codeBlock); assert.ok(sections.length > 100, `12 MB fence must be split into multiple sections, got ${sections.length}`); // Every section must stay strictly bounded for (let i = 0; i < sections.length; i++) { const secBytes = getUtf8Bytes(sections[i].text); assert.ok(secBytes <= SECTION_HARD_LIMIT_BYTES + 1024, `Section ${i} size ${secBytes} must be <= hard limit`); // Each section must be a valid closed code block const text = sections[i].text.trim(); assert.ok(text.startsWith('```typescript'), `Section ${i} must open with code fence`); assert.ok(text.endsWith('```'), `Section ${i} must close with code fence`); } }); test('partitionMarkdown splits dense 2.2 MB list with no blank lines into bounded sections', () => { // 2.2 MB dense list (runtime probe from H1) const listItem = '- Item with detailed specification text and metadata\n'; const itemsCount = Math.ceil((2.2 * 1024 * 1024) / listItem.length); const denseList = listItem.repeat(itemsCount); const sections = partitionMarkdown(denseList); assert.ok(sections.length >= 30, `2.2 MB dense list must be partitioned, got ${sections.length} sections`); for (let i = 0; i < sections.length; i++) { const secBytes = getUtf8Bytes(sections[i].text); assert.ok(secBytes <= SECTION_HARD_LIMIT_BYTES + 512, `Section ${i} size ${secBytes} must not exceed hard limit`); } }); test('partitionMarkdown partitions ~122 MB simulated answer into bounded sections', () => { // 122 MB simulated answer (from H1 runtime probe) // Construct in chunks to avoid single-line issues const chunk = '# Header\n' + 'Paragraph content line for analysis.\n'.repeat(1000); // ~37 KB const chunkBytes = getUtf8Bytes(chunk); const repetitions = Math.ceil((122 * 1024 * 1024) / chunkBytes); const hugeDoc = chunk.repeat(repetitions); const sections = partitionMarkdown(hugeDoc); assert.ok(sections.length >= 1000, `122 MB document must partition into >1000 sections, got ${sections.length}`); for (let i = 0; i < Math.min(20, sections.length); i++) { assert.ok(getUtf8Bytes(sections[i].text) <= SECTION_HARD_LIMIT_BYTES + 512); } }); test('partitionMarkdown splits large tables at row boundaries and repeats table headers', () => { const header = '| Column 1 | Column 2 | Column 3 |\n|---|---|---|\n'; const row = '| data alpha | data beta | data gamma |\n'; const table = header + row.repeat(3000); // ~120 KB const sections = partitionMarkdown(table); assert.ok(sections.length >= 2, 'Large table should partition into multiple sections'); // Section 2 should continue with repeated table header assert.ok(sections[1].text.includes('| Column 1 | Column 2 | Column 3 |'), 'Subsequent section must repeat table header'); assert.ok(sections[1].text.includes('|---|---|---|'), 'Subsequent section must repeat table separator'); }); test('partitionMarkdown handles multibyte UTF-8 byte boundary correctly', () => { // 4-byte UTF-8 emoji const emojiLine = '🚀'.repeat(500) + '\n'; // 2000 bytes per line const multibyteDoc = emojiLine.repeat(40); // 80,000 bytes const sections = partitionMarkdown(multibyteDoc); assert.ok(sections.length >= 2, 'Should partition multibyte document based on byte size'); for (const sec of sections) { assert.ok(getUtf8Bytes(sec.text) <= SECTION_HARD_LIMIT_BYTES + 2048); } }); test('partitionMarkdown falls back to bounded plain-text for single pathological line', () => { // Single line exceeding 128 KiB const hugeLine = 'A'.repeat(150 * 1024); const sections = partitionMarkdown(hugeLine); assert.ok(sections.length >= 1); assert.ok(sections[0].isPathologicalFallback); assert.ok(sections[0].text.includes('Block truncated for performance')); assert.ok(sections[0].text.length < 35 * 1024); }); test('renderMarkdownSectionToFragment fails safe when marked or DOMPurify absent', () => { // In Node test environment, window.marked and window.DOMPurify are undefined const rawUntrusted = ''; const fragment = renderMarkdownSectionToFragment(rawUntrusted); // Must return a safe DocumentFragment containing the render error notice, NEVER raw innerHTML assert.ok(fragment); if (typeof document !== 'undefined') { const notice = fragment.querySelector('.render-error-notice'); assert.ok(notice, 'Must contain error notice when parser/sanitizer is absent'); assert.ok(!fragment.querySelector('script'), 'Must never inject script tags'); assert.ok(!fragment.querySelector('img'), 'Must never inject img tags'); } }); test('serializeBounded bounds huge 10 MB objects to <= 32 KiB without memory exhaustion', () => { const hugeObject = { title: 'Large Tool Result', markdown: 'x'.repeat(10 * 1024 * 1024), // 10 MB string items: Array.from({ length: 50000 }, (_, i) => ({ id: i, name: `item_${i}` })) }; const serialized = serializeBounded(hugeObject, 32 * 1024); assert.ok(serialized.length <= 34 * 1024, `Serialized result length ${serialized.length} must be bounded around 32 KB`); assert.ok(serialized.includes('Result display bounded to 32.0 KB'), 'Must include truncation indicator'); }); });