import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { mkdtemp, rm } from 'node:fs/promises'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; import type { Skill, Person } from '../schemas/types'; import { getEntityDir, getEntityPath, readEntity, writeEntity, listEntities, deleteEntity, entityExists, } from './entity-files'; describe('entity-files', () => { let tempDir: string; beforeEach(async () => { tempDir = await mkdtemp(join(tmpdir(), 'kb-test-')); }); afterEach(async () => { await rm(tempDir, { recursive: true, force: true }); }); describe('getEntityDir', () => { it('maps person to kb/profiles/', () => { expect(getEntityDir('person', '/base')).toBe(join('/base', 'kb', 'profiles')); }); it('maps experience to kb/experiences/', () => { expect(getEntityDir('experience', '/base')).toBe(join('/base', 'kb', 'experiences')); }); it('maps skill to kb/skills/', () => { expect(getEntityDir('skill', '/base')).toBe(join('/base', 'kb', 'skills')); }); it('maps organization to kb/organizations/', () => { expect(getEntityDir('organization', '/base')).toBe(join('/base', 'kb', 'organizations')); }); it('maps project to kb/projects/', () => { expect(getEntityDir('project', '/base')).toBe(join('/base', 'kb', 'projects')); }); it('maps certification to kb/certifications/', () => { expect(getEntityDir('certification', '/base')).toBe(join('/base', 'kb', 'certifications')); }); it('maps tandem to kb/tandems/', () => { expect(getEntityDir('tandem', '/base')).toBe(join('/base', 'kb', 'tandems')); }); }); describe('getEntityPath', () => { it('constructs correct file path', () => { expect(getEntityPath('skill', 'python', '/base')).toBe(join('/base', 'kb', 'skills', 'python.yaml')); }); }); describe('writeEntity + readEntity round-trip', () => { const skill: Skill = { id: 'typescript', type: 'skill', created: '2025-01-15', modified: '2025-01-15', name: 'TypeScript', category: 'programming-language', level: 'expert', years: 5, }; it('writes and reads back an entity', async () => { await writeEntity(skill, tempDir); const result = await readEntity('skill', 'typescript', tempDir); expect(result).toEqual(skill); }); it('rejects invalid entities on write', async () => { const invalid = { id: 'bad', type: 'skill', created: '2025-01-15', modified: '2025-01-15' } as unknown as Skill; await expect(writeEntity(invalid, tempDir)).rejects.toThrow('Validation failed'); }); }); describe('readEntity errors', () => { it('throws descriptive error when entity not found', async () => { await expect(readEntity('skill', 'nonexistent', tempDir)).rejects.toThrow( /Entity not found: skill "nonexistent"/ ); }); }); describe('listEntities', () => { it('returns empty array for empty/missing directory', async () => { const ids = await listEntities('skill', tempDir); expect(ids).toEqual([]); }); it('lists entity IDs from directory', async () => { const skill1: Skill = { id: 'python', type: 'skill', created: '2025-01-15', modified: '2025-01-15', name: 'Python', category: 'programming-language', level: 'expert', }; const skill2: Skill = { id: 'rust', type: 'skill', created: '2025-01-15', modified: '2025-01-15', name: 'Rust', category: 'programming-language', level: 'intermediate', }; await writeEntity(skill1, tempDir); await writeEntity(skill2, tempDir); const ids = await listEntities('skill', tempDir); expect(ids.sort()).toEqual(['python', 'rust']); }); }); describe('deleteEntity', () => { it('deletes an existing entity file', async () => { const skill: Skill = { id: 'go-lang', type: 'skill', created: '2025-01-15', modified: '2025-01-15', name: 'Go', category: 'programming-language', level: 'advanced', }; await writeEntity(skill, tempDir); expect(await entityExists('skill', 'go-lang', tempDir)).toBe(true); await deleteEntity('skill', 'go-lang', tempDir); expect(await entityExists('skill', 'go-lang', tempDir)).toBe(false); }); it('throws descriptive error when entity not found', async () => { await expect(deleteEntity('skill', 'nonexistent', tempDir)).rejects.toThrow( /Entity not found: skill "nonexistent"/ ); }); }); describe('entityExists', () => { it('returns false for non-existent entity', async () => { expect(await entityExists('person', 'nobody', tempDir)).toBe(false); }); it('returns true for existing entity', async () => { const person: Person = { id: 'andre-knie', type: 'person', created: '2025-01-15', modified: '2025-09-01', name: { first: 'Andre', last: 'Knie', display: 'Andre Knie' }, }; await writeEntity(person, tempDir); expect(await entityExists('person', 'andre-knie', tempDir)).toBe(true); }); }); describe('multiple profiles coexist (Req 1.5)', () => { it('supports multiple person profiles in the same KB', async () => { const person1: Person = { id: 'andre-knie', type: 'person', created: '2025-01-15', modified: '2025-09-01', name: { first: 'Andre', last: 'Knie', display: 'Andre Knie' }, }; const person2: Person = { id: 'claudia-froldi', type: 'person', created: '2025-01-15', modified: '2025-09-01', name: { first: 'Claudia', last: 'Froldi', display: 'Claudia Froldi' }, }; await writeEntity(person1, tempDir); await writeEntity(person2, tempDir); const ids = await listEntities('person', tempDir); expect(ids.sort()).toEqual(['andre-knie', 'claudia-froldi']); const read1 = await readEntity('person', 'andre-knie', tempDir); const read2 = await readEntity('person', 'claudia-froldi', tempDir); expect(read1.id).toBe('andre-knie'); expect(read2.id).toBe('claudia-froldi'); }); }); });