test(08-01): create E2E happy path test file
- Single task flow: queue -> dispatch -> complete - Sequential dependencies: priority ordering, queue state - Parallel dispatch: multiple agents, different tasks - Full merge flow: task completion -> merge queue -> process
This commit is contained in:
269
src/test/e2e/happy-path.test.ts
Normal file
269
src/test/e2e/happy-path.test.ts
Normal file
@@ -0,0 +1,269 @@
|
||||
/**
|
||||
* 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 vi.runAllTimersAsync();
|
||||
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 vi.runAllTimersAsync();
|
||||
|
||||
// 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 vi.runAllTimersAsync();
|
||||
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 vi.runAllTimersAsync();
|
||||
|
||||
// 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 vi.runAllTimersAsync();
|
||||
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 vi.runAllTimersAsync();
|
||||
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 vi.runAllTimersAsync();
|
||||
|
||||
// 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user