Files
msd-core/gsd-memory/tests/integration/qmd-wrapper.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

266 lines
7.6 KiB
TypeScript

import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { qmd } from '../../src/qmd.js';
import { execSync } from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
describe('QMD wrapper integration', () => {
let testDir: string;
let originalIsAvailable: typeof qmd.isAvailable;
beforeEach(() => {
// Create temp test directory with some markdown files
testDir = path.join(os.tmpdir(), `gsd-qmd-test-${Date.now()}`);
fs.mkdirSync(testDir, { recursive: true });
// Create test markdown files
fs.writeFileSync(
path.join(testDir, 'test1.md'),
`---
phase: 01
tags: [auth, jwt]
---
# Authentication Module
This module handles JWT authentication and session management.
We use jose for token signing and verification.
`
);
fs.writeFileSync(
path.join(testDir, 'test2.md'),
`---
phase: 02
tags: [api, rest]
---
# API Layer
RESTful API endpoints for user management.
Authentication is required for all protected routes.
`
);
fs.writeFileSync(
path.join(testDir, 'test3.md'),
`# Unrelated Content
This file contains unrelated content about database migrations.
`
);
originalIsAvailable = qmd.isAvailable.bind(qmd);
});
afterEach(() => {
// Cleanup
if (fs.existsSync(testDir)) {
fs.rmSync(testDir, { recursive: true });
}
qmd.isAvailable = originalIsAvailable;
});
describe('isAvailable', () => {
it('returns boolean indicating QMD availability', async () => {
const available = await qmd.isAvailable();
expect(typeof available).toBe('boolean');
});
});
describe('findMarkdownFiles', () => {
it('finds all markdown files recursively', () => {
const files = qmd.findMarkdownFiles(testDir);
expect(files.length).toBe(3);
expect(files.some(f => f.endsWith('test1.md'))).toBe(true);
expect(files.some(f => f.endsWith('test2.md'))).toBe(true);
expect(files.some(f => f.endsWith('test3.md'))).toBe(true);
});
it('excludes node_modules and hidden directories', () => {
// Create node_modules with markdown
const nmDir = path.join(testDir, 'node_modules');
fs.mkdirSync(nmDir, { recursive: true });
fs.writeFileSync(path.join(nmDir, 'package.md'), '# Package');
// Create hidden directory with markdown
const hiddenDir = path.join(testDir, '.hidden');
fs.mkdirSync(hiddenDir, { recursive: true });
fs.writeFileSync(path.join(hiddenDir, 'secret.md'), '# Secret');
const files = qmd.findMarkdownFiles(testDir);
expect(files.some(f => f.includes('node_modules'))).toBe(false);
expect(files.some(f => f.includes('.hidden'))).toBe(false);
});
it('handles nested directories', () => {
const nestedDir = path.join(testDir, 'docs', 'api');
fs.mkdirSync(nestedDir, { recursive: true });
fs.writeFileSync(path.join(nestedDir, 'nested.md'), '# Nested');
const files = qmd.findMarkdownFiles(testDir);
expect(files.some(f => f.endsWith('nested.md'))).toBe(true);
});
it('returns empty array for non-existent directory', () => {
const files = qmd.findMarkdownFiles('/nonexistent/path');
expect(files).toEqual([]);
});
});
describe('grepFallback', () => {
it('finds content matching query', async () => {
const results = await qmd.grepFallback({
query: 'authentication',
paths: [testDir],
limit: 10
});
expect(results.length).toBeGreaterThan(0);
expect(results.some(r => r.source.includes('test1.md'))).toBe(true);
expect(results.some(r => r.source.includes('test2.md'))).toBe(true);
});
it('returns results with relevance scores', async () => {
const results = await qmd.grepFallback({
query: 'jwt',
paths: [testDir]
});
expect(results.length).toBeGreaterThan(0);
for (const result of results) {
expect(result).toHaveProperty('relevance');
expect(typeof result.relevance).toBe('number');
expect(result.relevance).toBeGreaterThanOrEqual(0);
expect(result.relevance).toBeLessThanOrEqual(1);
}
});
it('returns content snippets around matches', async () => {
const results = await qmd.grepFallback({
query: 'jwt',
paths: [testDir]
});
expect(results.length).toBeGreaterThan(0);
for (const result of results) {
expect(result).toHaveProperty('content');
expect(result.content.toLowerCase()).toContain('jwt');
}
});
it('respects limit parameter', async () => {
const results = await qmd.grepFallback({
query: 'the', // Common word
paths: [testDir],
limit: 2
});
expect(results.length).toBeLessThanOrEqual(2);
});
it('sorts results by relevance', async () => {
const results = await qmd.grepFallback({
query: 'authentication',
paths: [testDir]
});
if (results.length > 1) {
for (let i = 1; i < results.length; i++) {
expect(results[i - 1].relevance).toBeGreaterThanOrEqual(results[i].relevance);
}
}
});
it('case insensitive search', async () => {
const results = await qmd.grepFallback({
query: 'JWT', // Uppercase
paths: [testDir]
});
expect(results.length).toBeGreaterThan(0);
expect(results.some(r => r.source.includes('test1.md'))).toBe(true);
});
it('returns empty array for no matches', async () => {
const results = await qmd.grepFallback({
query: 'xyznonexistent123',
paths: [testDir]
});
expect(results).toEqual([]);
});
it('marks context as grep', async () => {
const results = await qmd.grepFallback({
query: 'authentication',
paths: [testDir]
});
for (const result of results) {
expect(result.context).toBe('grep');
}
});
});
describe('search (integration)', () => {
it('uses grep fallback when QMD unavailable', async () => {
// Force QMD unavailable
vi.spyOn(qmd, 'isAvailable').mockResolvedValue(false);
const results = await qmd.search({
query: 'authentication',
paths: [testDir]
});
expect(results.length).toBeGreaterThan(0);
expect(results[0].context).toBe('grep');
});
it('uses grep fallback when no collection specified', async () => {
const results = await qmd.search({
query: 'authentication',
paths: [testDir]
// No collection specified
});
expect(results.length).toBeGreaterThan(0);
expect(results[0].context).toBe('grep');
});
});
describe('createCollection', () => {
it('returns graceful error when QMD unavailable', async () => {
vi.spyOn(qmd, 'isAvailable').mockResolvedValue(false);
const result = await qmd.createCollection({
name: 'test-collection',
paths: [testDir]
});
expect(result.success).toBe(false);
expect(result.reason).toBe('qmd_not_available');
});
});
describe('index', () => {
it('returns graceful error when QMD unavailable', async () => {
vi.spyOn(qmd, 'isAvailable').mockResolvedValue(false);
const result = await qmd.index('test-collection');
expect(result.success).toBe(false);
expect(result.reason).toBe('qmd_not_available');
});
});
describe('status', () => {
it('returns available: false when QMD unavailable', async () => {
vi.spyOn(qmd, 'isAvailable').mockResolvedValue(false);
const result = await qmd.status('test-collection');
expect(result.available).toBe(false);
});
});
});