/** * E2E Happy Path Tests * * Tests proving core dispatch/coordination flow works end-to-end * using the TestHarness with mocked agents and worktrees. */ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { createTestHarness, SIMPLE_FIXTURE, PARALLEL_FIXTURE, COMPLEX_FIXTURE, type TestHarness, } from '../index.js'; describe('E2E Happy Path', () => { let harness: TestHarness; beforeEach(() => { harness = createTestHarness(); }); afterEach(() => { harness.cleanup(); vi.useRealTimers(); }); // =========================================================================== // Scenario 1: Single Task Flow // =========================================================================== describe('Single task flow', () => { it('completes a single task from queue to completion', async () => { vi.useFakeTimers(); const seeded = await harness.seedFixture(SIMPLE_FIXTURE); const taskAId = seeded.tasks.get('Task A')!; // Pre-seed idle agent (required by DispatchManager before spawning new ones) await harness.agentManager.spawn({ name: 'pool-agent', taskId: 'placeholder', prompt: 'placeholder', }); await harness.advanceTimers(); harness.clearEvents(); // Step 1: Queue task await harness.dispatchManager.queue(taskAId); // Verify task:queued event const queuedEvents = harness.getEventsByType('task:queued'); expect(queuedEvents.length).toBe(1); expect((queuedEvents[0].payload as { taskId: string }).taskId).toBe(taskAId); // Step 2: Dispatch task const dispatchResult = await harness.dispatchManager.dispatchNext(); expect(dispatchResult.success).toBe(true); expect(dispatchResult.taskId).toBe(taskAId); expect(dispatchResult.agentId).toBeDefined(); // Verify task:dispatched event const dispatchedEvents = harness.getEventsByType('task:dispatched'); expect(dispatchedEvents.length).toBe(1); expect((dispatchedEvents[0].payload as { taskId: string }).taskId).toBe(taskAId); // Verify agent:spawned event const spawnedEvents = harness.getEventsByType('agent:spawned'); expect(spawnedEvents.length).toBe(1); // Step 3: Wait for agent completion await harness.advanceTimers(); // Verify agent:stopped event const stoppedEvents = harness.getEventsByType('agent:stopped'); expect(stoppedEvents.length).toBe(1); // Step 4: Mark task complete await harness.dispatchManager.completeTask(taskAId); // Verify task status in database const task = await harness.taskRepository.findById(taskAId); expect(task?.status).toBe('completed'); }); }); // =========================================================================== // Scenario 2: Sequential Dependencies // =========================================================================== describe('Sequential dependencies', () => { it('dispatches tasks in priority order (dependency ordering via task status)', async () => { vi.useFakeTimers(); const seeded = await harness.seedFixture(SIMPLE_FIXTURE); const taskAId = seeded.tasks.get('Task A')!; const taskBId = seeded.tasks.get('Task B')!; const taskCId = seeded.tasks.get('Task C')!; // Pre-seed idle agent await harness.agentManager.spawn({ name: 'pool-agent', taskId: 'placeholder', prompt: 'placeholder', }); await harness.advanceTimers(); harness.clearEvents(); // Queue all three tasks await harness.dispatchManager.queue(taskAId); await harness.dispatchManager.queue(taskBId); await harness.dispatchManager.queue(taskCId); harness.clearEvents(); // All three tasks are queued const queueState = await harness.dispatchManager.getQueueState(); expect(queueState.queued.length).toBe(3); // First dispatchNext: Task A (high priority) dispatches first const nextTask = await harness.dispatchManager.getNextDispatchable(); expect(nextTask).not.toBeNull(); expect(nextTask!.taskId).toBe(taskAId); // High priority first // All tasks are "ready" in current implementation (dependency loading TBD) const readyTaskIds = queueState.ready.map((t) => t.taskId); expect(readyTaskIds).toContain(taskAId); // Dispatch Task A const dispatchResult = await harness.dispatchManager.dispatchNext(); expect(dispatchResult.success).toBe(true); expect(dispatchResult.taskId).toBe(taskAId); // Wait for agent completion await harness.advanceTimers(); // Complete Task A await harness.dispatchManager.completeTask(taskAId); // Verify Task A removed from queue, B and C remain const queueStateAfter = await harness.dispatchManager.getQueueState(); const remainingTaskIds = queueStateAfter.queued.map((t) => t.taskId); expect(remainingTaskIds).not.toContain(taskAId); expect(remainingTaskIds).toContain(taskBId); expect(remainingTaskIds).toContain(taskCId); // Task A marked completed in database const taskA = await harness.taskRepository.findById(taskAId); expect(taskA?.status).toBe('completed'); }); }); // =========================================================================== // Scenario 3: Parallel Dispatch // =========================================================================== describe('Parallel dispatch', () => { it('dispatches multiple independent tasks to multiple agents', async () => { vi.useFakeTimers(); const seeded = await harness.seedFixture(PARALLEL_FIXTURE); const taskXId = seeded.tasks.get('Task X')!; const taskYId = seeded.tasks.get('Task Y')!; const taskPId = seeded.tasks.get('Task P')!; const taskQId = seeded.tasks.get('Task Q')!; // Pre-seed 2 idle agents await harness.agentManager.spawn({ name: 'pool-agent-1', taskId: 'placeholder-1', prompt: 'placeholder', }); await harness.agentManager.spawn({ name: 'pool-agent-2', taskId: 'placeholder-2', prompt: 'placeholder', }); await harness.advanceTimers(); harness.clearEvents(); // Queue all 4 tasks await harness.dispatchManager.queue(taskXId); await harness.dispatchManager.queue(taskYId); await harness.dispatchManager.queue(taskPId); await harness.dispatchManager.queue(taskQId); harness.clearEvents(); // All 4 tasks should be dispatchable (no dependencies) const queueState = await harness.dispatchManager.getQueueState(); expect(queueState.ready.length).toBe(4); // Dispatch first task const result1 = await harness.dispatchManager.dispatchNext(); expect(result1.success).toBe(true); // Dispatch second task (parallel) const result2 = await harness.dispatchManager.dispatchNext(); expect(result2.success).toBe(true); // Verify both agents assigned different tasks expect(result1.taskId).not.toBe(result2.taskId); expect(result1.agentId).not.toBe(result2.agentId); // Both dispatches succeeded const dispatchedEvents = harness.getEventsByType('task:dispatched'); expect(dispatchedEvents.length).toBe(2); }); }); // =========================================================================== // Scenario 4: Full Merge Flow // =========================================================================== describe('Full merge flow', () => { it('queues and processes merge after task completion', async () => { vi.useFakeTimers(); const seeded = await harness.seedFixture(SIMPLE_FIXTURE); const taskAId = seeded.tasks.get('Task A')!; // Pre-seed idle agent in MockAgentManager await harness.agentManager.spawn({ name: 'pool-agent', taskId: 'placeholder', prompt: 'placeholder', }); await harness.advanceTimers(); harness.clearEvents(); // Queue and dispatch task await harness.dispatchManager.queue(taskAId); const dispatchResult = await harness.dispatchManager.dispatchNext(); expect(dispatchResult.success).toBe(true); // Wait for agent completion await harness.advanceTimers(); // Complete task await harness.dispatchManager.completeTask(taskAId); harness.clearEvents(); // Create agent in database (CoordinationManager.queueMerge requires it) // This bridges the gap between MockAgentManager (in-memory) and AgentRepository (database) const worktreeId = `worktree-${taskAId.slice(0, 8)}`; const agent = await harness.agentRepository.create({ name: `agent-${taskAId.slice(0, 6)}`, taskId: taskAId, worktreeId, status: 'idle', }); // Create worktree for merge await harness.worktreeManager.create(worktreeId, `feature-${taskAId.slice(0, 6)}`); // Queue merge await harness.coordinationManager.queueMerge(taskAId); // Verify merge:queued event const mergeQueuedEvents = harness.getEventsByType('merge:queued'); expect(mergeQueuedEvents.length).toBe(1); // Process merges const mergeResults = await harness.coordinationManager.processMerges('main'); expect(mergeResults.length).toBe(1); expect(mergeResults[0].taskId).toBe(taskAId); expect(mergeResults[0].success).toBe(true); // Verify merge:completed event const mergeCompletedEvents = harness.getEventsByType('merge:completed'); expect(mergeCompletedEvents.length).toBe(1); }); }); // =========================================================================== // Scenario 5: Complex Dependency Flow // =========================================================================== describe('Complex dependency flow', () => { it('handles multi-level dependency graph with COMPLEX_FIXTURE', async () => { vi.useFakeTimers(); const seeded = await harness.seedFixture(COMPLEX_FIXTURE); // Get all task IDs const task1AId = seeded.tasks.get('Task 1A')!; const task1BId = seeded.tasks.get('Task 1B')!; const task2AId = seeded.tasks.get('Task 2A')!; const task3AId = seeded.tasks.get('Task 3A')!; const task4AId = seeded.tasks.get('Task 4A')!; // Pre-seed idle agent await harness.agentManager.spawn({ name: 'pool-agent', taskId: 'placeholder', prompt: 'placeholder', }); await harness.advanceTimers(); harness.clearEvents(); // Queue all 5 tasks await harness.dispatchManager.queue(task1AId); await harness.dispatchManager.queue(task1BId); await harness.dispatchManager.queue(task2AId); await harness.dispatchManager.queue(task3AId); await harness.dispatchManager.queue(task4AId); harness.clearEvents(); // Verify all 5 tasks are queued const initialState = await harness.dispatchManager.getQueueState(); expect(initialState.queued.length).toBe(5); // Only tasks with no dependencies are ready: // - Task 1A: no deps -> READY // - Task 1B: no deps -> READY // - Task 2A: depends on 1A -> NOT READY // - Task 3A: depends on 1B -> NOT READY // - Task 4A: depends on 2A, 3A -> NOT READY expect(initialState.ready.length).toBe(2); // First dispatch: Task 1A (high priority, first queued) const result1 = await harness.dispatchManager.dispatchNext(); expect(result1.success).toBe(true); expect(result1.taskId).toBe(task1AId); // Wait for agent completion await harness.advanceTimers(); // Complete Task 1A await harness.dispatchManager.completeTask(task1AId); // Verify Task 1A completed in database const task1A = await harness.taskRepository.findById(task1AId); expect(task1A?.status).toBe('completed'); // 4 tasks remain in queue const afterFirstState = await harness.dispatchManager.getQueueState(); expect(afterFirstState.queued.length).toBe(4); // Dispatch and complete remaining tasks one by one // Task 1B (high priority among remaining) const result2 = await harness.dispatchManager.dispatchNext(); expect(result2.success).toBe(true); await harness.advanceTimers(); await harness.dispatchManager.completeTask(result2.taskId!); // 3 tasks remain const midState = await harness.dispatchManager.getQueueState(); expect(midState.queued.length).toBe(3); // Continue dispatching remaining tasks const result3 = await harness.dispatchManager.dispatchNext(); expect(result3.success).toBe(true); await harness.advanceTimers(); await harness.dispatchManager.completeTask(result3.taskId!); const result4 = await harness.dispatchManager.dispatchNext(); expect(result4.success).toBe(true); await harness.advanceTimers(); await harness.dispatchManager.completeTask(result4.taskId!); const result5 = await harness.dispatchManager.dispatchNext(); expect(result5.success).toBe(true); await harness.advanceTimers(); await harness.dispatchManager.completeTask(result5.taskId!); // All tasks completed const finalState = await harness.dispatchManager.getQueueState(); expect(finalState.queued.length).toBe(0); // Verify all 5 tasks completed in database const allTasks = await Promise.all([ harness.taskRepository.findById(task1AId), harness.taskRepository.findById(task1BId), harness.taskRepository.findById(task2AId), harness.taskRepository.findById(task3AId), harness.taskRepository.findById(task4AId), ]); expect(allTasks.every((t) => t?.status === 'completed')).toBe(true); // Verify event sequence: 5 task:dispatched, 5 task:completed const dispatchedEvents = harness.getEventsByType('task:dispatched'); expect(dispatchedEvents.length).toBe(5); const completedEvents = harness.getEventsByType('task:completed'); expect(completedEvents.length).toBe(5); }); it('fixture dependencies are stored correctly in database', async () => { const seeded = await harness.seedFixture(COMPLEX_FIXTURE); // Get task IDs const task1AId = seeded.tasks.get('Task 1A')!; const task1BId = seeded.tasks.get('Task 1B')!; const task2AId = seeded.tasks.get('Task 2A')!; const task3AId = seeded.tasks.get('Task 3A')!; const task4AId = seeded.tasks.get('Task 4A')!; // Query task_dependencies directly to verify fixture setup const { taskDependencies } = await import('../../db/schema.js'); const { eq } = await import('drizzle-orm'); // Task 2A should depend on Task 1A const task2ADeps = await harness.db .select() .from(taskDependencies) .where(eq(taskDependencies.taskId, task2AId)); expect(task2ADeps.length).toBe(1); expect(task2ADeps[0].dependsOnTaskId).toBe(task1AId); // Task 3A should depend on Task 1B const task3ADeps = await harness.db .select() .from(taskDependencies) .where(eq(taskDependencies.taskId, task3AId)); expect(task3ADeps.length).toBe(1); expect(task3ADeps[0].dependsOnTaskId).toBe(task1BId); // Task 4A should depend on both Task 2A and Task 3A const task4ADeps = await harness.db .select() .from(taskDependencies) .where(eq(taskDependencies.taskId, task4AId)); expect(task4ADeps.length).toBe(2); const depIds = task4ADeps.map((d) => d.dependsOnTaskId); expect(depIds).toContain(task2AId); expect(depIds).toContain(task3AId); // Tasks 1A and 1B should have no dependencies const task1ADeps = await harness.db .select() .from(taskDependencies) .where(eq(taskDependencies.taskId, task1AId)); expect(task1ADeps.length).toBe(0); const task1BDeps = await harness.db .select() .from(taskDependencies) .where(eq(taskDependencies.taskId, task1BId)); expect(task1BDeps.length).toBe(0); }); }); });