/** * Tests for Test Harness * * Proves that the test harness enables E2E testing scenarios. */ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { createTestHarness, SIMPLE_FIXTURE, PARALLEL_FIXTURE, COMPLEX_FIXTURE, type TestHarness, } from './index.js'; import { taskDependencies } from '../db/schema.js'; import { eq } from 'drizzle-orm'; describe('TestHarness', () => { let harness: TestHarness; beforeEach(() => { harness = createTestHarness(); }); afterEach(() => { harness.cleanup(); vi.useRealTimers(); }); describe('createTestHarness', () => { it('returns all components', () => { expect(harness.db).toBeDefined(); expect(harness.eventBus).toBeDefined(); expect(harness.agentManager).toBeDefined(); expect(harness.worktreeManager).toBeDefined(); expect(harness.dispatchManager).toBeDefined(); expect(harness.coordinationManager).toBeDefined(); expect(harness.taskRepository).toBeDefined(); expect(harness.messageRepository).toBeDefined(); expect(harness.agentRepository).toBeDefined(); }); it('provides helper methods', () => { expect(typeof harness.seedFixture).toBe('function'); expect(typeof harness.setAgentScenario).toBe('function'); expect(typeof harness.setAgentQuestion).toBe('function'); expect(typeof harness.setAgentQuestions).toBe('function'); expect(typeof harness.getEventsByType).toBe('function'); expect(typeof harness.clearEvents).toBe('function'); expect(typeof harness.cleanup).toBe('function'); }); }); describe('setAgentQuestion convenience helper', () => { it('wraps single question in array format', async () => { vi.useFakeTimers(); // Set single question using convenience method harness.setAgentQuestion('test-agent', 'q1', 'Which option?', [ { label: 'Option A', description: 'First option' }, { label: 'Option B', description: 'Second option' }, ]); // Spawn agent with that scenario const agent = await harness.agentManager.spawn({ name: 'test-agent', taskId: 'task-1', prompt: 'test', }); await harness.advanceTimers(); // Verify questions array format const pending = await harness.getPendingQuestions(agent.id); expect(pending).not.toBeNull(); expect(pending?.questions).toHaveLength(1); expect(pending?.questions[0].id).toBe('q1'); expect(pending?.questions[0].question).toBe('Which option?'); expect(pending?.questions[0].options).toHaveLength(2); }); }); describe('seedFixture', () => { it('creates task hierarchy from SIMPLE_FIXTURE', async () => { const seeded = await harness.seedFixture(SIMPLE_FIXTURE); // Check initiative created expect(seeded.initiativeId).toBeDefined(); // Check phases created expect(seeded.phases.size).toBe(1); expect(seeded.phases.has('Phase 1')).toBe(true); // Check task groups created expect(seeded.taskGroups.size).toBe(1); expect(seeded.taskGroups.has('Task Group 1')).toBe(true); // Check tasks created expect(seeded.tasks.size).toBe(3); expect(seeded.tasks.has('Task A')).toBe(true); expect(seeded.tasks.has('Task B')).toBe(true); expect(seeded.tasks.has('Task C')).toBe(true); }); it('returns correct IDs that exist in database', async () => { const seeded = await harness.seedFixture(SIMPLE_FIXTURE); // Verify task exists in database const taskAId = seeded.tasks.get('Task A')!; const taskA = await harness.taskRepository.findById(taskAId); expect(taskA).not.toBeNull(); expect(taskA?.name).toBe('Task A'); }); it('creates PARALLEL_FIXTURE correctly', async () => { const seeded = await harness.seedFixture(PARALLEL_FIXTURE); expect(seeded.phases.size).toBe(1); expect(seeded.taskGroups.size).toBe(2); expect(seeded.tasks.size).toBe(4); expect(seeded.tasks.has('Task X')).toBe(true); expect(seeded.tasks.has('Task Q')).toBe(true); }); it('creates COMPLEX_FIXTURE correctly', async () => { const seeded = await harness.seedFixture(COMPLEX_FIXTURE); expect(seeded.phases.size).toBe(2); expect(seeded.taskGroups.size).toBe(4); expect(seeded.tasks.size).toBe(5); }); }); describe('task dependencies', () => { it('resolves dependencies correctly (dependsOn contains actual task IDs)', async () => { const seeded = await harness.seedFixture(SIMPLE_FIXTURE); const taskAId = seeded.tasks.get('Task A')!; const taskBId = seeded.tasks.get('Task B')!; // Query task_dependencies table directly const deps = await harness.db .select() .from(taskDependencies) .where(eq(taskDependencies.taskId, taskBId)); expect(deps.length).toBe(1); expect(deps[0].dependsOnTaskId).toBe(taskAId); }); it('creates multiple dependencies for a task', async () => { const seeded = await harness.seedFixture(COMPLEX_FIXTURE); // Task 4A depends on both Task 2A and Task 3A const task4AId = seeded.tasks.get('Task 4A')!; const task2AId = seeded.tasks.get('Task 2A')!; const task3AId = seeded.tasks.get('Task 3A')!; const deps = await harness.db .select() .from(taskDependencies) .where(eq(taskDependencies.taskId, task4AId)); expect(deps.length).toBe(2); const depIds = deps.map((d) => d.dependsOnTaskId); expect(depIds).toContain(task2AId); expect(depIds).toContain(task3AId); }); }); describe('event capture', () => { it('captures events via getEventsByType', async () => { const seeded = await harness.seedFixture(SIMPLE_FIXTURE); const taskAId = seeded.tasks.get('Task A')!; // Queue a task (emits task:queued event) await harness.dispatchManager.queue(taskAId); const events = harness.getEventsByType('task:queued'); expect(events.length).toBe(1); expect((events[0].payload as { taskId: string }).taskId).toBe(taskAId); }); it('clears events via clearEvents', async () => { const seeded = await harness.seedFixture(SIMPLE_FIXTURE); const taskAId = seeded.tasks.get('Task A')!; await harness.dispatchManager.queue(taskAId); expect(harness.getEventsByType('task:queued').length).toBe(1); harness.clearEvents(); expect(harness.getEventsByType('task:queued').length).toBe(0); }); }); describe('dispatch flow', () => { it('dispatchManager.queue() + dispatchNext() uses MockAgentManager', async () => { vi.useFakeTimers(); const seeded = await harness.seedFixture(SIMPLE_FIXTURE); const taskAId = seeded.tasks.get('Task A')!; // Note: DispatchManager.dispatchNext() requires an idle agent in the pool // before it will spawn a new agent. Pre-seed an idle agent. await harness.agentManager.spawn({ name: 'pool-agent', taskId: 'placeholder', prompt: 'placeholder', }); // Wait for agent to complete and become idle await harness.advanceTimers(); // Queue the task await harness.dispatchManager.queue(taskAId); // Clear events from queue and agent spawn harness.clearEvents(); // Dispatch the task const result = await harness.dispatchManager.dispatchNext(); // Advance timers to trigger mock agent completion await harness.advanceTimers(); expect(result.success).toBe(true); expect(result.taskId).toBe(taskAId); expect(result.agentId).toBeDefined(); // Should have emitted task:dispatched const dispatchedEvents = harness.getEventsByType('task:dispatched'); expect(dispatchedEvents.length).toBe(1); }); it('returns failure when no tasks are queued', async () => { const result = await harness.dispatchManager.dispatchNext(); expect(result.success).toBe(false); expect(result.reason).toBe('No dispatchable tasks'); }); it('returns failure when no idle agents available', async () => { const seeded = await harness.seedFixture(SIMPLE_FIXTURE); const taskAId = seeded.tasks.get('Task A')!; // Queue the task but don't pre-seed any agents await harness.dispatchManager.queue(taskAId); // Dispatch without any agents in pool const result = await harness.dispatchManager.dispatchNext(); expect(result.success).toBe(false); expect(result.reason).toBe('No available agents'); }); }); describe('agent completion triggers events', () => { it('agent completion emits agent:stopped event', async () => { vi.useFakeTimers(); const seeded = await harness.seedFixture(SIMPLE_FIXTURE); const taskAId = seeded.tasks.get('Task A')!; // Pre-seed an idle agent (required by DispatchManager) await harness.agentManager.spawn({ name: 'pool-agent', taskId: 'placeholder', prompt: 'placeholder', }); await harness.advanceTimers(); harness.clearEvents(); // Queue and dispatch await harness.dispatchManager.queue(taskAId); harness.clearEvents(); await harness.dispatchManager.dispatchNext(); // Should have agent:spawned const spawnedEvents = harness.getEventsByType('agent:spawned'); expect(spawnedEvents.length).toBe(1); // Advance timers to trigger completion await harness.advanceTimers(); // Should have agent:stopped const stoppedEvents = harness.getEventsByType('agent:stopped'); expect(stoppedEvents.length).toBe(1); }); it('custom scenario affects agent behavior', async () => { vi.useFakeTimers(); const seeded = await harness.seedFixture(SIMPLE_FIXTURE); const taskAId = seeded.tasks.get('Task A')!; // Pre-seed an idle agent (required by DispatchManager) await harness.agentManager.spawn({ name: 'pool-agent', taskId: 'placeholder', prompt: 'placeholder', }); await harness.advanceTimers(); harness.clearEvents(); // Set error scenario for the agent that will be spawned harness.setAgentScenario(`agent-${taskAId.slice(0, 6)}`, { status: 'error', delay: 0, error: 'Test crash', }); // Queue and dispatch await harness.dispatchManager.queue(taskAId); harness.clearEvents(); await harness.dispatchManager.dispatchNext(); // Advance timers await harness.advanceTimers(); // Should have agent:crashed const crashedEvents = harness.getEventsByType('agent:crashed'); expect(crashedEvents.length).toBe(1); }); }); describe('full dispatch -> complete -> merge flow', () => { it('works end-to-end', async () => { vi.useFakeTimers(); const seeded = await harness.seedFixture(SIMPLE_FIXTURE); const taskAId = seeded.tasks.get('Task A')!; // Pre-seed an idle agent (required by DispatchManager) 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); // Step 2: Dispatch task const dispatchResult = await harness.dispatchManager.dispatchNext(); expect(dispatchResult.success).toBe(true); // Advance timers for agent completion await harness.advanceTimers(); // Clear events for cleaner verification harness.clearEvents(); // Step 3: Complete task await harness.dispatchManager.completeTask(taskAId); // Verify task:completed event const completedEvents = harness.getEventsByType('task:completed'); expect(completedEvents.length).toBe(1); expect((completedEvents[0].payload as { taskId: string }).taskId).toBe(taskAId); // Step 4: Verify task status in database const task = await harness.taskRepository.findById(taskAId); expect(task?.status).toBe('completed'); }); }); describe('MockWorktreeManager', () => { it('creates fake worktrees', async () => { const worktree = await harness.worktreeManager.create('wt-1', 'feature-1'); expect(worktree.id).toBe('wt-1'); expect(worktree.branch).toBe('feature-1'); expect(worktree.path).toContain('wt-1'); }); it('merge returns success by default', async () => { await harness.worktreeManager.create('wt-1', 'feature-1'); const result = await harness.worktreeManager.merge('wt-1', 'main'); expect(result.success).toBe(true); }); it('allows setting custom merge results', async () => { await harness.worktreeManager.create('wt-1', 'feature-1'); harness.worktreeManager.setMergeResult('wt-1', { success: false, conflicts: ['file1.ts', 'file2.ts'], message: 'Merge conflict', }); const result = await harness.worktreeManager.merge('wt-1', 'main'); expect(result.success).toBe(false); expect(result.conflicts).toEqual(['file1.ts', 'file2.ts']); }); }); });