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 { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||||
import { MockAgentManager, type MockAgentScenario } from './mock-manager.js';
|
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
|
// Test Helpers
|
||||||
@@ -632,8 +632,8 @@ describe('MockAgentManager', () => {
|
|||||||
status: 'breakdown_complete',
|
status: 'breakdown_complete',
|
||||||
delay: 0,
|
delay: 0,
|
||||||
phases: [
|
phases: [
|
||||||
{ number: 1, name: 'Foundation', description: 'Core setup' },
|
{ number: 1, name: 'Foundation', description: 'Core setup', dependencies: [] },
|
||||||
{ number: 2, name: 'Features', description: 'Main features' },
|
{ number: 2, name: 'Features', description: 'Main features', dependencies: [1] },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -663,7 +663,7 @@ describe('MockAgentManager', () => {
|
|||||||
});
|
});
|
||||||
await vi.runAllTimersAsync();
|
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');
|
expect(stopped?.payload.reason).toBe('context_complete');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -682,7 +682,7 @@ describe('MockAgentManager', () => {
|
|||||||
});
|
});
|
||||||
await vi.runAllTimersAsync();
|
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');
|
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 type { AgentManager } from '../agent/types.js';
|
||||||
import { MockAgentManager, type MockAgentScenario } from '../agent/mock-manager.js';
|
import { MockAgentManager, type MockAgentScenario } from '../agent/mock-manager.js';
|
||||||
import type { PendingQuestions, QuestionItem } from '../agent/types.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 { WorktreeManager, Worktree, WorktreeDiff, MergeResult } from '../git/types.js';
|
||||||
import type { DispatchManager } from '../dispatch/types.js';
|
import type { DispatchManager } from '../dispatch/types.js';
|
||||||
import { DefaultDispatchManager } from '../dispatch/manager.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 { TaskRepository } from '../db/repositories/task-repository.js';
|
||||||
import type { MessageRepository } from '../db/repositories/message-repository.js';
|
import type { MessageRepository } from '../db/repositories/message-repository.js';
|
||||||
import type { AgentRepository } from '../db/repositories/agent-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 {
|
import {
|
||||||
DrizzleTaskRepository,
|
DrizzleTaskRepository,
|
||||||
DrizzleMessageRepository,
|
DrizzleMessageRepository,
|
||||||
DrizzleAgentRepository,
|
DrizzleAgentRepository,
|
||||||
|
DrizzleInitiativeRepository,
|
||||||
|
DrizzlePhaseRepository,
|
||||||
} from '../db/repositories/drizzle/index.js';
|
} from '../db/repositories/drizzle/index.js';
|
||||||
import { createTestDatabase } from '../db/repositories/drizzle/test-helpers.js';
|
import { createTestDatabase } from '../db/repositories/drizzle/test-helpers.js';
|
||||||
import {
|
import {
|
||||||
@@ -32,6 +38,8 @@ import {
|
|||||||
type InitiativeFixture,
|
type InitiativeFixture,
|
||||||
type SeededFixture,
|
type SeededFixture,
|
||||||
} from './fixtures.js';
|
} from './fixtures.js';
|
||||||
|
import { appRouter, createCallerFactory } from '../trpc/router.js';
|
||||||
|
import { createContext, type TRPCContext } from '../trpc/context.js';
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// MockWorktreeManager
|
// 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
|
// TestHarness Interface
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
@@ -165,6 +187,8 @@ export interface TestHarness {
|
|||||||
eventBus: CapturingEventBus;
|
eventBus: CapturingEventBus;
|
||||||
/** Mock agent manager */
|
/** Mock agent manager */
|
||||||
agentManager: MockAgentManager;
|
agentManager: MockAgentManager;
|
||||||
|
/** Alias for agentManager - used in tests for clarity */
|
||||||
|
mockAgentManager: MockAgentManager;
|
||||||
/** Mock worktree manager */
|
/** Mock worktree manager */
|
||||||
worktreeManager: MockWorktreeManager;
|
worktreeManager: MockWorktreeManager;
|
||||||
/** Real dispatch manager wired to mocks */
|
/** Real dispatch manager wired to mocks */
|
||||||
@@ -179,6 +203,14 @@ export interface TestHarness {
|
|||||||
messageRepository: MessageRepository;
|
messageRepository: MessageRepository;
|
||||||
/** Agent repository */
|
/** Agent repository */
|
||||||
agentRepository: AgentRepository;
|
agentRepository: AgentRepository;
|
||||||
|
/** Initiative repository */
|
||||||
|
initiativeRepository: InitiativeRepository;
|
||||||
|
/** Phase repository */
|
||||||
|
phaseRepository: PhaseRepository;
|
||||||
|
|
||||||
|
// tRPC Caller
|
||||||
|
/** tRPC caller for direct procedure calls */
|
||||||
|
caller: TRPCCaller;
|
||||||
|
|
||||||
// Helpers
|
// Helpers
|
||||||
/**
|
/**
|
||||||
@@ -230,6 +262,11 @@ export interface TestHarness {
|
|||||||
*/
|
*/
|
||||||
getEventsByType(type: string): DomainEvent[];
|
getEventsByType(type: string): DomainEvent[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get emitted events by type (alias for getEventsByType).
|
||||||
|
*/
|
||||||
|
getEmittedEvents(type: string): DomainEvent[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clear all captured events.
|
* Clear all captured events.
|
||||||
*/
|
*/
|
||||||
@@ -239,6 +276,68 @@ export interface TestHarness {
|
|||||||
* Clean up all resources.
|
* Clean up all resources.
|
||||||
*/
|
*/
|
||||||
cleanup(): void;
|
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 DefaultDispatchManager (with mock agent manager)
|
||||||
* - Real DefaultCoordinationManager (with mock worktree manager)
|
* - Real DefaultCoordinationManager (with mock worktree manager)
|
||||||
* - All repositories (Drizzle implementations)
|
* - All repositories (Drizzle implementations)
|
||||||
|
* - tRPC caller with full context
|
||||||
*/
|
*/
|
||||||
export function createTestHarness(): TestHarness {
|
export function createTestHarness(): TestHarness {
|
||||||
// Create database
|
// Create database
|
||||||
@@ -272,6 +372,8 @@ export function createTestHarness(): TestHarness {
|
|||||||
const taskRepository = new DrizzleTaskRepository(db);
|
const taskRepository = new DrizzleTaskRepository(db);
|
||||||
const messageRepository = new DrizzleMessageRepository(db);
|
const messageRepository = new DrizzleMessageRepository(db);
|
||||||
const agentRepository = new DrizzleAgentRepository(db);
|
const agentRepository = new DrizzleAgentRepository(db);
|
||||||
|
const initiativeRepository = new DrizzleInitiativeRepository(db);
|
||||||
|
const phaseRepository = new DrizzlePhaseRepository(db);
|
||||||
|
|
||||||
// Create real managers wired to mocks
|
// Create real managers wired to mocks
|
||||||
const dispatchManager = new DefaultDispatchManager(
|
const dispatchManager = new DefaultDispatchManager(
|
||||||
@@ -289,12 +391,30 @@ export function createTestHarness(): TestHarness {
|
|||||||
eventBus
|
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
|
// Build harness
|
||||||
const harness: TestHarness = {
|
const harness: TestHarness = {
|
||||||
// Core components
|
// Core components
|
||||||
db,
|
db,
|
||||||
eventBus,
|
eventBus,
|
||||||
agentManager,
|
agentManager,
|
||||||
|
mockAgentManager: agentManager, // Alias for clarity in tests
|
||||||
worktreeManager,
|
worktreeManager,
|
||||||
dispatchManager,
|
dispatchManager,
|
||||||
coordinationManager,
|
coordinationManager,
|
||||||
@@ -303,6 +423,11 @@ export function createTestHarness(): TestHarness {
|
|||||||
taskRepository,
|
taskRepository,
|
||||||
messageRepository,
|
messageRepository,
|
||||||
agentRepository,
|
agentRepository,
|
||||||
|
initiativeRepository,
|
||||||
|
phaseRepository,
|
||||||
|
|
||||||
|
// tRPC Caller
|
||||||
|
caller,
|
||||||
|
|
||||||
// Helpers
|
// Helpers
|
||||||
seedFixture: (fixture: InitiativeFixture) => seedFixture(db, fixture),
|
seedFixture: (fixture: InitiativeFixture) => seedFixture(db, fixture),
|
||||||
@@ -342,6 +467,8 @@ export function createTestHarness(): TestHarness {
|
|||||||
|
|
||||||
getEventsByType: (type: string) => eventBus.getEventsByType(type),
|
getEventsByType: (type: string) => eventBus.getEventsByType(type),
|
||||||
|
|
||||||
|
getEmittedEvents: (type: string) => eventBus.getEventsByType(type),
|
||||||
|
|
||||||
clearEvents: () => eventBus.clearEvents(),
|
clearEvents: () => eventBus.clearEvents(),
|
||||||
|
|
||||||
cleanup: () => {
|
cleanup: () => {
|
||||||
@@ -349,6 +476,79 @@ export function createTestHarness(): TestHarness {
|
|||||||
worktreeManager.clear();
|
worktreeManager.clear();
|
||||||
eventBus.clearEvents();
|
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;
|
return harness;
|
||||||
|
|||||||
@@ -23,4 +23,5 @@ export {
|
|||||||
MockWorktreeManager,
|
MockWorktreeManager,
|
||||||
CapturingEventBus,
|
CapturingEventBus,
|
||||||
type TestHarness,
|
type TestHarness,
|
||||||
|
type TRPCCaller,
|
||||||
} from './harness.js';
|
} from './harness.js';
|
||||||
|
|||||||
Reference in New Issue
Block a user