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
386 lines
15 KiB
TypeScript
386 lines
15 KiB
TypeScript
/**
|
|
* E2E Tests for Detail Workflow
|
|
*
|
|
* Tests the complete detail workflow from phase through task creation:
|
|
* - Detail mode: Break phase into executable tasks
|
|
* - Q&A flow: Handle clarifying questions during detailing
|
|
* - Task persistence: Save child tasks from detail output
|
|
*
|
|
* Uses TestHarness from src/test/ for full system wiring.
|
|
*/
|
|
|
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
import { createTestHarness, type TestHarness } from '../index.js';
|
|
import type { AgentStoppedEvent, AgentWaitingEvent } from '../../events/types.js';
|
|
|
|
describe('Detail Workflow E2E', () => {
|
|
let harness: TestHarness;
|
|
|
|
beforeEach(() => {
|
|
harness = createTestHarness();
|
|
});
|
|
|
|
afterEach(() => {
|
|
harness.cleanup();
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
describe('spawn detail agent', () => {
|
|
it('should spawn agent in detail mode and complete with tasks', async () => {
|
|
vi.useFakeTimers();
|
|
|
|
// Setup: Create initiative -> phase -> plan
|
|
const initiative = await harness.createInitiative('Test Project');
|
|
const phases = await harness.createPhasesFromPlan(initiative.id, [
|
|
{ name: 'Phase 1' },
|
|
]);
|
|
const detailTask = await harness.createDetailTask(phases[0].id, 'Auth Plan', 'Implement authentication');
|
|
|
|
// Set detail scenario
|
|
harness.setArchitectDetailComplete('detailer', [
|
|
{ number: 1, name: 'Create schema', content: 'User table', type: 'auto', dependencies: [] },
|
|
{ number: 2, name: 'Create endpoint', content: 'Login API', type: 'auto', dependencies: [1] },
|
|
]);
|
|
|
|
// Spawn detail agent
|
|
const agent = await harness.caller.spawnArchitectDetail({
|
|
name: 'detailer',
|
|
phaseId: phases[0].id,
|
|
});
|
|
|
|
expect(agent.mode).toBe('detail');
|
|
|
|
// Advance timers for async completion
|
|
await harness.advanceTimers();
|
|
|
|
// Verify agent completed
|
|
const events = harness.getEmittedEvents('agent:stopped') as AgentStoppedEvent[];
|
|
expect(events).toHaveLength(1);
|
|
expect(events[0].payload.name).toBe('detailer');
|
|
expect(events[0].payload.reason).toBe('detail_complete');
|
|
});
|
|
|
|
it('should pause on questions and resume', async () => {
|
|
vi.useFakeTimers();
|
|
|
|
const initiative = await harness.createInitiative('Test Project');
|
|
const phases = await harness.createPhasesFromPlan(initiative.id, [
|
|
{ name: 'Phase 1' },
|
|
]);
|
|
const detailTask = await harness.createDetailTask(phases[0].id, 'Complex Plan');
|
|
|
|
// Set questions scenario
|
|
harness.setArchitectDetailQuestions('detailer', [
|
|
{ id: 'q1', question: 'How granular should tasks be?' },
|
|
]);
|
|
|
|
const agent = await harness.caller.spawnArchitectDetail({
|
|
name: 'detailer',
|
|
phaseId: phases[0].id,
|
|
});
|
|
|
|
await harness.advanceTimers();
|
|
|
|
// Verify agent is waiting for input
|
|
const waitingAgent = await harness.caller.getAgent({ name: 'detailer' });
|
|
expect(waitingAgent?.status).toBe('waiting_for_input');
|
|
|
|
// Verify paused on questions (emits agent:waiting, not agent:stopped)
|
|
const waitingEvents = harness.getEmittedEvents('agent:waiting') as AgentWaitingEvent[];
|
|
expect(waitingEvents).toHaveLength(1);
|
|
expect(waitingEvents[0].payload.questions).toHaveLength(1);
|
|
|
|
// Get pending questions
|
|
const pending = await harness.mockAgentManager.getPendingQuestions(agent.id);
|
|
expect(pending?.questions).toHaveLength(1);
|
|
expect(pending?.questions[0].question).toBe('How granular should tasks be?');
|
|
|
|
// Set completion scenario for resume
|
|
harness.setArchitectDetailComplete('detailer', [
|
|
{ number: 1, name: 'Task 1', content: 'Single task', type: 'auto', dependencies: [] },
|
|
]);
|
|
|
|
// Resume with answer
|
|
await harness.caller.resumeAgent({
|
|
name: 'detailer',
|
|
answers: { q1: 'Very granular' },
|
|
});
|
|
await harness.advanceTimers();
|
|
|
|
// Verify completed after resume
|
|
const finalAgent = await harness.caller.getAgent({ name: 'detailer' });
|
|
expect(finalAgent?.status).toBe('idle');
|
|
});
|
|
|
|
it('should handle multiple questions', async () => {
|
|
vi.useFakeTimers();
|
|
|
|
const initiative = await harness.createInitiative('Multi-Q Project');
|
|
const phases = await harness.createPhasesFromPlan(initiative.id, [
|
|
{ name: 'Phase 1' },
|
|
]);
|
|
const detailTask = await harness.createDetailTask(phases[0].id, 'Complex Plan');
|
|
|
|
// Set multiple questions scenario
|
|
harness.setArchitectDetailQuestions('detailer', [
|
|
{ id: 'q1', question: 'What task granularity?', options: [{ label: 'Fine' }, { label: 'Coarse' }] },
|
|
{ id: 'q2', question: 'Include checkpoints?' },
|
|
{ id: 'q3', question: 'Any blocking dependencies?' },
|
|
]);
|
|
|
|
const agent = await harness.caller.spawnArchitectDetail({
|
|
name: 'detailer',
|
|
phaseId: phases[0].id,
|
|
});
|
|
|
|
await harness.advanceTimers();
|
|
|
|
// Verify all questions received
|
|
const pending = await harness.mockAgentManager.getPendingQuestions(agent.id);
|
|
expect(pending?.questions).toHaveLength(3);
|
|
|
|
// Set completion scenario for resume
|
|
harness.setArchitectDetailComplete('detailer', [
|
|
{ number: 1, name: 'Task 1', content: 'First task', type: 'auto', dependencies: [] },
|
|
{ number: 2, name: 'Task 2', content: 'Second task', type: 'auto', dependencies: [1] },
|
|
{ number: 3, name: 'Verify', content: 'Verify all', type: 'checkpoint:human-verify', dependencies: [2] },
|
|
]);
|
|
|
|
// Resume with all answers
|
|
await harness.caller.resumeAgent({
|
|
name: 'detailer',
|
|
answers: {
|
|
q1: 'Fine',
|
|
q2: 'Yes, add human verification',
|
|
q3: 'Tasks 1 and 2 are sequential',
|
|
},
|
|
});
|
|
await harness.advanceTimers();
|
|
|
|
// Verify completed
|
|
const finalAgent = await harness.caller.getAgent({ name: 'detailer' });
|
|
expect(finalAgent?.status).toBe('idle');
|
|
});
|
|
});
|
|
|
|
describe('detail conflict detection', () => {
|
|
it('should reject if a detail agent is already running for the same phase', async () => {
|
|
vi.useFakeTimers();
|
|
|
|
const initiative = await harness.createInitiative('Test Project');
|
|
const phases = await harness.createPhasesFromPlan(initiative.id, [
|
|
{ name: 'Phase 1' },
|
|
]);
|
|
|
|
// Long-running detail agent
|
|
harness.setAgentScenario('detailer-1', { status: 'done', delay: 999999 });
|
|
|
|
await harness.caller.spawnArchitectDetail({
|
|
name: 'detailer-1',
|
|
phaseId: phases[0].id,
|
|
});
|
|
|
|
// Second detail for same phase should be rejected
|
|
await expect(
|
|
harness.caller.spawnArchitectDetail({
|
|
name: 'detailer-2',
|
|
phaseId: phases[0].id,
|
|
}),
|
|
).rejects.toThrow(/already running/);
|
|
});
|
|
|
|
it('should auto-dismiss stale detail agents before checking', async () => {
|
|
vi.useFakeTimers();
|
|
|
|
const initiative = await harness.createInitiative('Test Project');
|
|
const phases = await harness.createPhasesFromPlan(initiative.id, [
|
|
{ name: 'Phase 1' },
|
|
]);
|
|
|
|
// Detail agent that crashes immediately
|
|
harness.setAgentScenario('stale-detailer', { status: 'error', error: 'crashed' });
|
|
|
|
await harness.caller.spawnArchitectDetail({
|
|
name: 'stale-detailer',
|
|
phaseId: phases[0].id,
|
|
});
|
|
await harness.advanceTimers();
|
|
|
|
// New detail should succeed
|
|
harness.setArchitectDetailComplete('new-detailer', [
|
|
{ number: 1, name: 'Task 1', content: 'Do it', type: 'auto', dependencies: [] },
|
|
]);
|
|
|
|
const agent = await harness.caller.spawnArchitectDetail({
|
|
name: 'new-detailer',
|
|
phaseId: phases[0].id,
|
|
});
|
|
expect(agent.mode).toBe('detail');
|
|
});
|
|
|
|
it('should allow detail for different phases simultaneously', async () => {
|
|
vi.useFakeTimers();
|
|
|
|
const initiative = await harness.createInitiative('Test Project');
|
|
const phases = await harness.createPhasesFromPlan(initiative.id, [
|
|
{ name: 'Phase 1' },
|
|
{ name: 'Phase 2' },
|
|
]);
|
|
|
|
// Long-running agent on phase 1
|
|
harness.setAgentScenario('detailer-p1', { status: 'done', delay: 999999 });
|
|
await harness.caller.spawnArchitectDetail({
|
|
name: 'detailer-p1',
|
|
phaseId: phases[0].id,
|
|
});
|
|
|
|
// Detail on phase 2 should succeed
|
|
harness.setArchitectDetailComplete('detailer-p2', [
|
|
{ number: 1, name: 'Task 1', content: 'Do it', type: 'auto', dependencies: [] },
|
|
]);
|
|
|
|
const agent = await harness.caller.spawnArchitectDetail({
|
|
name: 'detailer-p2',
|
|
phaseId: phases[1].id,
|
|
});
|
|
expect(agent.mode).toBe('detail');
|
|
});
|
|
});
|
|
|
|
describe('task persistence', () => {
|
|
it('should create tasks from detail output', async () => {
|
|
const initiative = await harness.createInitiative('Test Project');
|
|
const phases = await harness.createPhasesFromPlan(initiative.id, [
|
|
{ name: 'Phase 1' },
|
|
]);
|
|
const detailTask = await harness.createDetailTask(phases[0].id, 'Auth Plan');
|
|
|
|
// Create tasks from detail output
|
|
await harness.caller.createChildTasks({
|
|
parentTaskId: detailTask.id,
|
|
tasks: [
|
|
{ number: 1, name: 'Schema', description: 'Create tables', type: 'auto', dependencies: [] },
|
|
{ number: 2, name: 'API', description: 'Create endpoints', type: 'auto', dependencies: [1] },
|
|
{ number: 3, name: 'Verify', description: 'Test flow', type: 'checkpoint:human-verify', dependencies: [2] },
|
|
],
|
|
});
|
|
|
|
// Verify tasks created
|
|
const tasks = await harness.getChildTasks(detailTask.id);
|
|
expect(tasks).toHaveLength(3);
|
|
expect(tasks[0].name).toBe('Schema');
|
|
expect(tasks[1].name).toBe('API');
|
|
expect(tasks[2].name).toBe('Verify');
|
|
expect(tasks[2].type).toBe('checkpoint:human-verify');
|
|
});
|
|
|
|
it('should handle all task types', async () => {
|
|
const initiative = await harness.createInitiative('Task Types Test');
|
|
const phases = await harness.createPhasesFromPlan(initiative.id, [
|
|
{ name: 'Phase 1' },
|
|
]);
|
|
const detailTask = await harness.createDetailTask(phases[0].id, 'Mixed Tasks');
|
|
|
|
// Create tasks with all types
|
|
await harness.caller.createChildTasks({
|
|
parentTaskId: detailTask.id,
|
|
tasks: [
|
|
{ number: 1, name: 'Auto Task', description: 'Automated work', type: 'auto' },
|
|
{ number: 2, name: 'Human Verify', description: 'Visual check', type: 'checkpoint:human-verify', dependencies: [1] },
|
|
{ number: 3, name: 'Decision', description: 'Choose approach', type: 'checkpoint:decision', dependencies: [2] },
|
|
{ number: 4, name: 'Human Action', description: 'Manual step', type: 'checkpoint:human-action', dependencies: [3] },
|
|
],
|
|
});
|
|
|
|
const tasks = await harness.getChildTasks(detailTask.id);
|
|
expect(tasks).toHaveLength(4);
|
|
expect(tasks[0].type).toBe('auto');
|
|
expect(tasks[1].type).toBe('checkpoint:human-verify');
|
|
expect(tasks[2].type).toBe('checkpoint:decision');
|
|
expect(tasks[3].type).toBe('checkpoint:human-action');
|
|
});
|
|
|
|
it('should create task dependencies', async () => {
|
|
const initiative = await harness.createInitiative('Dependencies Test');
|
|
const phases = await harness.createPhasesFromPlan(initiative.id, [
|
|
{ name: 'Phase 1' },
|
|
]);
|
|
const detailTask = await harness.createDetailTask(phases[0].id, 'Dependent Tasks');
|
|
|
|
// Create tasks with complex dependencies
|
|
await harness.caller.createChildTasks({
|
|
parentTaskId: detailTask.id,
|
|
tasks: [
|
|
{ number: 1, name: 'Task A', description: 'No deps', type: 'auto' },
|
|
{ number: 2, name: 'Task B', description: 'Depends on A', type: 'auto', dependencies: [1] },
|
|
{ number: 3, name: 'Task C', description: 'Depends on A', type: 'auto', dependencies: [1] },
|
|
{ number: 4, name: 'Task D', description: 'Depends on B and C', type: 'auto', dependencies: [2, 3] },
|
|
],
|
|
});
|
|
|
|
const tasks = await harness.getChildTasks(detailTask.id);
|
|
expect(tasks).toHaveLength(4);
|
|
|
|
// All tasks should be created with correct names
|
|
expect(tasks.map(t => t.name)).toEqual(['Task A', 'Task B', 'Task C', 'Task D']);
|
|
});
|
|
});
|
|
|
|
describe('full detail workflow', () => {
|
|
it('should complete initiative -> phase -> plan -> detail -> tasks workflow', async () => {
|
|
vi.useFakeTimers();
|
|
|
|
// 1. Create initiative
|
|
const initiative = await harness.createInitiative('Full Workflow Test');
|
|
|
|
// 2. Create phase
|
|
const phases = await harness.createPhasesFromPlan(initiative.id, [
|
|
{ name: 'Auth Phase' },
|
|
]);
|
|
|
|
// 3. Create plan
|
|
const detailTask = await harness.createDetailTask(phases[0].id, 'Auth Plan', 'Implement JWT auth');
|
|
|
|
// 4. Spawn detail agent
|
|
harness.setArchitectDetailComplete('detailer', [
|
|
{ number: 1, name: 'Create user schema', content: 'Define User model', type: 'auto', dependencies: [] },
|
|
{ number: 2, name: 'Implement JWT', content: 'Token generation', type: 'auto', dependencies: [1] },
|
|
{ number: 3, name: 'Protected routes', content: 'Middleware', type: 'auto', dependencies: [2] },
|
|
{ number: 4, name: 'Verify auth', content: 'Test login flow', type: 'checkpoint:human-verify', dependencies: [3] },
|
|
]);
|
|
|
|
await harness.caller.spawnArchitectDetail({
|
|
name: 'detailer',
|
|
phaseId: phases[0].id,
|
|
});
|
|
await harness.advanceTimers();
|
|
|
|
// 5. Verify agent completed
|
|
const events = harness.getEmittedEvents('agent:stopped') as AgentStoppedEvent[];
|
|
expect(events).toHaveLength(1);
|
|
expect(events[0].payload.reason).toBe('detail_complete');
|
|
|
|
// 6. Persist tasks (simulating what orchestrator would do after detail)
|
|
await harness.caller.createChildTasks({
|
|
parentTaskId: detailTask.id,
|
|
tasks: [
|
|
{ number: 1, name: 'Create user schema', description: 'Define User model', type: 'auto', dependencies: [] },
|
|
{ number: 2, name: 'Implement JWT', description: 'Token generation', type: 'auto', dependencies: [1] },
|
|
{ number: 3, name: 'Protected routes', description: 'Middleware', type: 'auto', dependencies: [2] },
|
|
{ number: 4, name: 'Verify auth', description: 'Test login flow', type: 'checkpoint:human-verify', dependencies: [3] },
|
|
],
|
|
});
|
|
|
|
// 7. Verify final state
|
|
const tasks = await harness.getChildTasks(detailTask.id);
|
|
expect(tasks).toHaveLength(4);
|
|
expect(tasks[0].name).toBe('Create user schema');
|
|
expect(tasks[3].type).toBe('checkpoint:human-verify');
|
|
|
|
// Agent should be idle
|
|
const finalAgent = await harness.caller.getAgent({ name: 'detailer' });
|
|
expect(finalAgent?.status).toBe('idle');
|
|
});
|
|
});
|
|
});
|