/**
* 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');
});
});