feat(07-02): create test harness with full system wiring
- Add TestHarness interface with all system components and helpers - Add createTestHarness() factory wiring full system with mocks - Add MockWorktreeManager (in-memory worktree simulation) - Add CapturingEventBus (event capture for verification) - Wire real DispatchManager and CoordinationManager with mocks - Export all components via src/test/index.ts
This commit is contained in:
291
src/test/harness.ts
Normal file
291
src/test/harness.ts
Normal file
@@ -0,0 +1,291 @@
|
|||||||
|
/**
|
||||||
|
* 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 { 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<string, Worktree> = new Map();
|
||||||
|
private mergeResults: Map<string, MergeResult> = 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<Worktree> {
|
||||||
|
const worktree: Worktree = {
|
||||||
|
id,
|
||||||
|
branch,
|
||||||
|
path: `/tmp/test-worktrees/${id}`,
|
||||||
|
isMainWorktree: false,
|
||||||
|
};
|
||||||
|
this.worktrees.set(id, worktree);
|
||||||
|
return worktree;
|
||||||
|
}
|
||||||
|
|
||||||
|
async remove(id: string): Promise<void> {
|
||||||
|
if (!this.worktrees.has(id)) {
|
||||||
|
throw new Error(`Worktree not found: ${id}`);
|
||||||
|
}
|
||||||
|
this.worktrees.delete(id);
|
||||||
|
this.mergeResults.delete(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async list(): Promise<Worktree[]> {
|
||||||
|
return Array.from(this.worktrees.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
async get(id: string): Promise<Worktree | null> {
|
||||||
|
return this.worktrees.get(id) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async diff(id: string): Promise<WorktreeDiff> {
|
||||||
|
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<MergeResult> {
|
||||||
|
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<T extends DomainEvent>(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<SeededFixture>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set scenario for a specific agent name.
|
||||||
|
*/
|
||||||
|
setAgentScenario(agentName: string, scenario: MockAgentScenario): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
},
|
||||||
|
|
||||||
|
getEventsByType: (type: string) => eventBus.getEventsByType(type),
|
||||||
|
|
||||||
|
clearEvents: () => eventBus.clearEvents(),
|
||||||
|
|
||||||
|
cleanup: () => {
|
||||||
|
agentManager.clear();
|
||||||
|
worktreeManager.clear();
|
||||||
|
eventBus.clearEvents();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return harness;
|
||||||
|
}
|
||||||
26
src/test/index.ts
Normal file
26
src/test/index.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
/**
|
||||||
|
* Test Module
|
||||||
|
*
|
||||||
|
* Provides test harness and fixtures for E2E testing.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Fixture helpers
|
||||||
|
export {
|
||||||
|
seedFixture,
|
||||||
|
type TaskFixture,
|
||||||
|
type PlanFixture,
|
||||||
|
type PhaseFixture,
|
||||||
|
type InitiativeFixture,
|
||||||
|
type SeededFixture,
|
||||||
|
SIMPLE_FIXTURE,
|
||||||
|
PARALLEL_FIXTURE,
|
||||||
|
COMPLEX_FIXTURE,
|
||||||
|
} from './fixtures.js';
|
||||||
|
|
||||||
|
// Test harness
|
||||||
|
export {
|
||||||
|
createTestHarness,
|
||||||
|
MockWorktreeManager,
|
||||||
|
CapturingEventBus,
|
||||||
|
type TestHarness,
|
||||||
|
} from './harness.js';
|
||||||
Reference in New Issue
Block a user