/** * 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: 'auto', 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: 'auto', 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('auto'); }); it('should create tasks with auto type', 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'); await harness.caller.createChildTasks({ parentTaskId: detailTask.id, tasks: [ { number: 1, name: 'Auto Task', description: 'Automated work', type: 'auto' }, { number: 2, name: 'Second Task', description: 'More work', type: 'auto', dependencies: [1] }, { number: 3, name: 'Third Task', description: 'Even more', type: 'auto', dependencies: [2] }, { number: 4, name: 'Final Task', description: 'Last step', type: 'auto', dependencies: [3] }, ], }); const tasks = await harness.getChildTasks(detailTask.id); expect(tasks).toHaveLength(4); for (const task of tasks) { expect(task.type).toBe('auto'); } }); 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: 'auto', 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: 'auto', 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('auto'); // Agent should be idle const finalAgent = await harness.caller.getAgent({ name: 'detailer' }); expect(finalAgent?.status).toBe('idle'); }); }); });