refactor: Restructure monorepo to apps/server/ and apps/web/ layout
Move src/ → apps/server/ and packages/web/ → apps/web/ to adopt standard monorepo conventions (apps/ for runnable apps, packages/ for reusable libraries). Update all config files, shared package imports, test fixtures, and documentation to reflect new paths. Key fixes: - Update workspace config to ["apps/*", "packages/*"] - Update tsconfig.json rootDir/include for apps/server/ - Add apps/web/** to vitest exclude list - Update drizzle.config.ts schema path - Fix ensure-schema.ts migration path detection (3 levels up in dev, 2 levels up in dist) - Fix tests/integration/cli-server.test.ts import paths - Update packages/shared imports to apps/server/ paths - Update all docs/ files with new paths
This commit is contained in:
341
apps/server/agent/file-io.test.ts
Normal file
341
apps/server/agent/file-io.test.ts
Normal file
@@ -0,0 +1,341 @@
|
||||
/**
|
||||
* File-Based Agent I/O Tests
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { mkdirSync, writeFileSync, 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,
|
||||
} from './file-io.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', () => {
|
||||
const initiative: Initiative = {
|
||||
id: 'init-1',
|
||||
name: 'Test Initiative',
|
||||
status: 'active',
|
||||
mergeRequiresApproval: true,
|
||||
branch: 'cw/test-initiative',
|
||||
executionMode: 'review_per_phase',
|
||||
createdAt: new Date('2026-01-01'),
|
||||
updatedAt: new Date('2026-01-02'),
|
||||
};
|
||||
|
||||
writeInputFiles({ agentWorkdir: testDir, initiative });
|
||||
|
||||
const filePath = join(testDir, '.cw', 'input', 'initiative.md');
|
||||
expect(existsSync(filePath)).toBe(true);
|
||||
});
|
||||
|
||||
it('writes phase.md with frontmatter', () => {
|
||||
const phase = {
|
||||
id: 'phase-1',
|
||||
initiativeId: 'init-1',
|
||||
number: 1,
|
||||
name: 'Phase One',
|
||||
content: 'First phase',
|
||||
status: 'pending',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
} as Phase;
|
||||
|
||||
writeInputFiles({ agentWorkdir: testDir, phase });
|
||||
|
||||
const filePath = join(testDir, '.cw', 'input', 'phase.md');
|
||||
expect(existsSync(filePath)).toBe(true);
|
||||
});
|
||||
|
||||
it('writes task.md with frontmatter', () => {
|
||||
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;
|
||||
|
||||
writeInputFiles({ agentWorkdir: testDir, task });
|
||||
|
||||
const filePath = join(testDir, '.cw', 'input', 'task.md');
|
||||
expect(existsSync(filePath)).toBe(true);
|
||||
});
|
||||
|
||||
it('writes pages to pages/ subdirectory', () => {
|
||||
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', () => {
|
||||
writeInputFiles({ agentWorkdir: testDir });
|
||||
expect(existsSync(join(testDir, '.cw', 'input'))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('readSummary', () => {
|
||||
it('reads SUMMARY.md with frontmatter', () => {
|
||||
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 = 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', () => {
|
||||
const summary = readSummary(testDir);
|
||||
expect(summary).toBeNull();
|
||||
});
|
||||
|
||||
it('handles SUMMARY.md without frontmatter', () => {
|
||||
const outputDir = join(testDir, '.cw', 'output');
|
||||
mkdirSync(outputDir, { recursive: true });
|
||||
writeFileSync(join(outputDir, 'SUMMARY.md'), 'Just plain text\n', 'utf-8');
|
||||
|
||||
const summary = readSummary(testDir);
|
||||
expect(summary).not.toBeNull();
|
||||
expect(summary!.body).toBe('Just plain text');
|
||||
expect(summary!.filesModified).toBeUndefined();
|
||||
});
|
||||
|
||||
it('handles empty files_modified', () => {
|
||||
const outputDir = join(testDir, '.cw', 'output');
|
||||
mkdirSync(outputDir, { recursive: true });
|
||||
writeFileSync(
|
||||
join(outputDir, 'SUMMARY.md'),
|
||||
`---
|
||||
files_modified: []
|
||||
---
|
||||
Done.
|
||||
`,
|
||||
'utf-8',
|
||||
);
|
||||
|
||||
const summary = readSummary(testDir);
|
||||
expect(summary!.filesModified).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('readPhaseFiles', () => {
|
||||
it('reads phase files from phases/ directory', () => {
|
||||
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 = 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', () => {
|
||||
const phases = readPhaseFiles(testDir);
|
||||
expect(phases).toEqual([]);
|
||||
});
|
||||
|
||||
it('handles phases with no dependencies', () => {
|
||||
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 = readPhaseFiles(testDir);
|
||||
expect(phases[0].dependencies).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('readTaskFiles', () => {
|
||||
it('reads task files from tasks/ directory', () => {
|
||||
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 = 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', () => {
|
||||
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 = readTaskFiles(testDir);
|
||||
expect(tasks[0].category).toBe('execute');
|
||||
expect(tasks[0].type).toBe('auto');
|
||||
});
|
||||
|
||||
it('returns empty array when directory does not exist', () => {
|
||||
expect(readTaskFiles(testDir)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('readDecisionFiles', () => {
|
||||
it('reads decision files from decisions/ directory', () => {
|
||||
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 = 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', () => {
|
||||
expect(readDecisionFiles(testDir)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('readPageFiles', () => {
|
||||
it('reads page files from pages/ directory', () => {
|
||||
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 = 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', () => {
|
||||
expect(readPageFiles(testDir)).toEqual([]);
|
||||
});
|
||||
|
||||
it('ignores non-.md files', () => {
|
||||
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 = readPageFiles(testDir);
|
||||
expect(pages).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user