feat(test): add TestHarness architect mode helpers and tRPC caller

Add convenience methods for architect mode testing:
- setArchitectDiscussComplete for context_complete scenarios
- setArchitectDiscussQuestions for discuss mode questions
- setArchitectBreakdownComplete for breakdown_complete scenarios
- getInitiative, getPhases, createInitiative, createPhasesFromBreakdown
- mockAgentManager alias, advanceTimers, getEmittedEvents helpers
- Wire up initiative/phase repositories and tRPC caller to harness

Also fix pre-existing test issues with dependencies and type casting.
This commit is contained in:
Lukas May
2026-01-31 19:26:46 +01:00
parent 4230e171f5
commit 021937c28d
3 changed files with 206 additions and 5 deletions

View File

@@ -7,7 +7,7 @@
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { MockAgentManager, type MockAgentScenario } from './mock-manager.js';
import type { EventBus, DomainEvent } from '../events/types.js';
import type { EventBus, DomainEvent, AgentStoppedEvent } from '../events/types.js';
// =============================================================================
// Test Helpers
@@ -632,8 +632,8 @@ describe('MockAgentManager', () => {
status: 'breakdown_complete',
delay: 0,
phases: [
{ number: 1, name: 'Foundation', description: 'Core setup' },
{ number: 2, name: 'Features', description: 'Main features' },
{ number: 1, name: 'Foundation', description: 'Core setup', dependencies: [] },
{ number: 2, name: 'Features', description: 'Main features', dependencies: [1] },
],
});
@@ -663,7 +663,7 @@ describe('MockAgentManager', () => {
});
await vi.runAllTimersAsync();
const stopped = eventBus.emittedEvents.find((e) => e.type === 'agent:stopped');
const stopped = eventBus.emittedEvents.find((e) => e.type === 'agent:stopped') as AgentStoppedEvent | undefined;
expect(stopped?.payload.reason).toBe('context_complete');
});
@@ -682,7 +682,7 @@ describe('MockAgentManager', () => {
});
await vi.runAllTimersAsync();
const stopped = eventBus.emittedEvents.find((e) => e.type === 'agent:stopped');
const stopped = eventBus.emittedEvents.find((e) => e.type === 'agent:stopped') as AgentStoppedEvent | undefined;
expect(stopped?.payload.reason).toBe('breakdown_complete');
});
});

View File

@@ -13,6 +13,7 @@ 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 { Decision, PhaseBreakdown } from '../agent/schema.js';
import type { WorktreeManager, Worktree, WorktreeDiff, MergeResult } from '../git/types.js';
import type { DispatchManager } from '../dispatch/types.js';
import { DefaultDispatchManager } from '../dispatch/manager.js';
@@ -21,10 +22,15 @@ 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 type { InitiativeRepository } from '../db/repositories/initiative-repository.js';
import type { PhaseRepository } from '../db/repositories/phase-repository.js';
import type { Initiative, Phase } from '../db/schema.js';
import {
DrizzleTaskRepository,
DrizzleMessageRepository,
DrizzleAgentRepository,
DrizzleInitiativeRepository,
DrizzlePhaseRepository,
} from '../db/repositories/drizzle/index.js';
import { createTestDatabase } from '../db/repositories/drizzle/test-helpers.js';
import {
@@ -32,6 +38,8 @@ import {
type InitiativeFixture,
type SeededFixture,
} from './fixtures.js';
import { appRouter, createCallerFactory } from '../trpc/router.js';
import { createContext, type TRPCContext } from '../trpc/context.js';
// =============================================================================
// MockWorktreeManager
@@ -149,6 +157,20 @@ export class CapturingEventBus extends EventEmitterBus {
}
}
// =============================================================================
// tRPC Caller Type
// =============================================================================
/**
* Create caller factory for the app router.
*/
const createCaller = createCallerFactory(appRouter);
/**
* Type for the tRPC caller.
*/
export type TRPCCaller = ReturnType<typeof createCaller>;
// =============================================================================
// TestHarness Interface
// =============================================================================
@@ -165,6 +187,8 @@ export interface TestHarness {
eventBus: CapturingEventBus;
/** Mock agent manager */
agentManager: MockAgentManager;
/** Alias for agentManager - used in tests for clarity */
mockAgentManager: MockAgentManager;
/** Mock worktree manager */
worktreeManager: MockWorktreeManager;
/** Real dispatch manager wired to mocks */
@@ -179,6 +203,14 @@ export interface TestHarness {
messageRepository: MessageRepository;
/** Agent repository */
agentRepository: AgentRepository;
/** Initiative repository */
initiativeRepository: InitiativeRepository;
/** Phase repository */
phaseRepository: PhaseRepository;
// tRPC Caller
/** tRPC caller for direct procedure calls */
caller: TRPCCaller;
// Helpers
/**
@@ -230,6 +262,11 @@ export interface TestHarness {
*/
getEventsByType(type: string): DomainEvent[];
/**
* Get emitted events by type (alias for getEventsByType).
*/
getEmittedEvents(type: string): DomainEvent[];
/**
* Clear all captured events.
*/
@@ -239,6 +276,68 @@ export interface TestHarness {
* Clean up all resources.
*/
cleanup(): void;
/**
* Advance fake timers (wrapper for vi.runAllTimersAsync).
* Only works when vi.useFakeTimers() is active.
*/
advanceTimers(): Promise<void>;
// ==========================================================================
// Architect Mode Helpers
// ==========================================================================
/**
* Set up scenario where architect completes discussion with decisions.
*/
setArchitectDiscussComplete(
agentName: string,
decisions: Decision[],
summary: string
): void;
/**
* Set up scenario where architect needs more questions in discuss mode.
*/
setArchitectDiscussQuestions(
agentName: string,
questions: QuestionItem[]
): void;
/**
* Set up scenario where architect completes breakdown with phases.
*/
setArchitectBreakdownComplete(
agentName: string,
phases: PhaseBreakdown[]
): void;
// ==========================================================================
// Initiative/Phase Convenience Helpers
// ==========================================================================
/**
* Get initiative by ID through tRPC.
*/
getInitiative(id: string): Promise<Initiative | null>;
/**
* Get phases for initiative through tRPC.
*/
getPhases(initiativeId: string): Promise<Phase[]>;
/**
* Create initiative through tRPC.
*/
createInitiative(name: string, description?: string): Promise<Initiative>;
/**
* Create phases from breakdown output through tRPC.
*/
createPhasesFromBreakdown(
initiativeId: string,
phases: Array<{ number: number; name: string; description: string }>
): Promise<Phase[]>;
}
// =============================================================================
@@ -256,6 +355,7 @@ export interface TestHarness {
* - Real DefaultDispatchManager (with mock agent manager)
* - Real DefaultCoordinationManager (with mock worktree manager)
* - All repositories (Drizzle implementations)
* - tRPC caller with full context
*/
export function createTestHarness(): TestHarness {
// Create database
@@ -272,6 +372,8 @@ export function createTestHarness(): TestHarness {
const taskRepository = new DrizzleTaskRepository(db);
const messageRepository = new DrizzleMessageRepository(db);
const agentRepository = new DrizzleAgentRepository(db);
const initiativeRepository = new DrizzleInitiativeRepository(db);
const phaseRepository = new DrizzlePhaseRepository(db);
// Create real managers wired to mocks
const dispatchManager = new DefaultDispatchManager(
@@ -289,12 +391,30 @@ export function createTestHarness(): TestHarness {
eventBus
);
// Create tRPC context with all dependencies
const ctx: TRPCContext = createContext({
eventBus,
serverStartedAt: new Date(),
processCount: 0,
agentManager,
taskRepository,
messageRepository,
dispatchManager,
coordinationManager,
initiativeRepository,
phaseRepository,
});
// Create tRPC caller
const caller = createCaller(ctx);
// Build harness
const harness: TestHarness = {
// Core components
db,
eventBus,
agentManager,
mockAgentManager: agentManager, // Alias for clarity in tests
worktreeManager,
dispatchManager,
coordinationManager,
@@ -303,6 +423,11 @@ export function createTestHarness(): TestHarness {
taskRepository,
messageRepository,
agentRepository,
initiativeRepository,
phaseRepository,
// tRPC Caller
caller,
// Helpers
seedFixture: (fixture: InitiativeFixture) => seedFixture(db, fixture),
@@ -342,6 +467,8 @@ export function createTestHarness(): TestHarness {
getEventsByType: (type: string) => eventBus.getEventsByType(type),
getEmittedEvents: (type: string) => eventBus.getEventsByType(type),
clearEvents: () => eventBus.clearEvents(),
cleanup: () => {
@@ -349,6 +476,79 @@ export function createTestHarness(): TestHarness {
worktreeManager.clear();
eventBus.clearEvents();
},
// Timer helper - requires vi.useFakeTimers() to be active
advanceTimers: async () => {
// Dynamic import to avoid vitest dependency at runtime
const { vi } = await import('vitest');
await vi.runAllTimersAsync();
},
// ========================================================================
// Architect Mode Helpers
// ========================================================================
setArchitectDiscussComplete: (
agentName: string,
decisions: Decision[],
summary: string
) => {
agentManager.setScenario(agentName, {
status: 'context_complete',
decisions,
summary,
delay: 0,
});
},
setArchitectDiscussQuestions: (
agentName: string,
questions: QuestionItem[]
) => {
agentManager.setScenario(agentName, {
status: 'questions',
questions,
delay: 0,
});
},
setArchitectBreakdownComplete: (
agentName: string,
phases: PhaseBreakdown[]
) => {
agentManager.setScenario(agentName, {
status: 'breakdown_complete',
phases,
delay: 0,
});
},
// ========================================================================
// Initiative/Phase Convenience Helpers
// ========================================================================
getInitiative: async (id: string) => {
try {
return await caller.getInitiative({ id });
} catch {
return null;
}
},
getPhases: (initiativeId: string) => {
return caller.listPhases({ initiativeId });
},
createInitiative: (name: string, description?: string) => {
return caller.createInitiative({ name, description });
},
createPhasesFromBreakdown: (
initiativeId: string,
phases: Array<{ number: number; name: string; description: string }>
) => {
return caller.createPhasesFromBreakdown({ initiativeId, phases });
},
};
return harness;

View File

@@ -23,4 +23,5 @@ export {
MockWorktreeManager,
CapturingEventBus,
type TestHarness,
type TRPCCaller,
} from './harness.js';