/** * E2E Tests for Architect Workflow * * Tests the complete architect workflow from discussion through phase creation: * - Discuss mode: Gather context, answer questions, capture decisions * - Plan mode: Break initiative into phases * - Full workflow: Discuss -> Plan -> Phase persistence * * 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 } from '../../events/types.js'; describe('Architect Workflow E2E', () => { let harness: TestHarness; beforeEach(() => { harness = createTestHarness(); }); afterEach(() => { harness.cleanup(); vi.useRealTimers(); }); describe('discuss mode', () => { it('should spawn architect in discuss mode and complete with decisions', async () => { vi.useFakeTimers(); // Create initiative const initiative = await harness.createInitiative('Auth System'); // Set up discuss completion scenario harness.setArchitectDiscussComplete('auth-discuss', [ { topic: 'Auth Method', decision: 'JWT', reason: 'Stateless, scalable' }, { topic: 'Token Storage', decision: 'httpOnly cookie', reason: 'XSS protection' }, ], 'Auth approach decided'); // Spawn architect in discuss mode const agent = await harness.caller.spawnArchitectDiscuss({ name: 'auth-discuss', initiativeId: initiative.id, }); expect(agent.mode).toBe('discuss'); // Wait for completion await harness.advanceTimers(); // Verify agent stopped with context_complete const events = harness.getEmittedEvents('agent:stopped') as AgentStoppedEvent[]; expect(events).toHaveLength(1); expect(events[0].payload.reason).toBe('context_complete'); }); it('should pause on questions and resume with answers', async () => { vi.useFakeTimers(); const initiative = await harness.createInitiative('Auth System'); // First, agent asks questions harness.setArchitectDiscussQuestions('auth-discuss', [ { id: 'q1', question: 'JWT or Session?', options: [{ label: 'JWT' }, { label: 'Session' }] }, { id: 'q2', question: 'OAuth providers?' }, ]); const agent = await harness.caller.spawnArchitectDiscuss({ name: 'auth-discuss', initiativeId: initiative.id, }); await harness.advanceTimers(); // Agent should be waiting const waitingAgent = await harness.caller.getAgent({ name: 'auth-discuss' }); expect(waitingAgent?.status).toBe('waiting_for_input'); // Get pending questions const pending = await harness.mockAgentManager.getPendingQuestions(agent.id); expect(pending?.questions).toHaveLength(2); // Now set up completion scenario for after resume harness.setArchitectDiscussComplete('auth-discuss', [ { topic: 'Auth', decision: 'JWT', reason: 'User chose' }, ], 'Complete'); // Resume with answers await harness.caller.resumeAgent({ name: 'auth-discuss', answers: { q1: 'JWT', q2: 'Google, GitHub' }, }); await harness.advanceTimers(); // Should complete const finalAgent = await harness.caller.getAgent({ name: 'auth-discuss' }); expect(finalAgent?.status).toBe('idle'); }); }); describe('plan mode', () => { it('should spawn architect in plan mode and create phases', async () => { vi.useFakeTimers(); const initiative = await harness.createInitiative('Auth System'); // Set up plan completion harness.setArchitectPlanComplete('auth-plan', [ { number: 1, name: 'Database Setup', description: 'User table and auth schema', dependencies: [] }, { number: 2, name: 'JWT Implementation', description: 'Token generation and validation', dependencies: [1] }, { number: 3, name: 'Protected Routes', description: 'Middleware and route guards', dependencies: [2] }, ]); const agent = await harness.caller.spawnArchitectPlan({ name: 'auth-plan', initiativeId: initiative.id, }); expect(agent.mode).toBe('plan'); await harness.advanceTimers(); // Verify stopped with plan_complete const events = harness.getEmittedEvents('agent:stopped') as AgentStoppedEvent[]; expect(events).toHaveLength(1); expect(events[0].payload.reason).toBe('plan_complete'); }); it('should persist phases from plan output', async () => { const initiative = await harness.createInitiative('Auth System'); const phasesData = [ { name: 'Foundation' }, { name: 'Features' }, ]; // Persist phases (simulating what would happen after plan) const created = await harness.createPhasesFromPlan(initiative.id, phasesData); expect(created).toHaveLength(2); // Verify retrieval const phases = await harness.getPhases(initiative.id); expect(phases).toHaveLength(2); expect(phases[0].name).toBe('Foundation'); expect(phases[1].name).toBe('Features'); }); }); describe('plan conflict detection', () => { it('should reject if a plan agent is already running', async () => { vi.useFakeTimers(); const initiative = await harness.createInitiative('Auth System'); // Set up a long-running plan agent (never completes during this test) harness.setArchitectPlanComplete('first-plan', [ { number: 1, name: 'Phase 1', description: 'First', dependencies: [] }, ]); // Use a delay so it stays running harness.setAgentScenario('first-plan', { status: 'done', delay: 999999 }); await harness.caller.spawnArchitectPlan({ name: 'first-plan', initiativeId: initiative.id, }); // Agent should be running const agents = await harness.caller.listAgents(); expect(agents.find(a => a.name === 'first-plan')?.status).toBe('running'); // Second plan should be rejected await expect( harness.caller.spawnArchitectPlan({ name: 'second-plan', initiativeId: initiative.id, }), ).rejects.toThrow(/already running/); }); it('should auto-dismiss stale plan agents before checking', async () => { vi.useFakeTimers(); const initiative = await harness.createInitiative('Auth System'); // Set up a plan agent that crashes immediately harness.setAgentScenario('stale-plan', { status: 'error', error: 'crashed' }); await harness.caller.spawnArchitectPlan({ name: 'stale-plan', initiativeId: initiative.id, }); await harness.advanceTimers(); // Should be crashed const agents = await harness.caller.listAgents(); expect(agents.find(a => a.name === 'stale-plan')?.status).toBe('crashed'); // New plan should succeed (stale one gets auto-dismissed) harness.setArchitectPlanComplete('new-plan', [ { number: 1, name: 'Phase 1', description: 'First', dependencies: [] }, ]); const agent = await harness.caller.spawnArchitectPlan({ name: 'new-plan', initiativeId: initiative.id, }); expect(agent.mode).toBe('plan'); }); it('should allow plan for different initiatives', async () => { vi.useFakeTimers(); const init1 = await harness.createInitiative('Initiative 1'); const init2 = await harness.createInitiative('Initiative 2'); // Long-running agent on initiative 1 harness.setAgentScenario('plan-1', { status: 'done', delay: 999999 }); await harness.caller.spawnArchitectPlan({ name: 'plan-1', initiativeId: init1.id, }); // Plan on initiative 2 should succeed harness.setArchitectPlanComplete('plan-2', [ { number: 1, name: 'Phase 1', description: 'First', dependencies: [] }, ]); const agent = await harness.caller.spawnArchitectPlan({ name: 'plan-2', initiativeId: init2.id, }); expect(agent.mode).toBe('plan'); }); }); describe('full workflow', () => { it('should complete discuss -> plan -> phases workflow', async () => { vi.useFakeTimers(); // 1. Create initiative const initiative = await harness.createInitiative('Full Workflow Test'); // 2. Discuss phase harness.setArchitectDiscussComplete('discuss-agent', [ { topic: 'Scope', decision: 'MVP only', reason: 'Time constraint' }, ], 'Scope defined'); await harness.caller.spawnArchitectDiscuss({ name: 'discuss-agent', initiativeId: initiative.id, }); await harness.advanceTimers(); // 3. Plan phase harness.setArchitectPlanComplete('plan-agent', [ { number: 1, name: 'Core', description: 'Core functionality', dependencies: [] }, { number: 2, name: 'Polish', description: 'UI and UX', dependencies: [1] }, ]); await harness.caller.spawnArchitectPlan({ name: 'plan-agent', initiativeId: initiative.id, contextSummary: 'MVP scope defined', }); await harness.advanceTimers(); // 4. Persist phases await harness.createPhasesFromPlan(initiative.id, [ { name: 'Core' }, { name: 'Polish' }, ]); // 5. Verify final state const phases = await harness.getPhases(initiative.id); expect(phases).toHaveLength(2); // Both agents should be idle const agents = await harness.caller.listAgents(); expect(agents.filter(a => a.status === 'idle')).toHaveLength(2); }); }); });