/** * DocumentTree v0 pure builder tests. * * Phase 1A is builder-only: no file reads, CLI, MCP, hosted MCP, Hub, search, * index, vector, memory, persistence, summaries, PageIndex, or OCR behavior. */ import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; import { DOCUMENT_TREE_SCHEMA, buildDocumentTree, buildDocumentTreeFromMarkdown, buildDocumentTreeFromOutline, } from '../lib/document-tree.mjs'; function outline(overrides = {}) { return { schema: 'knowtation.note_outline/v1', path: 'notes/example.md', title: 'Example', headings: [], truncated: false, ...overrides, }; } describe('DocumentTree v0 pure builder', () => { it('unit: returns v0 schema, path, title, empty root, and truncation flag', () => { const tree = buildDocumentTreeFromOutline(outline()); assert.equal(tree.schema, DOCUMENT_TREE_SCHEMA); assert.equal(tree.path, 'notes/example.md'); assert.equal(tree.title, 'Example'); assert.equal(tree.truncated, false); assert.deepEqual(tree.root, { children: [] }); }); it('unit: nests deeper headings and keeps same-level headings as siblings', () => { const tree = buildDocumentTreeFromOutline( outline({ headings: [ { level: 1, text: 'Intro', id: 'h1-intro-0001' }, { level: 2, text: 'Background', id: 'h2-background-0002' }, { level: 2, text: 'Method', id: 'h2-method-0003' }, { level: 3, text: 'Step One', id: 'h3-step-one-0004' }, ], }) ); assert.deepEqual(tree.root.children, [ { id: 'h1-intro-0001', level: 1, text: 'Intro', children: [ { id: 'h2-background-0002', level: 2, text: 'Background', children: [], }, { id: 'h2-method-0003', level: 2, text: 'Method', children: [ { id: 'h3-step-one-0004', level: 3, text: 'Step One', children: [], }, ], }, ], }, ]); }); it('unit: closes ancestors on lower-level headings and allows skipped levels', () => { const tree = buildDocumentTreeFromOutline( outline({ headings: [ { level: 1, text: 'A', id: 'h1-a-0001' }, { level: 3, text: 'B', id: 'h3-b-0002' }, { level: 2, text: 'C', id: 'h2-c-0003' }, { level: 1, text: 'D', id: 'h1-d-0004' }, ], }) ); assert.deepEqual( tree.root.children.map((node) => ({ text: node.text, children: node.children.map((child) => ({ text: child.text, children: child.children.map((grandchild) => grandchild.text), })), })), [ { text: 'A', children: [ { text: 'B', children: [] }, { text: 'C', children: [] }, ], }, { text: 'D', children: [] }, ] ); }); it('unit: preserves empty heading text and duplicate heading IDs from NoteOutline', () => { const tree = buildDocumentTreeFromOutline( outline({ headings: [ { level: 2, text: '', id: 'h2-heading-0001' }, { level: 2, text: 'Install', id: 'h2-install-0002' }, { level: 2, text: 'Install', id: 'h2-install-0003' }, ], }) ); assert.deepEqual( tree.root.children.map((node) => ({ id: node.id, text: node.text })), [ { id: 'h2-heading-0001', text: '' }, { id: 'h2-install-0002', text: 'Install' }, { id: 'h2-install-0003', text: 'Install' }, ] ); }); it('unit: rejects invalid heading records', () => { assert.throws( () => buildDocumentTreeFromOutline( outline({ headings: [{ level: 7, text: 'Bad', id: 'h7-bad-0001' }] }) ), /heading.level/ ); assert.throws( () => buildDocumentTreeFromOutline( outline({ headings: [{ level: 1, text: 'Missing id' }] }) ), /heading.id/ ); }); it('data-integrity: does not mutate the input outline or heading records', () => { const input = outline({ headings: [ { level: 1, text: 'Root', id: 'h1-root-0001' }, { level: 2, text: 'Child', id: 'h2-child-0002' }, ], }); const before = JSON.stringify(input); buildDocumentTreeFromOutline(input); assert.equal(JSON.stringify(input), before); }); it('security: does not include body, snippets, frontmatter, vectors, or summaries', () => { const tree = buildDocumentTreeFromOutline( outline({ path: 'private/secret.md', title: 'Private', frontmatter: { api_key: 'must-not-appear' }, body: 'Sensitive body text must not appear.', snippet: 'Sensitive snippet must not appear.', vectorScore: 0.99, summary: 'Sensitive summary must not appear.', headings: [{ level: 1, text: '', id: 'h1-script-0001' }], }) ); const serialized = JSON.stringify(tree); assert.equal(Object.hasOwn(tree, 'body'), false); assert.equal(Object.hasOwn(tree, 'frontmatter'), false); assert.equal(Object.hasOwn(tree, 'snippet'), false); assert.equal(serialized.includes('must-not-appear'), false); assert.equal(serialized.includes('Sensitive body text'), false); assert.equal(serialized.includes('Sensitive snippet'), false); assert.equal(serialized.includes('Sensitive summary'), false); assert.equal(serialized.includes('vectorScore'), false); assert.equal(tree.root.children[0].text, ''); }); it('security: rejects absolute and traversal paths before returning a tree', () => { assert.throws( () => buildDocumentTreeFromOutline(outline({ path: '/Users/example/vault/secret.md' })), /vault-relative/ ); assert.throws( () => buildDocumentTreeFromOutline(outline({ path: '../secret.md' })), /escape vault/ ); assert.throws( () => buildDocumentTreeFromOutline(outline({ path: '\\Users\\example\\vault\\secret.md' })), /vault-relative/ ); }); it('stress: builds a capped heading list deterministically', () => { const headings = Array.from({ length: 500 }, (_, index) => ({ level: (index % 6) + 1, text: `Heading ${index + 1}`, id: `h${(index % 6) + 1}-heading-${String(index + 1).padStart(4, '0')}`, })); const input = outline({ headings, truncated: true }); const first = buildDocumentTreeFromOutline(input); const second = buildDocumentTreeFromOutline(input); assert.deepEqual(first, second); assert.equal(first.truncated, true); }); it('stress: caps untrusted direct outline heading input', () => { const headings = Array.from({ length: 501 }, (_, index) => ({ level: 1, text: `Heading ${index + 1}`, id: `h1-heading-${String(index + 1).padStart(4, '0')}`, })); const tree = buildDocumentTreeFromOutline(outline({ headings, truncated: false })); assert.equal(tree.root.children.length, 500); assert.equal(tree.root.children[499].text, 'Heading 500'); assert.equal(tree.truncated, true); }); it('performance: tree construction is linear for normal heading counts', () => { const headings = Array.from({ length: 500 }, (_, index) => ({ level: Math.min(6, (index % 4) + 1), text: `Heading ${index + 1}`, id: `h${Math.min(6, (index % 4) + 1)}-heading-${String(index + 1).padStart(4, '0')}`, })); const started = Date.now(); const tree = buildDocumentTreeFromOutline(outline({ headings })); const elapsedMs = Date.now() - started; assert.equal(tree.root.children.length > 0, true); assert.ok(elapsedMs < 200, `expected builder under 200ms, got ${elapsedMs}ms`); }); }); describe('DocumentTree v0 Markdown parser integration', () => { it('integration: builds a nested tree from Markdown using NoteOutline parsing semantics', () => { const tree = buildDocumentTree({ path: 'notes/markdown.md', frontmatter: { title: 'Markdown Tree' }, body: '# Intro\n\n### Context\n\n## Method\n\n# Outro', }); assert.equal(tree.schema, DOCUMENT_TREE_SCHEMA); assert.equal(tree.path, 'notes/markdown.md'); assert.equal(tree.title, 'Markdown Tree'); assert.deepEqual( tree.root.children.map((node) => ({ id: node.id, level: node.level, text: node.text, children: node.children.map((child) => child.text), })), [ { id: 'h1-intro-0001', level: 1, text: 'Intro', children: ['Context', 'Method'], }, { id: 'h1-outro-0004', level: 1, text: 'Outro', children: [], }, ] ); }); it('integration: parses raw Markdown frontmatter and derives the display title', () => { const tree = buildDocumentTreeFromMarkdown( 'projects/tree/frontmatter.md', '---\ntitle: Frontmatter Tree\napi_key: must-not-appear\n---\n\n# Root\n\n## Child\n' ); const serialized = JSON.stringify(tree); assert.equal(tree.title, 'Frontmatter Tree'); assert.deepEqual(tree.root.children, [ { id: 'h1-root-0001', level: 1, text: 'Root', children: [ { id: 'h2-child-0002', level: 2, text: 'Child', children: [], }, ], }, ]); assert.equal(serialized.includes('api_key'), false); assert.equal(serialized.includes('must-not-appear'), false); }); it('integration: keeps NoteOutline block-awareness for code blocks and Setext headings', () => { const tree = buildDocumentTree({ path: 'notes/block-aware.md', frontmatter: {}, body: [ 'Title', '=====', '', '```', '## Not a child', '```', '', 'Subtitle', '--------', ].join('\n'), }); assert.deepEqual(tree.root.children, [ { id: 'h1-title-0001', level: 1, text: 'Title', children: [ { id: 'h2-subtitle-0002', level: 2, text: 'Subtitle', children: [], }, ], }, ]); }); it('security: Markdown integration stays body-free and rejects unsafe paths', () => { const tree = buildDocumentTreeFromMarkdown( 'private/secret.md', '# Visible\n\nSensitive body text must not appear.' ); const serialized = JSON.stringify(tree); assert.equal(Object.hasOwn(tree, 'body'), false); assert.equal(Object.hasOwn(tree, 'frontmatter'), false); assert.equal(serialized.includes('Sensitive body text'), false); assert.throws( () => buildDocumentTreeFromMarkdown('../secret.md', '# Secret'), /escape vault/ ); }); it('stress: Markdown integration preserves truncation from NoteOutline caps', () => { const tree = buildDocumentTree( { path: 'notes/many-markdown.md', frontmatter: {}, body: Array.from({ length: 10 }, (_, index) => `## Heading ${index + 1}`).join('\n\n'), }, { maxHeadings: 3 } ); assert.equal(tree.truncated, true); assert.deepEqual( tree.root.children.map((node) => node.text), ['Heading 1', 'Heading 2', 'Heading 3'] ); }); });