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:
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -23,4 +23,5 @@ export {
|
||||
MockWorktreeManager,
|
||||
CapturingEventBus,
|
||||
type TestHarness,
|
||||
type TRPCCaller,
|
||||
} from './harness.js';
|
||||
|
||||
Reference in New Issue
Block a user