/** * File-Based Agent I/O Tests */ import { describe, it, expect, beforeEach, afterEach, afterAll } from 'vitest'; import { mkdirSync, writeFileSync, readFileSync, rmSync, existsSync } from 'node:fs'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; import { randomUUID } from 'crypto'; import { writeInputFiles, readSummary, readPhaseFiles, readTaskFiles, readDecisionFiles, readPageFiles, generateId, writeErrandManifest, } from './file-io.js'; import { buildErrandPrompt } from './prompts/index.js'; import type { Initiative, Phase, Task } from '../db/schema.js'; let testDir: string; beforeEach(() => { testDir = join(tmpdir(), `cw-file-io-test-${randomUUID()}`); mkdirSync(testDir, { recursive: true }); }); afterEach(() => { rmSync(testDir, { recursive: true, force: true }); }); describe('generateId', () => { it('returns a non-empty string', () => { const id = generateId(); expect(id).toBeTruthy(); expect(typeof id).toBe('string'); }); it('returns unique values', () => { const ids = new Set(Array.from({ length: 100 }, () => generateId())); expect(ids.size).toBe(100); }); }); describe('writeInputFiles', () => { it('writes initiative.md with frontmatter', async () => { const initiative: Initiative = { id: 'init-1', name: 'Test Initiative', status: 'active', branch: 'cw/test-initiative', executionMode: 'review_per_phase', qualityReview: false, createdAt: new Date('2026-01-01'), updatedAt: new Date('2026-01-02'), }; await writeInputFiles({ agentWorkdir: testDir, initiative }); const filePath = join(testDir, '.cw', 'input', 'initiative.md'); expect(existsSync(filePath)).toBe(true); }); it('writes phase.md with frontmatter', async () => { const phase = { id: 'phase-1', initiativeId: 'init-1', number: 1, name: 'Phase One', content: 'First phase', status: 'pending', mergeBase: null, createdAt: new Date(), updatedAt: new Date(), } as Phase; await writeInputFiles({ agentWorkdir: testDir, phase }); const filePath = join(testDir, '.cw', 'input', 'phase.md'); expect(existsSync(filePath)).toBe(true); }); it('writes task.md with frontmatter', async () => { const task = { id: 'task-1', name: 'Test Task', description: 'Do the thing', category: 'execute', type: 'auto', priority: 'medium', status: 'pending', order: 1, createdAt: new Date(), updatedAt: new Date(), } as Task; await writeInputFiles({ agentWorkdir: testDir, task }); const filePath = join(testDir, '.cw', 'input', 'task.md'); expect(existsSync(filePath)).toBe(true); }); it('writes pages to pages/ subdirectory', async () => { await writeInputFiles({ agentWorkdir: testDir, pages: [ { id: 'page-1', parentPageId: null, title: 'Root', content: null, sortOrder: 0 }, { id: 'page-2', parentPageId: 'page-1', title: 'Child', content: null, sortOrder: 1 }, ], }); expect(existsSync(join(testDir, '.cw', 'input', 'pages', 'page-1.md'))).toBe(true); expect(existsSync(join(testDir, '.cw', 'input', 'pages', 'page-2.md'))).toBe(true); }); it('handles empty options without error', async () => { await writeInputFiles({ agentWorkdir: testDir }); expect(existsSync(join(testDir, '.cw', 'input'))).toBe(true); }); it('writes context/index.json grouping tasks by phaseId', async () => { await writeInputFiles({ agentWorkdir: testDir, tasks: [ { id: 't1', name: 'Task A', phaseId: 'ph1', status: 'pending', category: 'execute', type: 'auto', priority: 'medium' } as Task, { id: 't2', name: 'Task B', phaseId: 'ph1', status: 'completed', category: 'execute', type: 'auto', priority: 'medium', summary: 'Done' } as Task, { id: 't3', name: 'Task C', phaseId: 'ph2', status: 'pending', category: 'research', type: 'auto', priority: 'high' } as Task, ], }); const indexPath = join(testDir, '.cw', 'input', 'context', 'index.json'); expect(existsSync(indexPath)).toBe(true); const index = JSON.parse(readFileSync(indexPath, 'utf-8')); expect(index.tasksByPhase.ph1).toHaveLength(2); expect(index.tasksByPhase.ph2).toHaveLength(1); expect(index.tasksByPhase.ph1[0]).toEqual({ file: 'context/tasks/t1.md', id: 't1', name: 'Task A', status: 'pending', }); }); it('does not write context/index.json when no tasks', async () => { await writeInputFiles({ agentWorkdir: testDir }); expect(existsSync(join(testDir, '.cw', 'input', 'context', 'index.json'))).toBe(false); }); }); describe('readSummary', () => { it('reads SUMMARY.md with frontmatter', async () => { const outputDir = join(testDir, '.cw', 'output'); mkdirSync(outputDir, { recursive: true }); writeFileSync( join(outputDir, 'SUMMARY.md'), `--- files_modified: - src/foo.ts - src/bar.ts --- Task completed successfully. Refactored the module. `, 'utf-8', ); const summary = await readSummary(testDir); expect(summary).not.toBeNull(); expect(summary!.body).toBe('Task completed successfully. Refactored the module.'); expect(summary!.filesModified).toEqual(['src/foo.ts', 'src/bar.ts']); }); it('returns null when SUMMARY.md does not exist', async () => { const summary = await readSummary(testDir); expect(summary).toBeNull(); }); it('handles SUMMARY.md without frontmatter', async () => { const outputDir = join(testDir, '.cw', 'output'); mkdirSync(outputDir, { recursive: true }); writeFileSync(join(outputDir, 'SUMMARY.md'), 'Just plain text\n', 'utf-8'); const summary = await readSummary(testDir); expect(summary).not.toBeNull(); expect(summary!.body).toBe('Just plain text'); expect(summary!.filesModified).toBeUndefined(); }); it('handles empty files_modified', async () => { const outputDir = join(testDir, '.cw', 'output'); mkdirSync(outputDir, { recursive: true }); writeFileSync( join(outputDir, 'SUMMARY.md'), `--- files_modified: [] --- Done. `, 'utf-8', ); const summary = await readSummary(testDir); expect(summary!.filesModified).toEqual([]); }); }); describe('readPhaseFiles', () => { it('reads phase files from phases/ directory', async () => { const phasesDir = join(testDir, '.cw', 'output', 'phases'); mkdirSync(phasesDir, { recursive: true }); writeFileSync( join(phasesDir, 'abc123.md'), `--- title: Database Schema dependencies: - xyz789 --- Create the user tables and auth schema. `, 'utf-8', ); const phases = await readPhaseFiles(testDir); expect(phases).toHaveLength(1); expect(phases[0].id).toBe('abc123'); expect(phases[0].title).toBe('Database Schema'); expect(phases[0].dependencies).toEqual(['xyz789']); expect(phases[0].body).toBe('Create the user tables and auth schema.'); }); it('returns empty array when directory does not exist', async () => { const phases = await readPhaseFiles(testDir); expect(phases).toEqual([]); }); it('handles phases with no dependencies', async () => { const phasesDir = join(testDir, '.cw', 'output', 'phases'); mkdirSync(phasesDir, { recursive: true }); writeFileSync( join(phasesDir, 'p1.md'), `--- title: Foundation --- Set up the base. `, 'utf-8', ); const phases = await readPhaseFiles(testDir); expect(phases[0].dependencies).toEqual([]); }); }); describe('readTaskFiles', () => { it('reads task files from tasks/ directory', async () => { const tasksDir = join(testDir, '.cw', 'output', 'tasks'); mkdirSync(tasksDir, { recursive: true }); writeFileSync( join(tasksDir, 'task-1.md'), `--- title: Implement login category: execute type: auto dependencies: - task-0 --- Build the login form and submit handler. `, 'utf-8', ); const tasks = await readTaskFiles(testDir); expect(tasks).toHaveLength(1); expect(tasks[0].id).toBe('task-1'); expect(tasks[0].title).toBe('Implement login'); expect(tasks[0].category).toBe('execute'); expect(tasks[0].type).toBe('auto'); expect(tasks[0].dependencies).toEqual(['task-0']); expect(tasks[0].body).toBe('Build the login form and submit handler.'); }); it('defaults category and type when missing', async () => { const tasksDir = join(testDir, '.cw', 'output', 'tasks'); mkdirSync(tasksDir, { recursive: true }); writeFileSync(join(tasksDir, 't1.md'), `---\ntitle: Minimal\n---\nDo it.\n`, 'utf-8'); const tasks = await readTaskFiles(testDir); expect(tasks[0].category).toBe('execute'); expect(tasks[0].type).toBe('auto'); }); it('returns empty array when directory does not exist', async () => { expect(await readTaskFiles(testDir)).toEqual([]); }); }); describe('readDecisionFiles', () => { it('reads decision files from decisions/ directory', async () => { const decisionsDir = join(testDir, '.cw', 'output', 'decisions'); mkdirSync(decisionsDir, { recursive: true }); writeFileSync( join(decisionsDir, 'd1.md'), `--- topic: Authentication decision: Use JWT reason: Stateless and scalable --- Additional context about the decision. `, 'utf-8', ); const decisions = await readDecisionFiles(testDir); expect(decisions).toHaveLength(1); expect(decisions[0].id).toBe('d1'); expect(decisions[0].topic).toBe('Authentication'); expect(decisions[0].decision).toBe('Use JWT'); expect(decisions[0].reason).toBe('Stateless and scalable'); expect(decisions[0].body).toBe('Additional context about the decision.'); }); it('returns empty array when directory does not exist', async () => { expect(await readDecisionFiles(testDir)).toEqual([]); }); }); describe('readPageFiles', () => { it('reads page files from pages/ directory', async () => { const pagesDir = join(testDir, '.cw', 'output', 'pages'); mkdirSync(pagesDir, { recursive: true }); writeFileSync( join(pagesDir, 'page-abc.md'), `--- title: Architecture Overview summary: Updated the overview section --- # Architecture New content for the page. `, 'utf-8', ); const pages = await readPageFiles(testDir); expect(pages).toHaveLength(1); expect(pages[0].pageId).toBe('page-abc'); expect(pages[0].title).toBe('Architecture Overview'); expect(pages[0].summary).toBe('Updated the overview section'); expect(pages[0].body).toBe('# Architecture\n\nNew content for the page.'); }); it('returns empty array when directory does not exist', async () => { expect(await readPageFiles(testDir)).toEqual([]); }); it('ignores non-.md files', async () => { const pagesDir = join(testDir, '.cw', 'output', 'pages'); mkdirSync(pagesDir, { recursive: true }); writeFileSync(join(pagesDir, 'readme.txt'), 'not a page', 'utf-8'); writeFileSync(join(pagesDir, 'page1.md'), '---\ntitle: Page 1\n---\nContent.\n', 'utf-8'); const pages = await readPageFiles(testDir); expect(pages).toHaveLength(1); }); }); describe('writeErrandManifest', () => { let errandTestDir: string; beforeEach(() => { errandTestDir = join(tmpdir(), `cw-errand-test-${randomUUID()}`); mkdirSync(errandTestDir, { recursive: true }); }); afterAll(() => { // no-op: beforeEach creates dirs, afterEach in outer scope cleans up }); it('writes manifest.json with correct shape', async () => { await writeErrandManifest({ agentWorkdir: errandTestDir, errandId: 'errand-abc', description: 'fix typo', branch: 'cw/errand/fix-typo-errandabc', projectName: 'my-project', agentId: 'agent-1', agentName: 'swift-owl', }); const manifestPath = join(errandTestDir, '.cw', 'input', 'manifest.json'); expect(existsSync(manifestPath)).toBe(true); const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8')); expect(manifest).toEqual({ errandId: 'errand-abc', agentId: 'agent-1', agentName: 'swift-owl', mode: 'errand', }); expect('files' in manifest).toBe(false); expect('contextFiles' in manifest).toBe(false); }); it('writes errand.md with correct YAML frontmatter', async () => { await writeErrandManifest({ agentWorkdir: errandTestDir, errandId: 'errand-abc', description: 'fix typo', branch: 'cw/errand/fix-typo-errandabc', projectName: 'my-project', agentId: 'agent-1', agentName: 'swift-owl', }); const errandMdPath = join(errandTestDir, '.cw', 'input', 'errand.md'); expect(existsSync(errandMdPath)).toBe(true); const content = readFileSync(errandMdPath, 'utf-8'); expect(content).toContain('id: errand-abc'); expect(content).toContain('description: fix typo'); expect(content).toContain('branch: cw/errand/fix-typo-errandabc'); expect(content).toContain('project: my-project'); }); it('writes expected-pwd.txt with agentWorkdir path', async () => { await writeErrandManifest({ agentWorkdir: errandTestDir, errandId: 'errand-abc', description: 'fix typo', branch: 'cw/errand/fix-typo-errandabc', projectName: 'my-project', agentId: 'agent-1', agentName: 'swift-owl', }); const pwdPath = join(errandTestDir, '.cw', 'expected-pwd.txt'); expect(existsSync(pwdPath)).toBe(true); const content = readFileSync(pwdPath, 'utf-8').trim(); expect(content).toBe(errandTestDir); }); it('creates input directory if it does not exist', async () => { const freshDir = join(tmpdir(), `cw-errand-fresh-${randomUUID()}`); mkdirSync(freshDir, { recursive: true }); await writeErrandManifest({ agentWorkdir: freshDir, errandId: 'errand-xyz', description: 'add feature', branch: 'cw/errand/add-feature-errandxyz', projectName: 'other-project', agentId: 'agent-2', agentName: 'brave-eagle', }); expect(existsSync(join(freshDir, '.cw', 'input', 'manifest.json'))).toBe(true); expect(existsSync(join(freshDir, '.cw', 'input', 'errand.md'))).toBe(true); expect(existsSync(join(freshDir, '.cw', 'expected-pwd.txt'))).toBe(true); rmSync(freshDir, { recursive: true, force: true }); }); }); describe('buildErrandPrompt', () => { it('includes the description in the output', () => { const result = buildErrandPrompt('fix typo in README'); expect(result).toContain('fix typo in README'); }); it('includes signal.json instruction', () => { const result = buildErrandPrompt('some change'); expect(result).toContain('signal.json'); expect(result).toContain('"status": "done"'); }); it('includes error signal format', () => { const result = buildErrandPrompt('some change'); expect(result).toContain('"status": "error"'); }); });