diff --git a/src/test/e2e/edge-cases.test.ts b/src/test/e2e/edge-cases.test.ts index dd34269..e7f1b6f 100644 --- a/src/test/e2e/edge-cases.test.ts +++ b/src/test/e2e/edge-cases.test.ts @@ -338,4 +338,186 @@ describe('E2E Edge Cases', () => { expect(task?.status).toBe('blocked'); }); }); + + describe('Merge conflict handling', () => { + it('detects merge conflict and emits merge:conflicted event', async () => { + const seeded = await harness.seedFixture(SIMPLE_FIXTURE); + const taskAId = seeded.tasks.get('Task A')!; + + // Mark task as completed (required for merge) + await harness.taskRepository.update(taskAId, { status: 'completed' }); + + // Create a worktree for this task + const worktreeId = `wt-${taskAId.slice(0, 6)}`; + await harness.worktreeManager.create(worktreeId, 'feature-task-a'); + + // Create agent in agentRepository with worktreeId + // (coordinationManager.queueMerge looks up agent by taskId) + const agent = await harness.agentRepository.create({ + name: `agent-${taskAId.slice(0, 6)}`, + worktreeId, + taskId: taskAId, + status: 'idle', + }); + + // Set up merge conflict result BEFORE processMerges + harness.worktreeManager.setMergeResult(worktreeId, { + success: false, + conflicts: ['src/shared.ts', 'src/types.ts'], + message: 'Merge conflict in 2 files', + }); + + // Queue for merge + await harness.coordinationManager.queueMerge(taskAId); + harness.clearEvents(); + + // Process merges - should hit conflict + const results = await harness.coordinationManager.processMerges('main'); + + // Verify: merge result indicates failure + expect(results.length).toBe(1); + expect(results[0].success).toBe(false); + expect(results[0].conflicts).toEqual(['src/shared.ts', 'src/types.ts']); + + // Verify: merge:conflicted event emitted + const conflictEvents = harness.getEventsByType('merge:conflicted'); + expect(conflictEvents.length).toBe(1); + const conflictPayload = (conflictEvents[0] as MergeConflictedEvent).payload; + expect(conflictPayload.taskId).toBe(taskAId); + expect(conflictPayload.conflictingFiles).toEqual(['src/shared.ts', 'src/types.ts']); + }); + + it('conflict appears in queue state as conflicted', async () => { + const seeded = await harness.seedFixture(SIMPLE_FIXTURE); + const taskAId = seeded.tasks.get('Task A')!; + + // Mark task as completed + await harness.taskRepository.update(taskAId, { status: 'completed' }); + + // Create worktree + const worktreeId = `wt-${taskAId.slice(0, 6)}`; + await harness.worktreeManager.create(worktreeId, 'feature-task-a'); + + // Create agent in agentRepository + await harness.agentRepository.create({ + name: `agent-${taskAId.slice(0, 6)}`, + worktreeId, + taskId: taskAId, + status: 'idle', + }); + + // Set up merge conflict + harness.worktreeManager.setMergeResult(worktreeId, { + success: false, + conflicts: ['src/shared.ts'], + message: 'Merge conflict', + }); + + // Queue and process + await harness.coordinationManager.queueMerge(taskAId); + await harness.coordinationManager.processMerges('main'); + + // Check queue state + const queueState = await harness.coordinationManager.getQueueState(); + expect(queueState.conflicted.length).toBe(1); + expect(queueState.conflicted[0].taskId).toBe(taskAId); + expect(queueState.conflicted[0].conflicts).toContain('src/shared.ts'); + }); + + it('handleConflict creates conflict-resolution task', async () => { + const seeded = await harness.seedFixture(SIMPLE_FIXTURE); + const taskAId = seeded.tasks.get('Task A')!; + + // Mark task as completed + await harness.taskRepository.update(taskAId, { status: 'completed' }); + + // Create worktree + const worktreeId = `wt-${taskAId.slice(0, 6)}`; + await harness.worktreeManager.create(worktreeId, 'feature-task-a'); + + // Create agent in agentRepository + await harness.agentRepository.create({ + name: `agent-${taskAId.slice(0, 6)}`, + worktreeId, + taskId: taskAId, + status: 'idle', + }); + + // Set up merge conflict + harness.worktreeManager.setMergeResult(worktreeId, { + success: false, + conflicts: ['src/shared.ts', 'src/types.ts'], + message: 'Merge conflict', + }); + + // Queue and process (handleConflict is called automatically) + await harness.coordinationManager.queueMerge(taskAId); + await harness.coordinationManager.processMerges('main'); + + // Verify: original task is now blocked + const originalTask = await harness.taskRepository.findById(taskAId); + expect(originalTask?.status).toBe('blocked'); + + // Verify: task:queued event emitted for conflict resolution task + const queuedEvents = harness.getEventsByType('task:queued'); + const conflictTaskEvent = queuedEvents.find( + (e) => e.payload && (e.payload as { taskId: string }).taskId !== taskAId + ); + expect(conflictTaskEvent).toBeDefined(); + }); + + it('successful merge after clearing conflict result', async () => { + const seeded = await harness.seedFixture(SIMPLE_FIXTURE); + const taskAId = seeded.tasks.get('Task A')!; + const taskBId = seeded.tasks.get('Task B')!; + + // Set up Task A for merge (with conflict) + await harness.taskRepository.update(taskAId, { status: 'completed' }); + const worktreeIdA = `wt-${taskAId.slice(0, 6)}`; + await harness.worktreeManager.create(worktreeIdA, 'feature-task-a'); + await harness.agentRepository.create({ + name: `agent-${taskAId.slice(0, 6)}`, + worktreeId: worktreeIdA, + taskId: taskAId, + status: 'idle', + }); + + // Set conflict for Task A + harness.worktreeManager.setMergeResult(worktreeIdA, { + success: false, + conflicts: ['src/shared.ts'], + message: 'Merge conflict', + }); + + // Process Task A merge (will conflict) + await harness.coordinationManager.queueMerge(taskAId); + const conflictResults = await harness.coordinationManager.processMerges('main'); + expect(conflictResults[0].success).toBe(false); + + // Now set up Task B for merge (should succeed) + await harness.taskRepository.update(taskBId, { status: 'completed' }); + const worktreeIdB = `wt-${taskBId.slice(0, 6)}`; + await harness.worktreeManager.create(worktreeIdB, 'feature-task-b'); + await harness.agentRepository.create({ + name: `agent-${taskBId.slice(0, 6)}`, + worktreeId: worktreeIdB, + taskId: taskBId, + status: 'idle', + }); + + // Task B merge should succeed (default behavior) + await harness.coordinationManager.queueMerge(taskBId); + harness.clearEvents(); + const successResults = await harness.coordinationManager.processMerges('main'); + + // Verify Task B merged successfully + expect(successResults.length).toBe(1); + expect(successResults[0].taskId).toBe(taskBId); + expect(successResults[0].success).toBe(true); + + // Verify Task B in merged list + const queueState = await harness.coordinationManager.getQueueState(); + expect(queueState.merged).toContain(taskBId); + }); + }); });