/** * Test Harness for E2E Testing * * Wires up the full system with mocks for E2E testing. * Uses real managers (DispatchManager, CoordinationManager) with * MockAgentManager and MockWorktreeManager for isolation. */ import { randomUUID } from 'crypto'; import type { DrizzleDatabase } from '../db/index.js'; import type { EventBus, DomainEvent } from '../events/types.js'; import { EventEmitterBus } from '../events/bus.js'; import type { AgentManager } from '../agent/types.js'; import { MockAgentManager, type MockAgentScenario } from '../agent/mock-manager.js'; import type { PendingQuestions, QuestionItem } from '../agent/types.js'; import type { WorktreeManager, Worktree, WorktreeDiff, MergeResult } from '../git/types.js'; import type { DispatchManager } from '../dispatch/types.js'; import { DefaultDispatchManager } from '../dispatch/manager.js'; import type { CoordinationManager } from '../coordination/types.js'; import { DefaultCoordinationManager } from '../coordination/manager.js'; import type { TaskRepository } from '../db/repositories/task-repository.js'; import type { MessageRepository } from '../db/repositories/message-repository.js'; import type { AgentRepository } from '../db/repositories/agent-repository.js'; import { DrizzleTaskRepository, DrizzleMessageRepository, DrizzleAgentRepository, } from '../db/repositories/drizzle/index.js'; import { createTestDatabase } from '../db/repositories/drizzle/test-helpers.js'; import { seedFixture, type InitiativeFixture, type SeededFixture, } from './fixtures.js'; // ============================================================================= // MockWorktreeManager // ============================================================================= /** * Simple in-memory WorktreeManager for testing. * Creates fake worktrees without actual git operations. */ export class MockWorktreeManager implements WorktreeManager { private worktrees: Map = new Map(); private mergeResults: Map = new Map(); /** * Set a custom merge result for a specific worktree. * Used to test conflict scenarios. */ setMergeResult(worktreeId: string, result: MergeResult): void { this.mergeResults.set(worktreeId, result); } async create(id: string, branch: string, baseBranch?: string): Promise { const worktree: Worktree = { id, branch, path: `/tmp/test-worktrees/${id}`, isMainWorktree: false, }; this.worktrees.set(id, worktree); return worktree; } async remove(id: string): Promise { if (!this.worktrees.has(id)) { throw new Error(`Worktree not found: ${id}`); } this.worktrees.delete(id); this.mergeResults.delete(id); } async list(): Promise { return Array.from(this.worktrees.values()); } async get(id: string): Promise { return this.worktrees.get(id) ?? null; } async diff(id: string): Promise { if (!this.worktrees.has(id)) { throw new Error(`Worktree not found: ${id}`); } return { files: [], summary: 'No changes (mock)', }; } async merge(id: string, targetBranch: string): Promise { if (!this.worktrees.has(id)) { throw new Error(`Worktree not found: ${id}`); } // Return custom result if set, otherwise success const customResult = this.mergeResults.get(id); if (customResult) { return customResult; } return { success: true, message: `Merged ${id} into ${targetBranch} (mock)`, }; } /** * Clear all worktrees. * Useful for test cleanup. */ clear(): void { this.worktrees.clear(); this.mergeResults.clear(); } } // ============================================================================= // CapturingEventBus // ============================================================================= /** * EventBus wrapper that captures all emitted events. * Extends EventEmitterBus with event capture functionality. */ export class CapturingEventBus extends EventEmitterBus { /** All emitted events */ emittedEvents: DomainEvent[] = []; emit(event: T): void { this.emittedEvents.push(event); super.emit(event); } /** * Get events by type. */ getEventsByType(type: string): DomainEvent[] { return this.emittedEvents.filter((e) => e.type === type); } /** * Clear captured events. */ clearEvents(): void { this.emittedEvents = []; } } // ============================================================================= // TestHarness Interface // ============================================================================= /** * Test harness for E2E testing. * Provides access to all system components and helper methods. */ export interface TestHarness { // Core components /** In-memory SQLite database */ db: DrizzleDatabase; /** Event bus with event capture */ eventBus: CapturingEventBus; /** Mock agent manager */ agentManager: MockAgentManager; /** Mock worktree manager */ worktreeManager: MockWorktreeManager; /** Real dispatch manager wired to mocks */ dispatchManager: DispatchManager; /** Real coordination manager wired to mocks */ coordinationManager: CoordinationManager; // Repositories /** Task repository */ taskRepository: TaskRepository; /** Message repository */ messageRepository: MessageRepository; /** Agent repository */ agentRepository: AgentRepository; // Helpers /** * Seed a fixture into the database. */ seedFixture(fixture: InitiativeFixture): Promise; /** * Set scenario for a specific agent name. */ setAgentScenario(agentName: string, scenario: MockAgentScenario): void; /** * Convenience: Set agent to complete with done status. */ setAgentDone(agentName: string, result?: string): void; /** * Convenience: Set agent to ask questions (array form). */ setAgentQuestions( agentName: string, questions: QuestionItem[] ): void; /** * Convenience: Set agent to ask a single question. * Wraps the question in an array internally. */ setAgentQuestion( agentName: string, questionId: string, question: string, options?: Array<{ label: string; description?: string }> ): void; /** * Convenience: Set agent to fail with unrecoverable error. */ setAgentError(agentName: string, error: string): void; /** * Get pending questions for an agent. */ getPendingQuestions(agentId: string): Promise; /** * Get events by type. */ getEventsByType(type: string): DomainEvent[]; /** * Clear all captured events. */ clearEvents(): void; /** * Clean up all resources. */ cleanup(): void; } // ============================================================================= // createTestHarness Factory // ============================================================================= /** * Create a fully wired test harness for E2E testing. * * Wires: * - In-memory SQLite database * - CapturingEventBus (captures all events) * - MockAgentManager (simulates agent behavior) * - MockWorktreeManager (fake worktrees) * - Real DefaultDispatchManager (with mock agent manager) * - Real DefaultCoordinationManager (with mock worktree manager) * - All repositories (Drizzle implementations) */ export function createTestHarness(): TestHarness { // Create database const db = createTestDatabase(); // Create event bus with capture const eventBus = new CapturingEventBus(); // Create mock managers const agentManager = new MockAgentManager({ eventBus }); const worktreeManager = new MockWorktreeManager(); // Create repositories const taskRepository = new DrizzleTaskRepository(db); const messageRepository = new DrizzleMessageRepository(db); const agentRepository = new DrizzleAgentRepository(db); // Create real managers wired to mocks const dispatchManager = new DefaultDispatchManager( taskRepository, messageRepository, agentManager, eventBus ); const coordinationManager = new DefaultCoordinationManager( worktreeManager, taskRepository, agentRepository, messageRepository, eventBus ); // Build harness const harness: TestHarness = { // Core components db, eventBus, agentManager, worktreeManager, dispatchManager, coordinationManager, // Repositories taskRepository, messageRepository, agentRepository, // Helpers seedFixture: (fixture: InitiativeFixture) => seedFixture(db, fixture), setAgentScenario: (agentName: string, scenario: MockAgentScenario) => { agentManager.setScenario(agentName, scenario); }, setAgentDone: (agentName: string, result?: string) => { agentManager.setScenario(agentName, { status: 'done', result }); }, setAgentQuestions: ( agentName: string, questions: QuestionItem[] ) => { agentManager.setScenario(agentName, { status: 'questions', questions }); }, setAgentQuestion: ( agentName: string, questionId: string, question: string, options?: Array<{ label: string; description?: string }> ) => { agentManager.setScenario(agentName, { status: 'questions', questions: [{ id: questionId, question, options }], }); }, setAgentError: (agentName: string, error: string) => { agentManager.setScenario(agentName, { status: 'unrecoverable_error', error }); }, getPendingQuestions: (agentId: string) => agentManager.getPendingQuestions(agentId), getEventsByType: (type: string) => eventBus.getEventsByType(type), clearEvents: () => eventBus.clearEvents(), cleanup: () => { agentManager.clear(); worktreeManager.clear(); eventBus.clearEvents(); }, }; return harness; }