feat(wave3): @mosaic/coord TypeScript orchestrator (#6)
Co-authored-by: Jason Woltje <jason@diversecanvas.com> Co-committed-by: Jason Woltje <jason@diversecanvas.com>
This commit was merged in pull request #6.
This commit is contained in:
64
packages/coord/tests/mission.test.ts
Normal file
64
packages/coord/tests/mission.test.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { promises as fs } from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { createMission, loadMission, missionFilePath } from '../src/mission.js';
|
||||
|
||||
describe('mission lifecycle', () => {
|
||||
it('creates and loads mission state files', async () => {
|
||||
const projectDir = await fs.mkdtemp(path.join(os.tmpdir(), 'coord-mission-'));
|
||||
|
||||
try {
|
||||
const mission = await createMission({
|
||||
name: 'Wave 3 Mission',
|
||||
projectPath: projectDir,
|
||||
milestones: ['Phase One', 'Phase Two'],
|
||||
qualityGates: 'pnpm lint && pnpm typecheck && pnpm test',
|
||||
description: 'Wave 3 implementation',
|
||||
});
|
||||
|
||||
expect(mission.id).toMatch(/^wave-3-mission-\d{8}$/);
|
||||
expect(mission.status).toBe('active');
|
||||
expect(mission.milestones).toHaveLength(2);
|
||||
|
||||
await expect(fs.stat(missionFilePath(projectDir, mission))).resolves.toBeDefined();
|
||||
await expect(fs.stat(path.join(projectDir, 'docs/TASKS.md'))).resolves.toBeDefined();
|
||||
await expect(
|
||||
fs.stat(path.join(projectDir, '.mosaic/orchestrator/mission.json')),
|
||||
).resolves.toBeDefined();
|
||||
|
||||
const loaded = await loadMission(projectDir);
|
||||
expect(loaded.id).toBe(mission.id);
|
||||
expect(loaded.name).toBe('Wave 3 Mission');
|
||||
expect(loaded.qualityGates).toBe('pnpm lint && pnpm typecheck && pnpm test');
|
||||
} finally {
|
||||
await fs.rm(projectDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('rejects inactive missions on load', async () => {
|
||||
const projectDir = await fs.mkdtemp(path.join(os.tmpdir(), 'coord-mission-inactive-'));
|
||||
|
||||
try {
|
||||
const mission = await createMission({
|
||||
name: 'Inactive Mission',
|
||||
projectPath: projectDir,
|
||||
});
|
||||
|
||||
const missionPath = missionFilePath(projectDir, mission);
|
||||
const payload = JSON.parse(await fs.readFile(missionPath, 'utf8')) as {
|
||||
status: string;
|
||||
};
|
||||
payload.status = 'inactive';
|
||||
await fs.writeFile(missionPath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
|
||||
|
||||
await expect(loadMission(projectDir)).rejects.toThrow(
|
||||
'Mission exists but is inactive',
|
||||
);
|
||||
} finally {
|
||||
await fs.rm(projectDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
74
packages/coord/tests/tasks-file.test.ts
Normal file
74
packages/coord/tests/tasks-file.test.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { parseTasksFile, writeTasksFile } from '../src/tasks-file.js';
|
||||
import type { MissionTask } from '../src/types.js';
|
||||
|
||||
describe('parseTasksFile', () => {
|
||||
it('normalizes legacy statuses from TASKS.md', () => {
|
||||
const content = [
|
||||
'# Tasks — Demo',
|
||||
'',
|
||||
'| id | status | milestone | description | pr | notes |',
|
||||
'|----|--------|-----------|-------------|----|-------|',
|
||||
'| T-1 | pending | phase-1 | First task | #10 | note a |',
|
||||
'| T-2 | completed | phase-1 | Second task | #11 | note b |',
|
||||
'| T-3 | in_progress | phase-2 | Third task | | |',
|
||||
'| T-4 | failed | phase-2 | Fourth task | | |',
|
||||
'',
|
||||
'trailing text ignored',
|
||||
].join('\n');
|
||||
|
||||
const tasks = parseTasksFile(content);
|
||||
|
||||
expect(tasks).toHaveLength(4);
|
||||
expect(tasks.map((task) => task.status)).toEqual([
|
||||
'not-started',
|
||||
'done',
|
||||
'in-progress',
|
||||
'blocked',
|
||||
]);
|
||||
expect(tasks.map((task) => task.rawStatus)).toEqual([
|
||||
'pending',
|
||||
'completed',
|
||||
'in_progress',
|
||||
'failed',
|
||||
]);
|
||||
expect(tasks[0]?.line).toBe(5);
|
||||
});
|
||||
});
|
||||
|
||||
describe('writeTasksFile', () => {
|
||||
it('round-trips parse/write output', () => {
|
||||
const tasks: MissionTask[] = [
|
||||
{
|
||||
id: 'W3-001',
|
||||
title: 'Implement parser',
|
||||
status: 'not-started',
|
||||
milestone: 'phase-1',
|
||||
pr: '#20',
|
||||
notes: 'pending',
|
||||
dependencies: [],
|
||||
},
|
||||
{
|
||||
id: 'W3-002',
|
||||
title: 'Implement runner',
|
||||
status: 'in-progress',
|
||||
milestone: 'phase-2',
|
||||
notes: 'active',
|
||||
dependencies: [],
|
||||
},
|
||||
];
|
||||
|
||||
const content = writeTasksFile(tasks);
|
||||
const reparsed = parseTasksFile(content);
|
||||
|
||||
expect(reparsed).toHaveLength(2);
|
||||
expect(reparsed.map((task) => task.id)).toEqual(['W3-001', 'W3-002']);
|
||||
expect(reparsed.map((task) => task.status)).toEqual([
|
||||
'not-started',
|
||||
'in-progress',
|
||||
]);
|
||||
expect(reparsed[0]?.title).toBe('Implement parser');
|
||||
expect(reparsed[1]?.milestone).toBe('phase-2');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user