import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; import { ProjectChronicleManager, ProjectProfile } from '../src/features/projectChronicle'; function makeProfile(root: string): ProjectProfile { const now = '2026-05-02T00:00:00.000Z'; return { projectId: 'test-project', projectName: 'Test Project', projectRoot: root, recordRoot: path.join(root, 'records', 'Test Project'), description: 'Test project records', corePurpose: 'Verify chronicle records', detailLevel: 'standard', createdAt: now, updatedAt: now }; } describe('ProjectChronicleManager', () => { let tempRoot: string; let manager: ProjectChronicleManager; let profile: ProjectProfile; beforeEach(() => { tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'chronicle-')); manager = new ProjectChronicleManager(); profile = makeProfile(tempRoot); }); afterEach(() => { fs.rmSync(tempRoot, { recursive: true, force: true }); }); it('creates the project record structure and seed files', () => { manager.ensureProject(profile); expect(fs.existsSync(path.join(profile.recordRoot, 'README.md'))).toBe(true); expect(fs.existsSync(path.join(profile.recordRoot, 'chronicle.config.json'))).toBe(true); expect(fs.existsSync(path.join(profile.recordRoot, 'project-profile.md'))).toBe(true); expect(fs.existsSync(path.join(profile.recordRoot, 'timeline.md'))).toBe(true); expect(fs.existsSync(path.join(profile.recordRoot, 'planning'))).toBe(true); expect(fs.existsSync(path.join(profile.recordRoot, 'decisions'))).toBe(true); expect(fs.existsSync(path.join(profile.recordRoot, 'development'))).toBe(true); expect(fs.existsSync(path.join(profile.recordRoot, 'bugs'))).toBe(true); const config = JSON.parse(fs.readFileSync(path.join(profile.recordRoot, 'chronicle.config.json'), 'utf8')); expect(config.projectId).toBe(profile.projectId); expect(config.recordRoot).toBe(profile.recordRoot); }); it('writes planning records without overwriting existing files', () => { const first = manager.writePlanning(profile, { featureName: 'Record Guard', purpose: 'Purpose', background: 'Background', userIntent: 'Intent', scope: ['Scope'], outOfScope: ['Out'], developmentDirection: 'Direction', dependencyStrategy: 'Dependency', expectedValue: 'Value', successCriteria: ['Success'], developerInstruction: 'Instruction', createdAt: '2026-05-02T00:00:00.000Z' }); const second = manager.writePlanning(profile, { featureName: 'Record Guard', purpose: 'Purpose', background: 'Background', userIntent: 'Intent', scope: ['Scope'], outOfScope: ['Out'], developmentDirection: 'Direction', dependencyStrategy: 'Dependency', expectedValue: 'Value', successCriteria: ['Success'], developerInstruction: 'Instruction', createdAt: '2026-05-02T00:00:00.000Z' }); expect(first.filePath).not.toBe(second.filePath); expect(path.basename(second.filePath)).toBe('2026-05-02_record-guard-2.md'); }); it('increments ADR and bug numbers from existing files', () => { manager.ensureProject(profile); fs.writeFileSync(path.join(profile.recordRoot, 'decisions', 'ADR-0007-existing.md'), '# Existing\n'); fs.writeFileSync(path.join(profile.recordRoot, 'bugs', 'BUG-0003-existing.md'), '# Existing\n'); expect(manager.nextAdrNumber(profile)).toBe(8); expect(manager.nextBugNumber(profile)).toBe(4); }); it('writes discussion, decision, bug, development, and retrospective records', () => { const discussion = manager.writeDiscussion(profile, { title: 'Question Intent', userRequest: 'Record why the AI asked a question.', interpretedIntent: 'Preserve reasoning behind clarification.', questions: [{ question: 'Which project should this be recorded under?', reason: 'Records must not be mixed across projects.', expectedInformation: 'The active project name and record path.', impactOnDecision: 'Determines where generated Markdown is written.' }], discussions: ['The project context must be explicit before writing files.'], decisions: [], createdAt: '2026-05-02T00:00:00.000Z' }); const decision = manager.writeDecision(profile, { title: 'Use Markdown Records', status: 'accepted', context: 'Records should be portable.', decision: 'Store records as Markdown.', reason: 'Markdown works without external services.', alternatives: ['External DB'], consequences: ['Easy to inspect in the repository.'], createdAt: '2026-05-02T00:00:00.000Z' }, manager.nextAdrNumber(profile)); const development = manager.writeDevelopmentLog(profile, { featureName: 'Chronicle Writer', purpose: 'Verify write paths.', implementationSummary: 'Wrote supported record types.', architecture: 'Manager -> Writer -> Markdown.', changedFiles: ['src/features/projectChronicle/index.ts'], dependencyNotes: 'No external services.', bugs: [], lessons: ['Keep record types explicit.'], createdAt: '2026-05-02T00:00:00.000Z' }); const bug = manager.writeBug(profile, { title: 'Missing Record Root', symptom: 'Write cannot proceed.', cause: 'Record root is empty.', fix: 'Validate profile before write.', prevention: 'Require selected project.', createdAt: '2026-05-02T00:00:00.000Z' }, manager.nextBugNumber(profile)); const retrospective = manager.writeRetrospective(profile, { title: 'Chronicle Iteration', summary: 'Record generation was expanded.', wentWell: ['Independent module stayed small.'], toImprove: ['Add richer UI later.'], nextActions: ['Capture events automatically.'], createdAt: '2026-05-02T00:00:00.000Z' }); for (const result of [discussion, decision, development, bug, retrospective]) { expect(fs.existsSync(result.filePath)).toBe(true); } expect(discussion.relativePath).toContain('discussions' + path.sep); expect(decision.relativePath).toContain('decisions' + path.sep); expect(development.relativePath).toContain('development' + path.sep); expect(bug.relativePath).toContain('bugs' + path.sep); expect(retrospective.relativePath).toContain('retrospectives' + path.sep); }); it('lists chronicle records across sections with relative paths', () => { manager.writePlanning(profile, { featureName: 'Record List', purpose: 'Purpose', background: 'Background', userIntent: 'Intent', scope: ['Scope'], outOfScope: ['Out'], developmentDirection: 'Direction', dependencyStrategy: 'Dependency', expectedValue: 'Value', successCriteria: ['Success'], developerInstruction: 'Instruction', createdAt: '2026-05-02T00:00:00.000Z' }); manager.writeBug(profile, { title: 'List Bug', symptom: 'Symptom', cause: 'Cause', fix: 'Fix', prevention: 'Prevention', createdAt: '2026-05-02T00:00:00.000Z' }, 1); const records = manager.listRecords(profile); expect(records.length).toBe(2); expect(records.map(record => record.relativePath)).toEqual( expect.arrayContaining([ path.join('planning', '2026-05-02_record-list.md'), path.join('bugs', 'BUG-0001-list-bug.md') ]) ); }); });