Files
gbrain/test/markdown.test.ts

390 lines
14 KiB
TypeScript

import { describe, test, expect } from 'bun:test';
import { parseMarkdown, serializeMarkdown, splitBody } from '../src/core/markdown.ts';
describe('Markdown Parser', () => {
test('parses frontmatter + compiled_truth + timeline (explicit sentinel)', () => {
const md = `---
type: concept
title: Do Things That Don't Scale
tags: [startups, growth]
---
Paul Graham argues that startups should do unscalable things early on.
<!-- timeline -->
- 2013-07-01: Published on paulgraham.com
- 2024-11-15: Referenced in batch kickoff talk
`;
const parsed = parseMarkdown(md);
expect(parsed.type).toBe('concept');
expect(parsed.title).toBe("Do Things That Don't Scale");
expect(parsed.tags).toEqual(['startups', 'growth']);
expect(parsed.compiled_truth).toContain('unscalable things');
expect(parsed.timeline).toContain('Published on paulgraham.com');
expect(parsed.timeline).toContain('batch kickoff talk');
});
test('handles no timeline separator', () => {
const md = `---
type: concept
title: Superlinear Returns
---
Returns in many fields are superlinear.
Performance compounds over time.
`;
const parsed = parseMarkdown(md);
expect(parsed.compiled_truth).toContain('superlinear');
expect(parsed.timeline).toBe('');
});
test('handles empty body', () => {
const md = `---
type: concept
title: Empty Page
---
`;
const parsed = parseMarkdown(md);
expect(parsed.compiled_truth).toBe('');
expect(parsed.timeline).toBe('');
});
test('removes type, title, tags from frontmatter object', () => {
const md = `---
type: concept
title: Test
tags: [a, b]
custom_field: hello
---
Content
`;
const parsed = parseMarkdown(md);
expect(parsed.frontmatter).not.toHaveProperty('type');
expect(parsed.frontmatter).not.toHaveProperty('title');
expect(parsed.frontmatter).not.toHaveProperty('tags');
expect(parsed.frontmatter).toHaveProperty('custom_field', 'hello');
});
test('infers type from file path', () => {
const md = `---
title: Someone
---
Content
`;
const parsed = parseMarkdown(md, 'people/someone.md');
expect(parsed.type).toBe('person');
});
test('infers slug from file path', () => {
const md = `---
type: concept
title: Test
---
Content
`;
const parsed = parseMarkdown(md, 'concepts/do-things-that-dont-scale.md');
expect(parsed.slug).toBe('concepts/do-things-that-dont-scale');
});
// v0.20: BrainBench / native inbox-chat-calendar Page types. These 5 directory
// heuristics exercise PageType 'email | slack | calendar-event | note | meeting'
// which were added for amara-life-v1 ingest but are useful for any gbrain user
// ingesting an inbox dump, Slack export, iCal, meeting transcript, or daily notes.
test.each([
['emails/em-0001.md', 'email'],
['email/em-0001.md', 'email'],
['slack/sl-0037.md', 'slack'],
['cal/evt-0042.md', 'calendar-event'],
['calendar/evt-0042.md', 'calendar-event'],
['notes/2026-04-standup.md', 'note'],
['note/2026-04-standup.md', 'note'],
['meetings/mtg-0003.md', 'meeting'],
['meeting/mtg-0003.md', 'meeting'],
] as const)('infers type %s -> %s', (path, expectedType) => {
const md = `---\ntitle: Fixture\n---\nBody\n`;
const parsed = parseMarkdown(md, path);
expect(parsed.type).toBe(expectedType);
});
});
describe('splitBody', () => {
test('splits at <!-- timeline --> sentinel', () => {
const body = 'Above the line\n\n<!-- timeline -->\n\nBelow the line';
const { compiled_truth, timeline } = splitBody(body);
expect(compiled_truth).toContain('Above the line');
expect(timeline).toContain('Below the line');
});
test('splits at --- timeline --- sentinel', () => {
const body = 'Above the line\n\n--- timeline ---\n\nBelow the line';
const { compiled_truth, timeline } = splitBody(body);
expect(compiled_truth).toContain('Above the line');
expect(timeline).toContain('Below the line');
});
test('splits at --- when followed by ## Timeline heading', () => {
const body = 'Article content\n\n---\n\n## Timeline\n\n- 2024: Event happened';
const { compiled_truth, timeline } = splitBody(body);
expect(compiled_truth).toContain('Article content');
expect(timeline).toContain('## Timeline');
expect(timeline).toContain('Event happened');
});
test('splits at --- when followed by ## History heading', () => {
const body = 'Article content\n\n---\n\n## History\n\n- 2020: Founded';
const { compiled_truth, timeline } = splitBody(body);
expect(compiled_truth).toContain('Article content');
expect(timeline).toContain('## History');
});
test('does NOT split at plain --- (horizontal rule in article body)', () => {
const body = 'Above the line\n\n---\n\nBelow the line';
const { compiled_truth, timeline } = splitBody(body);
expect(compiled_truth).toBe(body);
expect(timeline).toBe('');
});
test('does NOT split on multiple plain --- horizontal rules', () => {
const body = 'Section 1\n\n---\n\nSection 2\n\n---\n\nSection 3';
const { compiled_truth, timeline } = splitBody(body);
expect(compiled_truth).toBe(body);
expect(timeline).toBe('');
});
test('returns all as compiled_truth if no sentinel', () => {
const body = 'Just some content\nWith multiple lines';
const { compiled_truth, timeline } = splitBody(body);
expect(compiled_truth).toBe(body);
expect(timeline).toBe('');
});
test('plain --- at end of content stays in compiled_truth', () => {
const body = 'Content here\n\n---\n';
const { compiled_truth, timeline } = splitBody(body);
expect(compiled_truth).toBe(body);
expect(timeline).toBe('');
});
test('<!-- timeline --> with content before and after', () => {
const body = '## Summary\n\nArticle summary here.\n\n---\n\nMore body content.\n\n<!-- timeline -->\n\n- 2024: Timeline entry';
const { compiled_truth, timeline } = splitBody(body);
expect(compiled_truth).toContain('## Summary');
expect(compiled_truth).toContain('More body content.');
expect(compiled_truth).not.toContain('Timeline entry');
expect(timeline).toContain('Timeline entry');
});
});
describe('serializeMarkdown', () => {
test('round-trips through parse and serialize (explicit sentinel)', () => {
const original = `---
type: concept
title: Do Things That Don't Scale
tags:
- startups
- growth
custom: value
---
Paul Graham argues that startups should do unscalable things early on.
<!-- timeline -->
- 2013-07-01: Published on paulgraham.com
`;
const parsed = parseMarkdown(original);
const serialized = serializeMarkdown(
parsed.frontmatter,
parsed.compiled_truth,
parsed.timeline,
{ type: parsed.type, title: parsed.title, tags: parsed.tags },
);
// Re-parse the serialized version
const reparsed = parseMarkdown(serialized);
expect(reparsed.type).toBe(parsed.type);
expect(reparsed.title).toBe(parsed.title);
expect(reparsed.compiled_truth).toBe(parsed.compiled_truth);
expect(reparsed.timeline).toBe(parsed.timeline);
expect(reparsed.frontmatter.custom).toBe('value');
});
});
describe('parseMarkdown edge cases', () => {
test('does NOT split on plain --- separators (horizontal rules stay in compiled_truth)', () => {
const md = `---
type: concept
title: Test
---
First section.
---
Second section.
---
Third section.`;
const parsed = parseMarkdown(md);
expect(parsed.compiled_truth).toContain('First section.');
expect(parsed.compiled_truth).toContain('Second section.');
expect(parsed.compiled_truth).toContain('Third section.');
expect(parsed.timeline).toBe('');
});
test('splits on <!-- timeline --> sentinel with horizontal rules in body', () => {
const md = `---
type: concept
title: Test
---
First section.
---
Second section.
<!-- timeline -->
- 2024: Timeline entry`;
const parsed = parseMarkdown(md);
expect(parsed.compiled_truth).toContain('First section.');
expect(parsed.compiled_truth).toContain('Second section.');
expect(parsed.compiled_truth).not.toContain('Timeline entry');
expect(parsed.timeline).toContain('Timeline entry');
});
test('handles frontmatter without type or title', () => {
const md = `---
custom_field: hello
---
Some content.`;
const parsed = parseMarkdown(md);
expect(parsed.type).toBeTruthy();
expect(parsed.compiled_truth.trim()).toBe('Some content.');
expect(parsed.frontmatter.custom_field).toBe('hello');
});
test('handles content with no frontmatter at all', () => {
const md = `Just plain text with no YAML.`;
const parsed = parseMarkdown(md);
expect(parsed.compiled_truth).toContain('Just plain text');
});
test('handles empty string', () => {
const parsed = parseMarkdown('');
expect(parsed.compiled_truth).toBe('');
expect(parsed.timeline).toBe('');
});
test('infers type from various directory paths', () => {
expect(parseMarkdown('', 'people/someone.md').type).toBe('person');
expect(parseMarkdown('', 'concepts/thing.md').type).toBe('concept');
expect(parseMarkdown('', 'companies/acme.md').type).toBe('company');
});
test('infers type from wiki subdirectory paths', () => {
expect(parseMarkdown('', 'tech/wiki/concepts/longevity-science.md').type).toBe('concept');
expect(parseMarkdown('', 'tech/wiki/guides/team-os-claude-code.md').type).toBe('guide');
expect(parseMarkdown('', 'tech/wiki/analysis/agi-timeline-debate.md').type).toBe('analysis');
expect(parseMarkdown('', 'tech/wiki/hardware/h100-vs-gb200-training-benchmarks.md').type).toBe('hardware');
expect(parseMarkdown('', 'tech/wiki/architecture/kb-infrastructure.md').type).toBe('architecture');
expect(parseMarkdown('', 'finance/wiki/analysis/polymarket-bot-automation-thesis.md').type).toBe('analysis');
expect(parseMarkdown('', 'personal/wiki/concepts/career-regrets-2026-framework.md').type).toBe('concept');
});
test('infers writing type from /writing/ paths', () => {
expect(parseMarkdown('', 'writing/post.md').type).toBe('writing');
expect(parseMarkdown('', 'projects/blog/writing/essay.md').type).toBe('writing');
});
});
// issue #1939 — js-yaml parses `title: 2024-06-01` as a Date and `title: 1458`
// as a number. The old `(frontmatter.title as string)` cast was a compile-time
// lie; at runtime downstream `.toLowerCase()` threw and wedged sync. Coercion
// must be non-throwing AND deterministic (UTC ISO for dates, no timezone drift).
describe('issue #1939 — non-string frontmatter coercion', () => {
test('date title coerces to its UTC ISO date string', () => {
const parsed = parseMarkdown('---\ntitle: 2024-06-01\n---\nbody\n', 'apple-notes/x.md');
expect(parsed.title).toBe('2024-06-01');
expect(typeof parsed.title).toBe('string');
});
test('number title coerces to its string form', () => {
const parsed = parseMarkdown('---\ntitle: 1458\n---\nbody\n', 'apple-notes/x.md');
expect(parsed.title).toBe('1458');
});
test('date title is timezone-independent (UTC) — repro file shape', () => {
// sources/apple-notes/YC/Talks YC/2023-04-25 1458.md style page.
const parsed = parseMarkdown('---\ntitle: 2023-04-25\n---\nnotes\n', 'apple-notes/2023-04-25 1458.md');
expect(parsed.title).toBe('2023-04-25'); // never "Mon Apr 24 2023 ...GMT-0700"
});
test('date/number slug + type coerce without throwing', () => {
const parsed = parseMarkdown('---\nslug: 2024-06-01\ntype: 2024\n---\nbody\n', 'x.md');
expect(typeof parsed.slug).toBe('string');
expect(parsed.slug).toBe('2024-06-01');
expect(typeof parsed.type).toBe('string');
});
test('missing/empty title falls back to inferred title (no throw)', () => {
const parsed = parseMarkdown('---\ntype: note\n---\nbody\n', 'people/alice-example.md');
expect(typeof parsed.title).toBe('string');
expect(parsed.title.length).toBeGreaterThan(0);
});
test('string title still passes through unchanged', () => {
const parsed = parseMarkdown('---\ntitle: A Normal Title\n---\nbody\n', 'x.md');
expect(parsed.title).toBe('A Normal Title');
});
});
// issue #2446 — when frontmatter has no `title:`, prefer the body's first H1
// over the slug/filename-humanized fallback. Slug-based imports (contacts,
// calendar) carry a correct `# Heading` but no frontmatter title; humanizing
// the slug leaks date/id tokens and loses casing (`Defalco` vs `DeFalco`).
describe('issue #2446 — body H1 fallback for missing frontmatter title', () => {
test('no frontmatter title uses the body H1, not the slug-humanized junk', () => {
const md = '---\ntype: person\n---\n\n# John DeFalco\n\nNotes about John.\n';
const parsed = parseMarkdown(md, 'people/contact-20170928-5-john-defalco.md');
expect(parsed.title).toBe('John DeFalco');
// The slug-derived junk title must NOT win.
expect(parsed.title).not.toBe('Contact 20170928 5 John Defalco');
});
test('no frontmatter title and no H1 falls back to the inferred slug title', () => {
const md = '---\ntype: note\n---\n\njust body prose, no heading\n';
const parsed = parseMarkdown(md, 'people/alice-example.md');
expect(parsed.title).toBe('Alice Example');
});
test('frontmatter title wins over a body H1 (no regression)', () => {
const md = '---\ntitle: Frontmatter Wins\n---\n\n# Body Heading\n\nbody\n';
const parsed = parseMarkdown(md, 'people/some-slug.md');
expect(parsed.title).toBe('Frontmatter Wins');
});
test('h2 is not treated as the title; first real H1 is used', () => {
const md = '---\ntype: note\n---\n\n## Subsection First\n\n# The Real Title\n\nbody\n';
const parsed = parseMarkdown(md, 'notes/x.md');
expect(parsed.title).toBe('The Real Title');
});
test('a # inside a fenced code block is not mistaken for the title', () => {
const md = '---\ntype: note\n---\n\n```sh\n# this is a shell comment, not a heading\n```\n\n# Actual Heading\n';
const parsed = parseMarkdown(md, 'notes/x.md');
expect(parsed.title).toBe('Actual Heading');
});
test('trailing closing hashes are stripped from the H1', () => {
const md = '---\ntype: note\n---\n\n# Closed ATX Heading #\n\nbody\n';
const parsed = parseMarkdown(md, 'notes/x.md');
expect(parsed.title).toBe('Closed ATX Heading');
});
});