From 021937c28d19729c7f02870128207a05bf4333ad Mon Sep 17 00:00:00 2001 From: Lukas May Date: Sat, 31 Jan 2026 19:26:46 +0100 Subject: [PATCH] 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. --- src/agent/mock-manager.test.ts | 10 +- src/test/harness.ts | 200 +++++++++++++++++++++++++++++++++ src/test/index.ts | 1 + 3 files changed, 206 insertions(+), 5 deletions(-) diff --git a/src/agent/mock-manager.test.ts b/src/agent/mock-manager.test.ts index 2b8efee..05a5043 100644 --- a/src/agent/mock-manager.test.ts +++ b/src/agent/mock-manager.test.ts @@ -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'); }); }); diff --git a/src/test/harness.ts b/src/test/harness.ts index 60da30e..f8bdeed 100644 --- a/src/test/harness.ts +++ b/src/test/harness.ts @@ -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; + // ============================================================================= // 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; + + // ========================================================================== + // 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; + + /** + * Get phases for initiative through tRPC. + */ + getPhases(initiativeId: string): Promise; + + /** + * Create initiative through tRPC. + */ + createInitiative(name: string, description?: string): Promise; + + /** + * Create phases from breakdown output through tRPC. + */ + createPhasesFromBreakdown( + initiativeId: string, + phases: Array<{ number: number; name: string; description: string }> + ): Promise; } // ============================================================================= @@ -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; diff --git a/src/test/index.ts b/src/test/index.ts index 31c4e21..e5f94be 100644 --- a/src/test/index.ts +++ b/src/test/index.ts @@ -23,4 +23,5 @@ export { MockWorktreeManager, CapturingEventBus, type TestHarness, + type TRPCCaller, } from './harness.js';