Files
Codewalkers/src/test/harness.ts
Lukas May 185a125307 feat(10-03): add setAgentQuestion convenience helper
- Add single-question convenience method to TestHarness
- Wraps question in array for questions scenario
- Keeps setAgentQuestions for multi-question cases
2026-01-31 18:01:45 +01:00

356 lines
9.7 KiB
TypeScript

/**
* 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<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;
/**
* 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<PendingQuestions | null>;
/**
* 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;
}