import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { mkdtemp, mkdir, writeFile, rm } from 'node:fs/promises'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; import { loadSpecDomains, readSpec, fileToSpecDomain, } from './lib/openlore-context-injector-helpers.ts'; // ─── Fixtures ───────────────────────────────────────────────────────────────── const SPEC_CONTENT = `# Auth Specification > Generated by openlore ## Purpose Handles authentication and session management. ## Requirements ### Requirement: Login The system SHALL authenticate users via JWT. #### Scenario: ValidCredentials - **GIVEN** valid credentials - **WHEN** login is called - **THEN** a JWT is returned `; async function createSpec(tmpDir: string, domain: string, content = SPEC_CONTENT) { const specDir = join(tmpDir, 'openspec', 'specs', domain); await mkdir(specDir, { recursive: true }); await writeFile(join(specDir, 'spec.md'), content); } // ─── loadSpecDomains ────────────────────────────────────────────────────────── describe('loadSpecDomains', () => { let tmpDir: string; beforeEach(async () => { tmpDir = await mkdtemp(join(tmpdir(), 'openlore-injector-test-')); }); afterEach(async () => { await rm(tmpDir, { recursive: true, force: true }); }); it('returns empty array when openspec/specs/ does not exist', () => { const result = loadSpecDomains(tmpDir); expect(result).toEqual([]); }); it('returns empty array when openspec/specs/ has no subdirectories', async () => { await mkdir(join(tmpDir, 'openspec', 'specs'), { recursive: true }); const result = loadSpecDomains(tmpDir); expect(result).toEqual([]); }); it('loads a single spec domain', async () => { await createSpec(tmpDir, 'auth'); const result = loadSpecDomains(tmpDir); expect(result).toHaveLength(1); expect(result[0].name).toBe('auth'); expect(result[0].purpose).toBe('Handles authentication and session management.'); expect(result[0].path).toContain('spec.md'); }); it('loads multiple spec domains', async () => { await createSpec(tmpDir, 'auth'); await createSpec(tmpDir, 'api', `# Api\n\n## Purpose\n\nProvides REST endpoints.\n`); const result = loadSpecDomains(tmpDir); expect(result).toHaveLength(2); const names = result.map((d) => d.name).sort(); expect(names).toEqual(['api', 'auth']); }); it('skips a domain with missing spec.md', async () => { await mkdir(join(tmpDir, 'openspec', 'specs', 'empty-domain'), { recursive: true }); await createSpec(tmpDir, 'auth'); const result = loadSpecDomains(tmpDir); expect(result).toHaveLength(1); expect(result[0].name).toBe('auth'); }); it('extracts purpose even with [PARTIAL SPEC] prefix', async () => { await createSpec( tmpDir, 'analyzer', `# Analyzer\n\n## Purpose\n\n[PARTIAL SPEC — file too large] Analyzes codebase.\n` ); const result = loadSpecDomains(tmpDir); expect(result[0].purpose).toBe('Analyzes codebase.'); }); it('returns empty purpose when ## Purpose section is missing', async () => { await createSpec(tmpDir, 'nopurpose', `# No Purpose\n\n## Requirements\n\nSomething.\n`); const result = loadSpecDomains(tmpDir); expect(result[0].purpose).toBe(''); }); }); // ─── readSpec ───────────────────────────────────────────────────────────────── describe('readSpec', () => { let tmpDir: string; beforeEach(async () => { tmpDir = await mkdtemp(join(tmpdir(), 'openlore-injector-test-')); }); afterEach(async () => { await rm(tmpDir, { recursive: true, force: true }); }); it('returns empty string when spec file does not exist', () => { const domain = { name: 'auth', path: join(tmpDir, 'nonexistent.md'), purpose: '' }; expect(readSpec(domain)).toBe(''); }); it('returns full content when under maxChars', async () => { await createSpec(tmpDir, 'auth'); const [domain] = loadSpecDomains(tmpDir); const result = readSpec(domain); expect(result).toBe(SPEC_CONTENT); }); it('truncates at a section boundary when over maxChars', async () => { const longContent = SPEC_CONTENT + '\n## Section2\n\n' + 'x'.repeat(5000); await createSpec(tmpDir, 'auth', longContent); const [domain] = loadSpecDomains(tmpDir); const result = readSpec(domain, 200); expect(result).toContain('spec truncated'); expect(result.length).toBeLessThan(longContent.length); }); it('truncates without section boundary when content has no ## headers after truncation point', async () => { const flatContent = 'a'.repeat(500); await createSpec(tmpDir, 'auth', flatContent); const [domain] = loadSpecDomains(tmpDir); const result = readSpec(domain, 100); expect(result).toContain('spec truncated'); }); }); // ─── fileToSpecDomain ───────────────────────────────────────────────────────── describe('fileToSpecDomain', () => { let tmpDir: string; const domains = [ { name: 'auth', path: '', purpose: '' }, { name: 'api', path: '', purpose: '' }, { name: 'analyzer', path: '', purpose: '' }, ]; beforeEach(async () => { tmpDir = await mkdtemp(join(tmpdir(), 'openlore-injector-test-')); await mkdir(join(tmpDir, '.openlore', 'analysis'), { recursive: true }); }); afterEach(async () => { await rm(tmpDir, { recursive: true, force: true }); }); it('returns null when no match is found', () => { const result = fileToSpecDomain('src/unknown/mystery.ts', domains, tmpDir); expect(result).toBeNull(); }); it('matches by domain name in file path (heuristic)', () => { const result = fileToSpecDomain('src/auth/middleware.ts', domains, tmpDir); expect(result).toBe('auth'); }); it('matches by parent directory name', () => { const result = fileToSpecDomain('src/api/routes.ts', domains, tmpDir); expect(result).toBe('api'); }); it('uses mapping.json when available (array form)', async () => { const mapping = [{ file: 'src/core/analyzer/engine.ts', domain: 'analyzer' }]; await writeFile(join(tmpDir, '.openlore', 'analysis', 'mapping.json'), JSON.stringify(mapping)); const result = fileToSpecDomain('src/core/analyzer/engine.ts', domains, tmpDir); expect(result).toBe('analyzer'); }); it('uses mapping.json when available (object form)', async () => { const mapping = { 'req-1': { filePath: 'src/api/server.ts', spec: 'api' }, }; await writeFile(join(tmpDir, '.openlore', 'analysis', 'mapping.json'), JSON.stringify(mapping)); const result = fileToSpecDomain('src/api/server.ts', domains, tmpDir); expect(result).toBe('api'); }); it('falls back to heuristic when mapping.json has no match', async () => { const mapping = [{ file: 'src/other.ts', domain: 'api' }]; await writeFile(join(tmpDir, '.openlore', 'analysis', 'mapping.json'), JSON.stringify(mapping)); const result = fileToSpecDomain('src/auth/service.ts', domains, tmpDir); expect(result).toBe('auth'); }); it('handles malformed mapping.json without throwing', async () => { await writeFile(join(tmpDir, '.openlore', 'analysis', 'mapping.json'), 'not valid json'); const result = fileToSpecDomain('src/auth/service.ts', domains, tmpDir); expect(result).toBe('auth'); }); });