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>
142 lines
4.0 KiB
TypeScript
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');
|
|
});
|
|
});
|