Files
msd-core/gsd-memory/tests/extractors/frontmatter.test.ts
Lex Christopherson af7a057294 feat: add GSD Memory cross-project knowledge system
Implements MCP server for semantic search across GSD projects:

MCP Tools:
- gsd_memory_search: Search across all registered projects
- gsd_memory_decisions: Find decisions from SUMMARY/PROJECT.md
- gsd_memory_patterns: Find patterns from SUMMARY.md
- gsd_memory_pitfalls: Find pitfalls from RESEARCH.md
- gsd_memory_stack: Find tech stack entries
- gsd_memory_register: Register project with memory
- gsd_memory_index: Trigger indexing/update
- gsd_memory_status: Show system status

Architecture:
- QMD wrapper with grep fallback when QMD unavailable
- YAML frontmatter extractors for GSD document types
- Project registry at ~/.gsd/projects.json
- Auto-install via bin/install.js for Claude Code

Workflow integrations:
- new-project: Registers project with memory
- plan-phase: Indexes after planning
- execute-phase: Indexes after execution
- complete-milestone: Indexes milestone completion
- Researcher agents query memory before Context7

Tests: 117 passing (66 unit + 51 integration)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 13:13:22 -06:00

142 lines
4.0 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import { extractFrontmatter } from '../../src/extractors/frontmatter.js';
import { readFixture } from '../setup.js';
describe('frontmatter extractor', () => {
it('extracts YAML frontmatter from markdown', () => {
const md = `---
phase: 01-foundation
plan: "01"
tags: [auth, jwt]
---
# Content`;
const result = extractFrontmatter(md);
expect(result.phase).toBe('01-foundation');
expect(result.plan).toBe('01');
expect(result.tags).toEqual(['auth', 'jwt']);
});
it('parses numeric plan values as numbers', () => {
const md = `---
phase: 01-foundation
plan: 01
---
# Content`;
const result = extractFrontmatter(md);
expect(result.plan).toBe(1); // YAML parses 01 as number
});
it('returns empty object for no frontmatter', () => {
const result = extractFrontmatter('# Just content');
expect(result).toEqual({});
});
it('returns empty object for markdown starting with heading', () => {
const md = `# Title
Some content here.`;
const result = extractFrontmatter(md);
expect(result).toEqual({});
});
it('handles malformed YAML gracefully', () => {
const md = `---
invalid: [unclosed
broken: yaml: here
---
# Content`;
expect(() => extractFrontmatter(md)).not.toThrow();
const result = extractFrontmatter(md);
expect(result).toEqual({});
});
it('handles empty frontmatter', () => {
const md = `---
---
# Content`;
const result = extractFrontmatter(md);
expect(result).toEqual({});
});
it('extracts nested objects', () => {
const md = `---
tech-stack:
added:
- jose: 5.2.0
- bcrypt: 5.1.1
patterns:
- httpOnly cookies
---
# Content`;
const result = extractFrontmatter(md);
expect(result['tech-stack']).toBeDefined();
// YAML parses these as objects with key-value pairs
expect(result['tech-stack'].added).toHaveLength(2);
expect(result['tech-stack'].added[0]).toHaveProperty('jose', '5.2.0');
expect(result['tech-stack'].patterns).toContain('httpOnly cookies');
});
it('extracts requires/provides structure', () => {
const md = `---
requires:
- phase: 01-auth
provides: User model
provides:
- Dashboard component
- Real-time updates
---
# Content`;
const result = extractFrontmatter(md);
expect(result.requires).toHaveLength(1);
expect(result.requires[0].phase).toBe('01-auth');
expect(result.provides).toContain('Dashboard component');
});
it('extracts from real SUMMARY.md fixture', () => {
const summary = readFixture('sample-summary.md');
const result = extractFrontmatter(summary);
expect(result.phase).toBe('01-foundation');
expect(result.plan).toBe(1); // YAML parses 01 as number
expect(result.subsystem).toBe('auth');
expect(result.tags).toContain('jwt');
expect(result['key-decisions']).toBeDefined();
expect(result['key-decisions'].length).toBeGreaterThan(0);
});
it('extracts from real RESEARCH.md fixture', () => {
const research = readFixture('sample-research.md');
const result = extractFrontmatter(research);
// Research frontmatter structure (lowercase keys)
expect(result).toHaveProperty('researched');
expect(result).toHaveProperty('domain');
expect(result).toHaveProperty('confidence');
expect(result.confidence).toBe('HIGH');
});
it('handles multiline strings in frontmatter', () => {
const md = `---
description: |
This is a multiline
description that spans
multiple lines
---
# Content`;
const result = extractFrontmatter(md);
expect(result.description).toContain('multiline');
expect(result.description).toContain('multiple lines');
});
it('handles special characters in values', () => {
const md = `---
decision: "Used jose instead of jsonwebtoken - ESM native"
path: src/lib/auth.ts
---
# Content`;
const result = extractFrontmatter(md);
expect(result.decision).toBe('Used jose instead of jsonwebtoken - ESM native');
expect(result.path).toBe('src/lib/auth.ts');
});
});