/** * NoteOutline parser tests. * * Phase 1A is parser-only: no CLI, MCP, Hub, search, index, memory, or * persistence behavior is expected here. */ import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; import { MAX_NOTE_OUTLINE_HEADINGS, MAX_NOTE_OUTLINE_INPUT_CHARS, NOTE_OUTLINE_SCHEMA, buildNoteOutline, buildNoteOutlineFromMarkdown, } from '../lib/note-outline.mjs'; describe('NoteOutline parser', () => { it('unit: returns the v1 schema, path, title, and ATX heading order', () => { const outline = buildNoteOutline({ path: 'inbox/example.md', frontmatter: { title: 'Example Note' }, body: '# Intro\n\n## Details\n\n### Next', }); assert.equal(outline.schema, NOTE_OUTLINE_SCHEMA); assert.equal(outline.path, 'inbox/example.md'); assert.equal(outline.title, 'Example Note'); assert.equal(outline.truncated, false); assert.deepEqual(outline.headings, [ { level: 1, text: 'Intro', id: 'h1-intro-0001' }, { level: 2, text: 'Details', id: 'h2-details-0002' }, { level: 3, text: 'Next', id: 'h3-next-0003' }, ]); }); it('unit: ignores YAML frontmatter as outline content', () => { const outline = buildNoteOutlineFromMarkdown( 'inbox/frontmatter.md', '---\ntitle: Frontmatter Title\nnote: "# not a heading"\n---\n\n# Body Heading\n' ); assert.equal(outline.title, 'Frontmatter Title'); assert.deepEqual(outline.headings, [ { level: 1, text: 'Body Heading', id: 'h1-body-heading-0001' }, ]); }); it('unit: supports Setext headings', () => { const outline = buildNoteOutline({ path: 'notes/setext.md', body: 'Title\n=====\n\nSubtitle\n--------\n\nText', frontmatter: {}, }); assert.deepEqual(outline.headings, [ { level: 1, text: 'Title', id: 'h1-title-0001' }, { level: 2, text: 'Subtitle', id: 'h2-subtitle-0002' }, ]); }); it('unit: ignores heading-looking text inside fenced and indented code blocks', () => { const outline = buildNoteOutline({ path: 'notes/code.md', frontmatter: {}, body: [ '# Real', '', '```', '# Not real', '```', '', ' ## Also not real', '', '## Real Child', ].join('\n'), }); assert.deepEqual(outline.headings, [ { level: 1, text: 'Real', id: 'h1-real-0001' }, { level: 2, text: 'Real Child', id: 'h2-real-child-0002' }, ]); }); it('unit: gives duplicate headings deterministic distinct IDs', () => { const note = { path: 'notes/duplicates.md', frontmatter: {}, body: '## Install\n\nText\n\n## Install\n\nMore', }; const first = buildNoteOutline(note); const second = buildNoteOutline(note); assert.deepEqual(first, second); assert.deepEqual(first.headings, [ { level: 2, text: 'Install', id: 'h2-install-0001' }, { level: 2, text: 'Install', id: 'h2-install-0002' }, ]); }); it('unit: extracts plain text from inline heading formatting', () => { const outline = buildNoteOutline({ path: 'notes/inline.md', frontmatter: {}, body: '## **Bold** [Link](https://example.com) `code`', }); assert.deepEqual(outline.headings, [ { level: 2, text: 'Bold Link code', id: 'h2-bold-link-code-0001' }, ]); }); it('security: treats HTML/script-looking heading content as plain text data', () => { const outline = buildNoteOutline({ path: 'notes/html.md', frontmatter: {}, body: '## ', }); assert.equal(outline.headings.length, 1); assert.equal(outline.headings[0].level, 2); assert.equal(outline.headings[0].text, ''); assert.equal(outline.headings[0].id, 'h2-script-alert-1-script-0001'); }); it('security: does not include body, snippets, frontmatter, or absolute paths', () => { const outline = buildNoteOutline({ path: 'private/secret.md', frontmatter: { title: 'Private', api_key: 'must-not-appear' }, body: '# Visible Heading\n\nSensitive body text must not appear.', }); const serialized = JSON.stringify(outline); assert.equal(Object.hasOwn(outline, 'body'), false); assert.equal(Object.hasOwn(outline, 'frontmatter'), false); assert.equal(Object.hasOwn(outline, 'snippet'), false); assert.equal(serialized.includes('must-not-appear'), false); assert.equal(serialized.includes('Sensitive body text'), false); assert.equal(serialized.includes('/Users/'), false); }); it('data-integrity: does not mutate the input note object', () => { const note = { path: 'notes/immutable.md', frontmatter: { title: 'Immutable' }, body: '# Heading', }; const before = JSON.stringify(note); buildNoteOutline(note); assert.equal(JSON.stringify(note), before); }); it('unit: returns an empty headings array for empty and no-heading notes', () => { assert.deepEqual( buildNoteOutline({ path: 'notes/empty.md', frontmatter: {}, body: '' }).headings, [] ); assert.deepEqual( buildNoteOutline({ path: 'notes/no-heading.md', frontmatter: {}, body: 'plain text' }).headings, [] ); }); it('unit: handles CRLF line endings', () => { const outline = buildNoteOutline({ path: 'notes/crlf.md', frontmatter: {}, body: '# First\r\n\r\n## Second\r\n', }); assert.deepEqual(outline.headings, [ { level: 1, text: 'First', id: 'h1-first-0001' }, { level: 2, text: 'Second', id: 'h2-second-0002' }, ]); }); it('stress: caps returned headings and sets truncated', () => { const maxHeadings = 3; const outline = buildNoteOutline( { path: 'notes/many.md', frontmatter: {}, body: Array.from({ length: 10 }, (_, index) => `## Heading ${index + 1}`).join('\n\n'), }, { maxHeadings } ); assert.equal(outline.headings.length, maxHeadings); assert.equal(outline.truncated, true); assert.deepEqual(outline.headings.map((heading) => heading.text), [ 'Heading 1', 'Heading 2', 'Heading 3', ]); }); it('stress: rejects input above the configured character cap', () => { assert.throws( () => buildNoteOutline( { path: 'notes/huge.md', frontmatter: {}, body: 'x'.repeat(11), }, { maxInputChars: 10 } ), /exceeds/ ); }); it('performance: parses a large but bounded heading set quickly', () => { const body = Array.from( { length: Math.min(1000, MAX_NOTE_OUTLINE_HEADINGS * 2) }, (_, index) => `## Heading ${index + 1}\n\nText ${index + 1}` ).join('\n\n'); const started = Date.now(); const outline = buildNoteOutline({ path: 'notes/perf.md', frontmatter: {}, body }); const elapsedMs = Date.now() - started; assert.equal(outline.headings.length, MAX_NOTE_OUTLINE_HEADINGS); assert.equal(outline.truncated, true); assert.ok(elapsedMs < 2000, `expected parser under 2s, got ${elapsedMs}ms`); }); it('unit: exposes documented default caps', () => { assert.equal(MAX_NOTE_OUTLINE_INPUT_CHARS, 1_000_000); assert.equal(MAX_NOTE_OUTLINE_HEADINGS, 500); }); });