test(06-02): add comprehensive CoordinationManager tests
18 tests covering: - Queue management (queueMerge, getQueueState) - Dependency ordering (getNextMergeable with priority) - Success path (processMerges with clean merges) - Conflict handling (detect conflicts, create resolution task, send message) - Error handling (missing repositories, missing agent) Exports DefaultCoordinationManager from coordination module index.
This commit is contained in:
@@ -21,3 +21,6 @@ export type { CoordinationManager } from './types.js';
|
|||||||
|
|
||||||
// Domain types
|
// Domain types
|
||||||
export type { MergeQueueItem, MergeStatus, MergeResult } from './types.js';
|
export type { MergeQueueItem, MergeStatus, MergeResult } from './types.js';
|
||||||
|
|
||||||
|
// Adapters
|
||||||
|
export { DefaultCoordinationManager } from './manager.js';
|
||||||
|
|||||||
689
src/coordination/manager.test.ts
Normal file
689
src/coordination/manager.test.ts
Normal file
@@ -0,0 +1,689 @@
|
|||||||
|
/**
|
||||||
|
* DefaultCoordinationManager Tests
|
||||||
|
*
|
||||||
|
* Tests for the CoordinationManager adapter with dependency-ordered
|
||||||
|
* merging and conflict handling.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||||
|
import { DefaultCoordinationManager } from './manager.js';
|
||||||
|
import { DrizzleTaskRepository } from '../db/repositories/drizzle/task.js';
|
||||||
|
import { DrizzleAgentRepository } from '../db/repositories/drizzle/agent.js';
|
||||||
|
import { DrizzleMessageRepository } from '../db/repositories/drizzle/message.js';
|
||||||
|
import { DrizzlePlanRepository } from '../db/repositories/drizzle/plan.js';
|
||||||
|
import { DrizzlePhaseRepository } from '../db/repositories/drizzle/phase.js';
|
||||||
|
import { DrizzleInitiativeRepository } from '../db/repositories/drizzle/initiative.js';
|
||||||
|
import { createTestDatabase } from '../db/repositories/drizzle/test-helpers.js';
|
||||||
|
import type { DrizzleDatabase } from '../db/index.js';
|
||||||
|
import type { EventBus, DomainEvent } from '../events/types.js';
|
||||||
|
import type { WorktreeManager, MergeResult as GitMergeResult } from '../git/types.js';
|
||||||
|
import type { TaskRepository } from '../db/repositories/task-repository.js';
|
||||||
|
import type { AgentRepository } from '../db/repositories/agent-repository.js';
|
||||||
|
import type { MessageRepository } from '../db/repositories/message-repository.js';
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Test Helpers
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a mock EventBus that captures emitted events.
|
||||||
|
*/
|
||||||
|
function createMockEventBus(): EventBus & { emittedEvents: DomainEvent[] } {
|
||||||
|
const emittedEvents: DomainEvent[] = [];
|
||||||
|
|
||||||
|
return {
|
||||||
|
emittedEvents,
|
||||||
|
emit<T extends DomainEvent>(event: T): void {
|
||||||
|
emittedEvents.push(event);
|
||||||
|
},
|
||||||
|
on: vi.fn(),
|
||||||
|
off: vi.fn(),
|
||||||
|
once: vi.fn(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a mock WorktreeManager with configurable merge results.
|
||||||
|
*/
|
||||||
|
function createMockWorktreeManager(
|
||||||
|
mergeResults: Map<string, GitMergeResult> = new Map()
|
||||||
|
): WorktreeManager {
|
||||||
|
return {
|
||||||
|
create: vi.fn(),
|
||||||
|
remove: vi.fn(),
|
||||||
|
list: vi.fn().mockResolvedValue([]),
|
||||||
|
get: vi.fn(),
|
||||||
|
diff: vi.fn(),
|
||||||
|
merge: vi.fn().mockImplementation(async (worktreeId: string) => {
|
||||||
|
const result = mergeResults.get(worktreeId);
|
||||||
|
if (result) return result;
|
||||||
|
// Default: successful merge
|
||||||
|
return { success: true, message: 'Merged successfully' };
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Tests
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
describe('DefaultCoordinationManager', () => {
|
||||||
|
let db: DrizzleDatabase;
|
||||||
|
let taskRepository: TaskRepository;
|
||||||
|
let agentRepository: AgentRepository;
|
||||||
|
let messageRepository: MessageRepository;
|
||||||
|
let eventBus: EventBus & { emittedEvents: DomainEvent[] };
|
||||||
|
let worktreeManager: WorktreeManager;
|
||||||
|
let manager: DefaultCoordinationManager;
|
||||||
|
let testPlanId: string;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
// Set up test database
|
||||||
|
db = createTestDatabase();
|
||||||
|
taskRepository = new DrizzleTaskRepository(db);
|
||||||
|
agentRepository = new DrizzleAgentRepository(db);
|
||||||
|
messageRepository = new DrizzleMessageRepository(db);
|
||||||
|
|
||||||
|
// Create required hierarchy for tasks
|
||||||
|
const initiativeRepo = new DrizzleInitiativeRepository(db);
|
||||||
|
const phaseRepo = new DrizzlePhaseRepository(db);
|
||||||
|
const planRepo = new DrizzlePlanRepository(db);
|
||||||
|
|
||||||
|
const initiative = await initiativeRepo.create({
|
||||||
|
name: 'Test Initiative',
|
||||||
|
});
|
||||||
|
const phase = await phaseRepo.create({
|
||||||
|
initiativeId: initiative.id,
|
||||||
|
number: 1,
|
||||||
|
name: 'Test Phase',
|
||||||
|
});
|
||||||
|
const plan = await planRepo.create({
|
||||||
|
phaseId: phase.id,
|
||||||
|
number: 1,
|
||||||
|
name: 'Test Plan',
|
||||||
|
});
|
||||||
|
testPlanId = plan.id;
|
||||||
|
|
||||||
|
// Create mocks
|
||||||
|
eventBus = createMockEventBus();
|
||||||
|
worktreeManager = createMockWorktreeManager();
|
||||||
|
|
||||||
|
// Create coordination manager
|
||||||
|
manager = new DefaultCoordinationManager(
|
||||||
|
worktreeManager,
|
||||||
|
taskRepository,
|
||||||
|
agentRepository,
|
||||||
|
messageRepository,
|
||||||
|
eventBus
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// queueMerge() Tests
|
||||||
|
// ===========================================================================
|
||||||
|
|
||||||
|
describe('queueMerge', () => {
|
||||||
|
it('should add task to queue and emit MergeQueuedEvent', async () => {
|
||||||
|
// Create task
|
||||||
|
const task = await taskRepository.create({
|
||||||
|
planId: testPlanId,
|
||||||
|
name: 'Test Task',
|
||||||
|
priority: 'high',
|
||||||
|
order: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create agent for task
|
||||||
|
const agent = await agentRepository.create({
|
||||||
|
name: 'agent-test',
|
||||||
|
taskId: task.id,
|
||||||
|
worktreeId: 'worktree-123',
|
||||||
|
});
|
||||||
|
|
||||||
|
await manager.queueMerge(task.id);
|
||||||
|
|
||||||
|
const state = await manager.getQueueState();
|
||||||
|
expect(state.queued.length).toBe(1);
|
||||||
|
expect(state.queued[0].taskId).toBe(task.id);
|
||||||
|
expect(state.queued[0].agentId).toBe(agent.id);
|
||||||
|
expect(state.queued[0].worktreeId).toBe('worktree-123');
|
||||||
|
expect(state.queued[0].priority).toBe('high');
|
||||||
|
|
||||||
|
// Check event was emitted
|
||||||
|
expect(eventBus.emittedEvents.length).toBe(1);
|
||||||
|
expect(eventBus.emittedEvents[0].type).toBe('merge:queued');
|
||||||
|
expect((eventBus.emittedEvents[0] as any).payload.taskId).toBe(task.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error when task not found', async () => {
|
||||||
|
await expect(manager.queueMerge('non-existent-id')).rejects.toThrow(
|
||||||
|
'Task not found'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error when no agent assigned to task', async () => {
|
||||||
|
const task = await taskRepository.create({
|
||||||
|
planId: testPlanId,
|
||||||
|
name: 'Orphan Task',
|
||||||
|
order: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(manager.queueMerge(task.id)).rejects.toThrow(
|
||||||
|
'No agent found for task'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// getNextMergeable() Tests
|
||||||
|
// ===========================================================================
|
||||||
|
|
||||||
|
describe('getNextMergeable', () => {
|
||||||
|
it('should return null when queue is empty', async () => {
|
||||||
|
const next = await manager.getNextMergeable();
|
||||||
|
expect(next).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return item when all dependencies merged', async () => {
|
||||||
|
const task = await taskRepository.create({
|
||||||
|
planId: testPlanId,
|
||||||
|
name: 'Mergeable Task',
|
||||||
|
priority: 'medium',
|
||||||
|
order: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
await agentRepository.create({
|
||||||
|
name: 'agent-merge',
|
||||||
|
taskId: task.id,
|
||||||
|
worktreeId: 'worktree-merge',
|
||||||
|
});
|
||||||
|
|
||||||
|
await manager.queueMerge(task.id);
|
||||||
|
|
||||||
|
const next = await manager.getNextMergeable();
|
||||||
|
expect(next).not.toBeNull();
|
||||||
|
expect(next!.taskId).toBe(task.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should respect priority ordering (high > medium > low)', async () => {
|
||||||
|
// Create tasks in different priority order
|
||||||
|
const lowTask = await taskRepository.create({
|
||||||
|
planId: testPlanId,
|
||||||
|
name: 'Low Priority',
|
||||||
|
priority: 'low',
|
||||||
|
order: 1,
|
||||||
|
});
|
||||||
|
const highTask = await taskRepository.create({
|
||||||
|
planId: testPlanId,
|
||||||
|
name: 'High Priority',
|
||||||
|
priority: 'high',
|
||||||
|
order: 2,
|
||||||
|
});
|
||||||
|
const mediumTask = await taskRepository.create({
|
||||||
|
planId: testPlanId,
|
||||||
|
name: 'Medium Priority',
|
||||||
|
priority: 'medium',
|
||||||
|
order: 3,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create agents for all tasks
|
||||||
|
await agentRepository.create({
|
||||||
|
name: 'agent-low',
|
||||||
|
taskId: lowTask.id,
|
||||||
|
worktreeId: 'wt-low',
|
||||||
|
});
|
||||||
|
await agentRepository.create({
|
||||||
|
name: 'agent-high',
|
||||||
|
taskId: highTask.id,
|
||||||
|
worktreeId: 'wt-high',
|
||||||
|
});
|
||||||
|
await agentRepository.create({
|
||||||
|
name: 'agent-medium',
|
||||||
|
taskId: mediumTask.id,
|
||||||
|
worktreeId: 'wt-medium',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Queue in wrong order (low, high, medium)
|
||||||
|
await manager.queueMerge(lowTask.id);
|
||||||
|
await manager.queueMerge(highTask.id);
|
||||||
|
await manager.queueMerge(mediumTask.id);
|
||||||
|
|
||||||
|
// Should get high priority first
|
||||||
|
const next = await manager.getNextMergeable();
|
||||||
|
expect(next).not.toBeNull();
|
||||||
|
expect(next!.taskId).toBe(highTask.id);
|
||||||
|
expect(next!.priority).toBe('high');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should order by queuedAt within same priority (oldest first)', async () => {
|
||||||
|
const task1 = await taskRepository.create({
|
||||||
|
planId: testPlanId,
|
||||||
|
name: 'First Task',
|
||||||
|
priority: 'medium',
|
||||||
|
order: 1,
|
||||||
|
});
|
||||||
|
const task2 = await taskRepository.create({
|
||||||
|
planId: testPlanId,
|
||||||
|
name: 'Second Task',
|
||||||
|
priority: 'medium',
|
||||||
|
order: 2,
|
||||||
|
});
|
||||||
|
|
||||||
|
await agentRepository.create({
|
||||||
|
name: 'agent-1',
|
||||||
|
taskId: task1.id,
|
||||||
|
worktreeId: 'wt-1',
|
||||||
|
});
|
||||||
|
await agentRepository.create({
|
||||||
|
name: 'agent-2',
|
||||||
|
taskId: task2.id,
|
||||||
|
worktreeId: 'wt-2',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Queue first task, wait, then queue second
|
||||||
|
await manager.queueMerge(task1.id);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||||
|
await manager.queueMerge(task2.id);
|
||||||
|
|
||||||
|
// Should get the first queued task
|
||||||
|
const next = await manager.getNextMergeable();
|
||||||
|
expect(next).not.toBeNull();
|
||||||
|
expect(next!.taskId).toBe(task1.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// processMerges() Tests - Success Path
|
||||||
|
// ===========================================================================
|
||||||
|
|
||||||
|
describe('processMerges - success path', () => {
|
||||||
|
it('should complete clean merges and emit MergeCompletedEvent', async () => {
|
||||||
|
const task = await taskRepository.create({
|
||||||
|
planId: testPlanId,
|
||||||
|
name: 'Mergeable Task',
|
||||||
|
priority: 'high',
|
||||||
|
order: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
const agent = await agentRepository.create({
|
||||||
|
name: 'agent-merge',
|
||||||
|
taskId: task.id,
|
||||||
|
worktreeId: 'worktree-success',
|
||||||
|
});
|
||||||
|
|
||||||
|
await manager.queueMerge(task.id);
|
||||||
|
|
||||||
|
const results = await manager.processMerges('main');
|
||||||
|
|
||||||
|
expect(results.length).toBe(1);
|
||||||
|
expect(results[0].taskId).toBe(task.id);
|
||||||
|
expect(results[0].success).toBe(true);
|
||||||
|
|
||||||
|
// Check state updated
|
||||||
|
const state = await manager.getQueueState();
|
||||||
|
expect(state.queued.length).toBe(0);
|
||||||
|
expect(state.merged).toContain(task.id);
|
||||||
|
|
||||||
|
// Check events emitted: queued, started, completed
|
||||||
|
const eventTypes = eventBus.emittedEvents.map((e) => e.type);
|
||||||
|
expect(eventTypes).toContain('merge:queued');
|
||||||
|
expect(eventTypes).toContain('merge:started');
|
||||||
|
expect(eventTypes).toContain('merge:completed');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should process multiple tasks in priority order', async () => {
|
||||||
|
const lowTask = await taskRepository.create({
|
||||||
|
planId: testPlanId,
|
||||||
|
name: 'Low Priority',
|
||||||
|
priority: 'low',
|
||||||
|
order: 1,
|
||||||
|
});
|
||||||
|
const highTask = await taskRepository.create({
|
||||||
|
planId: testPlanId,
|
||||||
|
name: 'High Priority',
|
||||||
|
priority: 'high',
|
||||||
|
order: 2,
|
||||||
|
});
|
||||||
|
|
||||||
|
await agentRepository.create({
|
||||||
|
name: 'agent-low',
|
||||||
|
taskId: lowTask.id,
|
||||||
|
worktreeId: 'wt-low',
|
||||||
|
});
|
||||||
|
await agentRepository.create({
|
||||||
|
name: 'agent-high',
|
||||||
|
taskId: highTask.id,
|
||||||
|
worktreeId: 'wt-high',
|
||||||
|
});
|
||||||
|
|
||||||
|
await manager.queueMerge(lowTask.id);
|
||||||
|
await manager.queueMerge(highTask.id);
|
||||||
|
|
||||||
|
const results = await manager.processMerges('main');
|
||||||
|
|
||||||
|
// Should process high priority first
|
||||||
|
expect(results.length).toBe(2);
|
||||||
|
expect(results[0].taskId).toBe(highTask.id);
|
||||||
|
expect(results[1].taskId).toBe(lowTask.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// processMerges() Tests - Conflict Handling
|
||||||
|
// ===========================================================================
|
||||||
|
|
||||||
|
describe('processMerges - conflict handling', () => {
|
||||||
|
it('should detect conflicts and emit MergeConflictedEvent', async () => {
|
||||||
|
const task = await taskRepository.create({
|
||||||
|
planId: testPlanId,
|
||||||
|
name: 'Conflicting Task',
|
||||||
|
priority: 'high',
|
||||||
|
order: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
await agentRepository.create({
|
||||||
|
name: 'agent-conflict',
|
||||||
|
taskId: task.id,
|
||||||
|
worktreeId: 'wt-conflict',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Configure merge to fail with conflicts
|
||||||
|
const mergeResults = new Map<string, GitMergeResult>();
|
||||||
|
mergeResults.set('wt-conflict', {
|
||||||
|
success: false,
|
||||||
|
conflicts: ['file1.ts', 'file2.ts'],
|
||||||
|
message: 'Merge conflicts detected',
|
||||||
|
});
|
||||||
|
|
||||||
|
worktreeManager = createMockWorktreeManager(mergeResults);
|
||||||
|
manager = new DefaultCoordinationManager(
|
||||||
|
worktreeManager,
|
||||||
|
taskRepository,
|
||||||
|
agentRepository,
|
||||||
|
messageRepository,
|
||||||
|
eventBus
|
||||||
|
);
|
||||||
|
|
||||||
|
await manager.queueMerge(task.id);
|
||||||
|
const results = await manager.processMerges('main');
|
||||||
|
|
||||||
|
expect(results.length).toBe(1);
|
||||||
|
expect(results[0].taskId).toBe(task.id);
|
||||||
|
expect(results[0].success).toBe(false);
|
||||||
|
expect(results[0].conflicts).toEqual(['file1.ts', 'file2.ts']);
|
||||||
|
|
||||||
|
// Check state updated
|
||||||
|
const state = await manager.getQueueState();
|
||||||
|
expect(state.conflicted.length).toBe(1);
|
||||||
|
expect(state.conflicted[0].taskId).toBe(task.id);
|
||||||
|
expect(state.conflicted[0].conflicts).toEqual(['file1.ts', 'file2.ts']);
|
||||||
|
|
||||||
|
// Check MergeConflictedEvent emitted
|
||||||
|
const conflictEvent = eventBus.emittedEvents.find(
|
||||||
|
(e) => e.type === 'merge:conflicted'
|
||||||
|
);
|
||||||
|
expect(conflictEvent).toBeDefined();
|
||||||
|
expect((conflictEvent as any).payload.conflictingFiles).toEqual([
|
||||||
|
'file1.ts',
|
||||||
|
'file2.ts',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create resolution task on conflict', async () => {
|
||||||
|
const task = await taskRepository.create({
|
||||||
|
planId: testPlanId,
|
||||||
|
name: 'Original Task',
|
||||||
|
priority: 'medium',
|
||||||
|
order: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
await agentRepository.create({
|
||||||
|
name: 'agent-conflict',
|
||||||
|
taskId: task.id,
|
||||||
|
worktreeId: 'wt-conflict',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Configure merge to fail
|
||||||
|
const mergeResults = new Map<string, GitMergeResult>();
|
||||||
|
mergeResults.set('wt-conflict', {
|
||||||
|
success: false,
|
||||||
|
conflicts: ['src/index.ts'],
|
||||||
|
message: 'Merge conflicts detected',
|
||||||
|
});
|
||||||
|
|
||||||
|
worktreeManager = createMockWorktreeManager(mergeResults);
|
||||||
|
manager = new DefaultCoordinationManager(
|
||||||
|
worktreeManager,
|
||||||
|
taskRepository,
|
||||||
|
agentRepository,
|
||||||
|
messageRepository,
|
||||||
|
eventBus
|
||||||
|
);
|
||||||
|
|
||||||
|
await manager.queueMerge(task.id);
|
||||||
|
await manager.processMerges('main');
|
||||||
|
|
||||||
|
// Check new task was created
|
||||||
|
const tasks = await taskRepository.findByPlanId(testPlanId);
|
||||||
|
const conflictTask = tasks.find((t) =>
|
||||||
|
t.name.startsWith('Resolve conflicts:')
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(conflictTask).toBeDefined();
|
||||||
|
expect(conflictTask!.name).toBe('Resolve conflicts: Original Task');
|
||||||
|
expect(conflictTask!.priority).toBe('high');
|
||||||
|
expect(conflictTask!.description).toContain('src/index.ts');
|
||||||
|
|
||||||
|
// Check original task blocked
|
||||||
|
const updatedOriginal = await taskRepository.findById(task.id);
|
||||||
|
expect(updatedOriginal!.status).toBe('blocked');
|
||||||
|
|
||||||
|
// Check TaskQueuedEvent emitted for conflict task
|
||||||
|
const queuedEvent = eventBus.emittedEvents.find(
|
||||||
|
(e) =>
|
||||||
|
e.type === 'task:queued' &&
|
||||||
|
(e as any).payload.taskId === conflictTask!.id
|
||||||
|
);
|
||||||
|
expect(queuedEvent).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create message to agent on conflict', async () => {
|
||||||
|
const task = await taskRepository.create({
|
||||||
|
planId: testPlanId,
|
||||||
|
name: 'Task with Message',
|
||||||
|
priority: 'medium',
|
||||||
|
order: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
const agent = await agentRepository.create({
|
||||||
|
name: 'agent-msg',
|
||||||
|
taskId: task.id,
|
||||||
|
worktreeId: 'wt-msg',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Configure merge to fail
|
||||||
|
const mergeResults = new Map<string, GitMergeResult>();
|
||||||
|
mergeResults.set('wt-msg', {
|
||||||
|
success: false,
|
||||||
|
conflicts: ['conflict.ts'],
|
||||||
|
message: 'Merge conflicts detected',
|
||||||
|
});
|
||||||
|
|
||||||
|
worktreeManager = createMockWorktreeManager(mergeResults);
|
||||||
|
manager = new DefaultCoordinationManager(
|
||||||
|
worktreeManager,
|
||||||
|
taskRepository,
|
||||||
|
agentRepository,
|
||||||
|
messageRepository,
|
||||||
|
eventBus
|
||||||
|
);
|
||||||
|
|
||||||
|
await manager.queueMerge(task.id);
|
||||||
|
await manager.processMerges('main');
|
||||||
|
|
||||||
|
// Check message was created
|
||||||
|
const messages = await messageRepository.findByRecipient('agent', agent.id);
|
||||||
|
expect(messages.length).toBe(1);
|
||||||
|
expect(messages[0].recipientType).toBe('agent');
|
||||||
|
expect(messages[0].recipientId).toBe(agent.id);
|
||||||
|
expect(messages[0].senderType).toBe('user');
|
||||||
|
expect(messages[0].content).toContain('Merge conflict detected');
|
||||||
|
expect(messages[0].content).toContain('conflict.ts');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// getQueueState() Tests
|
||||||
|
// ===========================================================================
|
||||||
|
|
||||||
|
describe('getQueueState', () => {
|
||||||
|
it('should return correct counts for all states', async () => {
|
||||||
|
// Create tasks
|
||||||
|
const task1 = await taskRepository.create({
|
||||||
|
planId: testPlanId,
|
||||||
|
name: 'Queued Task',
|
||||||
|
priority: 'high',
|
||||||
|
order: 1,
|
||||||
|
});
|
||||||
|
const task2 = await taskRepository.create({
|
||||||
|
planId: testPlanId,
|
||||||
|
name: 'Conflict Task',
|
||||||
|
priority: 'medium',
|
||||||
|
order: 2,
|
||||||
|
});
|
||||||
|
|
||||||
|
await agentRepository.create({
|
||||||
|
name: 'agent-1',
|
||||||
|
taskId: task1.id,
|
||||||
|
worktreeId: 'wt-1',
|
||||||
|
});
|
||||||
|
await agentRepository.create({
|
||||||
|
name: 'agent-2',
|
||||||
|
taskId: task2.id,
|
||||||
|
worktreeId: 'wt-2',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Configure task2 to conflict
|
||||||
|
const mergeResults = new Map<string, GitMergeResult>();
|
||||||
|
mergeResults.set('wt-2', {
|
||||||
|
success: false,
|
||||||
|
conflicts: ['test.ts'],
|
||||||
|
message: 'Conflict',
|
||||||
|
});
|
||||||
|
|
||||||
|
worktreeManager = createMockWorktreeManager(mergeResults);
|
||||||
|
manager = new DefaultCoordinationManager(
|
||||||
|
worktreeManager,
|
||||||
|
taskRepository,
|
||||||
|
agentRepository,
|
||||||
|
messageRepository,
|
||||||
|
eventBus
|
||||||
|
);
|
||||||
|
|
||||||
|
// Queue both, process task2 (will conflict), leave task1 queued
|
||||||
|
await manager.queueMerge(task2.id);
|
||||||
|
await manager.processMerges('main');
|
||||||
|
await manager.queueMerge(task1.id);
|
||||||
|
|
||||||
|
const state = await manager.getQueueState();
|
||||||
|
|
||||||
|
// task1 should be queued, task2 should be conflicted
|
||||||
|
expect(state.queued.length).toBe(1);
|
||||||
|
expect(state.queued[0].taskId).toBe(task1.id);
|
||||||
|
expect(state.inProgress.length).toBe(0);
|
||||||
|
expect(state.merged.length).toBe(0);
|
||||||
|
expect(state.conflicted.length).toBe(1);
|
||||||
|
expect(state.conflicted[0].taskId).toBe(task2.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// handleConflict() Tests
|
||||||
|
// ===========================================================================
|
||||||
|
|
||||||
|
describe('handleConflict', () => {
|
||||||
|
it('should throw error when task not found', async () => {
|
||||||
|
await expect(
|
||||||
|
manager.handleConflict('non-existent', ['file.ts'])
|
||||||
|
).rejects.toThrow('Original task not found');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error when no agent for task', async () => {
|
||||||
|
const task = await taskRepository.create({
|
||||||
|
planId: testPlanId,
|
||||||
|
name: 'Orphan Task',
|
||||||
|
order: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(manager.handleConflict(task.id, ['file.ts'])).rejects.toThrow(
|
||||||
|
'No agent found for task'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// Error Handling Tests
|
||||||
|
// ===========================================================================
|
||||||
|
|
||||||
|
describe('error handling', () => {
|
||||||
|
it('should throw when TaskRepository not configured', async () => {
|
||||||
|
const managerNoRepo = new DefaultCoordinationManager(
|
||||||
|
worktreeManager,
|
||||||
|
undefined, // No task repo
|
||||||
|
agentRepository,
|
||||||
|
messageRepository,
|
||||||
|
eventBus
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(managerNoRepo.queueMerge('task-id')).rejects.toThrow(
|
||||||
|
'TaskRepository not configured'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw when AgentRepository not configured', async () => {
|
||||||
|
const managerNoAgentRepo = new DefaultCoordinationManager(
|
||||||
|
worktreeManager,
|
||||||
|
taskRepository,
|
||||||
|
undefined, // No agent repo
|
||||||
|
messageRepository,
|
||||||
|
eventBus
|
||||||
|
);
|
||||||
|
|
||||||
|
const task = await taskRepository.create({
|
||||||
|
planId: testPlanId,
|
||||||
|
name: 'Test',
|
||||||
|
order: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(managerNoAgentRepo.queueMerge(task.id)).rejects.toThrow(
|
||||||
|
'AgentRepository not configured'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw when WorktreeManager not configured', async () => {
|
||||||
|
const managerNoWorktree = new DefaultCoordinationManager(
|
||||||
|
undefined, // No worktree manager
|
||||||
|
taskRepository,
|
||||||
|
agentRepository,
|
||||||
|
messageRepository,
|
||||||
|
eventBus
|
||||||
|
);
|
||||||
|
|
||||||
|
const task = await taskRepository.create({
|
||||||
|
planId: testPlanId,
|
||||||
|
name: 'Test',
|
||||||
|
order: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
await agentRepository.create({
|
||||||
|
name: 'agent-test',
|
||||||
|
taskId: task.id,
|
||||||
|
worktreeId: 'wt-test',
|
||||||
|
});
|
||||||
|
|
||||||
|
await managerNoWorktree.queueMerge(task.id);
|
||||||
|
await expect(managerNoWorktree.processMerges('main')).rejects.toThrow(
|
||||||
|
'WorktreeManager not configured'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user