note-outline.test.mjs file-level

at sha256:8 · View file ↗ · Intel ↗

History
1 files
1 commits
0 hotspots
0 🧊 dead
0 💥 blast risk
sha256:4 fix(security): pin patched transitive deps to clear Dependabot moderate… · aaronrene · Jun 11, 2026
1 /**
2 * NoteOutline parser tests.
3 *
4 * Phase 1A is parser-only: no CLI, MCP, Hub, search, index, memory, or
5 * persistence behavior is expected here.
6 */
7 import { describe, it } from 'node:test';
8 import assert from 'node:assert/strict';
9
10 import {
11 MAX_NOTE_OUTLINE_HEADINGS,
12 MAX_NOTE_OUTLINE_INPUT_CHARS,
13 NOTE_OUTLINE_SCHEMA,
14 buildNoteOutline,
15 buildNoteOutlineFromMarkdown,
16 } from '../lib/note-outline.mjs';
17
18 describe('NoteOutline parser', () => {
19 it('unit: returns the v1 schema, path, title, and ATX heading order', () => {
20 const outline = buildNoteOutline({
21 path: 'inbox/example.md',
22 frontmatter: { title: 'Example Note' },
23 body: '# Intro\n\n## Details\n\n### Next',
24 });
25
26 assert.equal(outline.schema, NOTE_OUTLINE_SCHEMA);
27 assert.equal(outline.path, 'inbox/example.md');
28 assert.equal(outline.title, 'Example Note');
29 assert.equal(outline.truncated, false);
30 assert.deepEqual(outline.headings, [
31 { level: 1, text: 'Intro', id: 'h1-intro-0001' },
32 { level: 2, text: 'Details', id: 'h2-details-0002' },
33 { level: 3, text: 'Next', id: 'h3-next-0003' },
34 ]);
35 });
36
37 it('unit: ignores YAML frontmatter as outline content', () => {
38 const outline = buildNoteOutlineFromMarkdown(
39 'inbox/frontmatter.md',
40 '---\ntitle: Frontmatter Title\nnote: "# not a heading"\n---\n\n# Body Heading\n'
41 );
42
43 assert.equal(outline.title, 'Frontmatter Title');
44 assert.deepEqual(outline.headings, [
45 { level: 1, text: 'Body Heading', id: 'h1-body-heading-0001' },
46 ]);
47 });
48
49 it('unit: supports Setext headings', () => {
50 const outline = buildNoteOutline({
51 path: 'notes/setext.md',
52 body: 'Title\n=====\n\nSubtitle\n--------\n\nText',
53 frontmatter: {},
54 });
55
56 assert.deepEqual(outline.headings, [
57 { level: 1, text: 'Title', id: 'h1-title-0001' },
58 { level: 2, text: 'Subtitle', id: 'h2-subtitle-0002' },
59 ]);
60 });
61
62 it('unit: ignores heading-looking text inside fenced and indented code blocks', () => {
63 const outline = buildNoteOutline({
64 path: 'notes/code.md',
65 frontmatter: {},
66 body: [
67 '# Real',
68 '',
69 '```',
70 '# Not real',
71 '```',
72 '',
73 ' ## Also not real',
74 '',
75 '## Real Child',
76 ].join('\n'),
77 });
78
79 assert.deepEqual(outline.headings, [
80 { level: 1, text: 'Real', id: 'h1-real-0001' },
81 { level: 2, text: 'Real Child', id: 'h2-real-child-0002' },
82 ]);
83 });
84
85 it('unit: gives duplicate headings deterministic distinct IDs', () => {
86 const note = {
87 path: 'notes/duplicates.md',
88 frontmatter: {},
89 body: '## Install\n\nText\n\n## Install\n\nMore',
90 };
91
92 const first = buildNoteOutline(note);
93 const second = buildNoteOutline(note);
94
95 assert.deepEqual(first, second);
96 assert.deepEqual(first.headings, [
97 { level: 2, text: 'Install', id: 'h2-install-0001' },
98 { level: 2, text: 'Install', id: 'h2-install-0002' },
99 ]);
100 });
101
102 it('unit: extracts plain text from inline heading formatting', () => {
103 const outline = buildNoteOutline({
104 path: 'notes/inline.md',
105 frontmatter: {},
106 body: '## **Bold** [Link](https://example.com) `code`',
107 });
108
109 assert.deepEqual(outline.headings, [
110 { level: 2, text: 'Bold Link code', id: 'h2-bold-link-code-0001' },
111 ]);
112 });
113
114 it('security: treats HTML/script-looking heading content as plain text data', () => {
115 const outline = buildNoteOutline({
116 path: 'notes/html.md',
117 frontmatter: {},
118 body: '## <script>alert(1)</script>',
119 });
120
121 assert.equal(outline.headings.length, 1);
122 assert.equal(outline.headings[0].level, 2);
123 assert.equal(outline.headings[0].text, '<script> alert(1) </script>');
124 assert.equal(outline.headings[0].id, 'h2-script-alert-1-script-0001');
125 });
126
127 it('security: does not include body, snippets, frontmatter, or absolute paths', () => {
128 const outline = buildNoteOutline({
129 path: 'private/secret.md',
130 frontmatter: { title: 'Private', api_key: 'must-not-appear' },
131 body: '# Visible Heading\n\nSensitive body text must not appear.',
132 });
133
134 const serialized = JSON.stringify(outline);
135 assert.equal(Object.hasOwn(outline, 'body'), false);
136 assert.equal(Object.hasOwn(outline, 'frontmatter'), false);
137 assert.equal(Object.hasOwn(outline, 'snippet'), false);
138 assert.equal(serialized.includes('must-not-appear'), false);
139 assert.equal(serialized.includes('Sensitive body text'), false);
140 assert.equal(serialized.includes('/Users/'), false);
141 });
142
143 it('data-integrity: does not mutate the input note object', () => {
144 const note = {
145 path: 'notes/immutable.md',
146 frontmatter: { title: 'Immutable' },
147 body: '# Heading',
148 };
149 const before = JSON.stringify(note);
150
151 buildNoteOutline(note);
152
153 assert.equal(JSON.stringify(note), before);
154 });
155
156 it('unit: returns an empty headings array for empty and no-heading notes', () => {
157 assert.deepEqual(
158 buildNoteOutline({ path: 'notes/empty.md', frontmatter: {}, body: '' }).headings,
159 []
160 );
161 assert.deepEqual(
162 buildNoteOutline({ path: 'notes/no-heading.md', frontmatter: {}, body: 'plain text' }).headings,
163 []
164 );
165 });
166
167 it('unit: handles CRLF line endings', () => {
168 const outline = buildNoteOutline({
169 path: 'notes/crlf.md',
170 frontmatter: {},
171 body: '# First\r\n\r\n## Second\r\n',
172 });
173
174 assert.deepEqual(outline.headings, [
175 { level: 1, text: 'First', id: 'h1-first-0001' },
176 { level: 2, text: 'Second', id: 'h2-second-0002' },
177 ]);
178 });
179
180 it('stress: caps returned headings and sets truncated', () => {
181 const maxHeadings = 3;
182 const outline = buildNoteOutline(
183 {
184 path: 'notes/many.md',
185 frontmatter: {},
186 body: Array.from({ length: 10 }, (_, index) => `## Heading ${index + 1}`).join('\n\n'),
187 },
188 { maxHeadings }
189 );
190
191 assert.equal(outline.headings.length, maxHeadings);
192 assert.equal(outline.truncated, true);
193 assert.deepEqual(outline.headings.map((heading) => heading.text), [
194 'Heading 1',
195 'Heading 2',
196 'Heading 3',
197 ]);
198 });
199
200 it('stress: rejects input above the configured character cap', () => {
201 assert.throws(
202 () =>
203 buildNoteOutline(
204 {
205 path: 'notes/huge.md',
206 frontmatter: {},
207 body: 'x'.repeat(11),
208 },
209 { maxInputChars: 10 }
210 ),
211 /exceeds/
212 );
213 });
214
215 it('performance: parses a large but bounded heading set quickly', () => {
216 const body = Array.from(
217 { length: Math.min(1000, MAX_NOTE_OUTLINE_HEADINGS * 2) },
218 (_, index) => `## Heading ${index + 1}\n\nText ${index + 1}`
219 ).join('\n\n');
220 const started = Date.now();
221 const outline = buildNoteOutline({ path: 'notes/perf.md', frontmatter: {}, body });
222 const elapsedMs = Date.now() - started;
223
224 assert.equal(outline.headings.length, MAX_NOTE_OUTLINE_HEADINGS);
225 assert.equal(outline.truncated, true);
226 assert.ok(elapsedMs < 2000, `expected parser under 2s, got ${elapsedMs}ms`);
227 });
228
229 it('unit: exposes documented default caps', () => {
230 assert.equal(MAX_NOTE_OUTLINE_INPUT_CHARS, 1_000_000);
231 assert.equal(MAX_NOTE_OUTLINE_HEADINGS, 500);
232 });
233 });