/** * Tests for frontmatter-inference.ts — the zero-friction ingest pipeline. * * Validates that files without frontmatter get correct type, title, date, * source, and tags inferred from their filesystem path and content. */ import { describe, test, expect } from 'bun:test'; import { inferFrontmatter, extractDateFromFilename, extractTitleFromFilename, extractTitleFromHeading, serializeFrontmatter, applyInference, DIRECTORY_RULES, } from '../src/core/frontmatter-inference.ts'; // ── Date extraction ────────────────────────────────────────────────── describe('extractDateFromFilename', () => { test('extracts YYYY-MM-DD from date-prefixed filename', () => { expect(extractDateFromFilename('2010-04-13 Apr 13 founders mtg.md')).toBe('2010-04-13'); }); test('extracts date with dash separator', () => { expect(extractDateFromFilename('2024-01-30-therapy-session.md')).toBe('2024-01-30'); }); test('extracts date with underscore separator', () => { expect(extractDateFromFilename('2023-06-15_meeting-notes.md')).toBe('2023-06-15'); }); test('returns null for no-date filename', () => { expect(extractDateFromFilename('README.md')).toBe(null); }); test('returns null for filename with numbers but no date', () => { expect(extractDateFromFilename('chapter-1-intro.md')).toBe(null); }); }); // ── Title extraction ───────────────────────────────────────────────── describe('extractTitleFromFilename', () => { test('strips date prefix and cleans up', () => { expect(extractTitleFromFilename('2010-04-13 Apr 13 founders mtg.md')).toBe('Apr 13 founders mtg'); }); test('strips YYYY-MM-DD- prefix', () => { expect(extractTitleFromFilename('2024-01-30-therapy-session.md')).toBe('Therapy Session'); }); test('handles filename without date', () => { expect(extractTitleFromFilename('cognitive-distortions.md')).toBe('Cognitive Distortions'); }); test('preserves mixed case', () => { expect(extractTitleFromFilename('YC presidency.md')).toBe('YC presidency'); }); test('returns Untitled for empty result', () => { expect(extractTitleFromFilename('.md')).toBe('Untitled'); }); }); describe('extractTitleFromHeading', () => { test('extracts first # heading', () => { expect(extractTitleFromHeading('# Dhravya Shah\n\n> Founder of Supermemory')).toBe('Dhravya Shah'); }); test('ignores ## headings', () => { expect(extractTitleFromHeading('Some text\n## Not this\n# This one')).toBe('This one'); }); test('returns null when no heading found', () => { expect(extractTitleFromHeading('Just some text\nwithout headings')).toBe(null); }); test('looks within first 20 lines only', () => { const lines = Array(25).fill('text').join('\n') + '\n# Too Late'; expect(extractTitleFromHeading(lines)).toBe(null); }); }); // ── Core inference ─────────────────────────────────────────────────── describe('inferFrontmatter', () => { test('skips files that already have frontmatter', () => { const result = inferFrontmatter('people/alice.md', '---\ntitle: Alice\n---\n# Alice'); expect(result.skipped).toBe(true); }); test('Apple Notes: infers type, date, title, source', () => { const result = inferFrontmatter( 'Apple Notes/2010-04-13 Apr 13 founders mtg.md', 'Top priority', ); expect(result.type).toBe('apple-note'); expect(result.date).toBe('2010-04-13'); expect(result.title).toBe('Apr 13 founders mtg'); expect(result.source).toBe('apple-notes'); }); test('Apple Notes/YC: adds yc tag', () => { const result = inferFrontmatter( 'Apple Notes/YC/2022-08-04 Project 1783Y.md', 'Some content', ); expect(result.type).toBe('apple-note'); expect(result.tags).toContain('yc'); expect(result.date).toBe('2022-08-04'); }); test('Apple Notes/Politics: adds politics tag', () => { const result = inferFrontmatter( 'Apple Notes/Politics/2023-11-15 DA race notes.md', 'Some content', ); expect(result.tags).toContain('politics'); }); test('people/ directory: type person, title from heading', () => { const result = inferFrontmatter( 'people/dhravya-shah.md', '# Dhravya Shah\n\n> Founder of Supermemory', ); expect(result.type).toBe('person'); expect(result.title).toBe('Dhravya Shah'); }); test('people/ directory: falls back to filename when no heading', () => { const result = inferFrontmatter( 'people/john-doe.md', 'Some text without a heading', ); expect(result.type).toBe('person'); expect(result.title).toBe('John Doe'); }); test('personal/therapy: infers therapy-session type with date', () => { const result = inferFrontmatter( 'personal/therapy/jan/2024-01-30.md', 'Session notes...', ); expect(result.type).toBe('therapy-session'); expect(result.date).toBe('2024-01-30'); expect(result.source).toBe('therapy'); }); test('personal/reflections: infers reflection type, title from heading', () => { const result = inferFrontmatter( 'personal/reflections/cognitive-distortions.md', '# Cognitive Distortions\n\nA list of common...', ); expect(result.type).toBe('reflection'); expect(result.title).toBe('Cognitive Distortions'); }); test('writing/essays: infers essay type', () => { const result = inferFrontmatter( 'writing/essays/2024-03-15-on-being-remembered.md', '# On Being Remembered Forever\n\nSome thoughts...', ); expect(result.type).toBe('essay'); expect(result.title).toBe('On Being Remembered Forever'); expect(result.date).toBe('2024-03-15'); }); test('daily/calendar: infers calendar-index type', () => { const result = inferFrontmatter( 'daily/calendar/2026-01-15-yc-office-hours.md', '# Calendar Index\nSome calendar data', ); expect(result.type).toBe('calendar-index'); expect(result.source).toBe('calendar'); }); test('docs/runbooks: infers guide instead of catch-all note', () => { const result = inferFrontmatter( 'docs/runbooks/cron-management-runbook.md', '# Cron Management Runbook\n\nUse this when changing schedules.', ); expect(result.type).toBe('guide'); expect(result.tags).toContain('runbook'); expect(result.title).toBe('Cron Management Runbook'); }); test('docs/projects: infers project type', () => { const result = inferFrontmatter( 'docs/projects/eva-brain.md', '# Eva Brain\n\nProject notes.', ); expect(result.type).toBe('project'); expect(result.tags).toContain('project'); }); test('companies/ directory: type company', () => { const result = inferFrontmatter( 'companies/stripe.md', '# Stripe\n\n> Online payments infrastructure', ); expect(result.type).toBe('company'); expect(result.title).toBe('Stripe'); }); test('unknown directory: catch-all remains note when explicitly used by callers', () => { const result = inferFrontmatter( 'random/some-file.md', '# My Random Notes\n\nStuff here', ); expect(result.type).toBe('note'); expect(result.title).toBe('My Random Notes'); }); test('handles empty content', () => { const result = inferFrontmatter('notes/empty.md', ''); expect(result.type).toBe('note'); expect(result.title).toBe('Empty'); }); }); // ── Serialization ──────────────────────────────────────────────────── describe('serializeFrontmatter', () => { test('generates valid YAML frontmatter', () => { const fm = serializeFrontmatter({ title: 'Apr 13 founders mtg', type: 'apple-note', date: '2010-04-13', source: 'apple-notes', tags: ['yc'], }); expect(fm).toContain('---'); expect(fm).toContain('title: Apr 13 founders mtg'); expect(fm).toContain('type: apple-note'); expect(fm).toContain('date: "2010-04-13"'); expect(fm).toContain('source: apple-notes'); // v0.37.9.0 — canonical single-quoted YAML flow. Aligns with // brain-writer's step 3a auto-fix output. Was double-quoted pre-v0.37.9.0. expect(fm).toContain(`tags: ['yc']`); }); test('tags with apostrophe fall back to double quotes', () => { const fm = serializeFrontmatter({ title: 'fashion note', type: 'note', tags: ["Men's Fashion", 'yc'], }); // Apostrophe item keeps double quotes (JSON.stringify); clean item uses single. expect(fm).toContain(`tags: ["Men's Fashion", 'yc']`); }); test('quotes title with special chars', () => { const fm = serializeFrontmatter({ title: 'What\'s the deal: a "primer"', type: 'note', }); expect(fm).toContain('title: "What\'s the deal: a \\"primer\\""'); }); test('returns empty string for skipped files', () => { expect(serializeFrontmatter({ title: '', type: '', skipped: true })).toBe(''); }); test('omits optional fields when absent', () => { const fm = serializeFrontmatter({ title: 'Test', type: 'note' }); expect(fm).not.toContain('date'); expect(fm).not.toContain('source'); expect(fm).not.toContain('tags'); }); }); // ── Integration ────────────────────────────────────────────────────── describe('applyInference', () => { test('prepends frontmatter to content without it', () => { const { content, inferred } = applyInference( 'people/alice-smith.md', '# Alice Smith\n\n> Founder of FooBar', ); expect(content).toMatch(/^---\n/); expect(content).toContain('type: person'); expect(content).toContain('title: Alice Smith'); expect(content).toContain('# Alice Smith'); expect(inferred.skipped).toBeUndefined(); }); test('returns original content for files with frontmatter', () => { const original = '---\ntitle: Bob\n---\n# Bob'; const { content, inferred } = applyInference('people/bob.md', original); expect(content).toBe(original); expect(inferred.skipped).toBe(true); }); }); // ── Rules coverage ─────────────────────────────────────────────────── describe('DIRECTORY_RULES', () => { test('has a catch-all rule with empty prefix', () => { const catchAll = DIRECTORY_RULES.find(r => r.pathPrefix === ''); expect(catchAll).toBeDefined(); expect(catchAll!.type).toBe('note'); }); test('Apple Notes rules are more specific than the catch-all', () => { const appleRules = DIRECTORY_RULES.filter(r => r.pathPrefix.startsWith('apple notes/')); expect(appleRules.length).toBeGreaterThan(1); // subfolder rules + catch-all // Subfolder rules should come before the generic apple notes/ rule const ycIdx = DIRECTORY_RULES.findIndex(r => r.pathPrefix === 'apple notes/yc/'); const genericIdx = DIRECTORY_RULES.findIndex(r => r.pathPrefix === 'apple notes/'); expect(ycIdx).toBeLessThan(genericIdx); }); });