/** * E2E Tests for Phase Parallel Execution * * Tests proving phase dispatch/coordination flow works end-to-end * using the TestHarness with phaseDispatchManager. */ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { createTestHarness, type TestHarness } from '../index.js'; import type { PhaseQueuedEvent, PhaseStartedEvent, PhaseCompletedEvent, PhaseBlockedEvent, } from '../../events/types.js'; describe('Phase Parallel Execution', () => { let harness: TestHarness; beforeEach(() => { harness = createTestHarness(); }); afterEach(() => { harness.cleanup(); }); // =========================================================================== // Test 1: Independent phases dispatch in parallel // =========================================================================== describe('Independent phases dispatch in parallel', () => { it('dispatches multiple independent phases when no dependencies exist', async () => { // Create initiative with 2 independent phases (no dependencies) const initiative = await harness.initiativeRepository.create({ name: 'Independent Phases Test', status: 'active', }); const phaseA = await harness.phaseRepository.create({ initiativeId: initiative.id, name: 'Phase A', content: 'Independent phase A', status: 'pending', }); const phaseB = await harness.phaseRepository.create({ initiativeId: initiative.id, name: 'Phase B', content: 'Independent phase B', status: 'pending', }); // Approve phases before queuing await harness.phaseRepository.update(phaseA.id, { status: 'approved' as const }); await harness.phaseRepository.update(phaseB.id, { status: 'approved' as const }); // Queue both phases await harness.phaseDispatchManager.queuePhase(phaseA.id); await harness.phaseDispatchManager.queuePhase(phaseB.id); // Verify phase:queued events const queuedEvents = harness.getEventsByType('phase:queued'); expect(queuedEvents.length).toBe(2); // Get queue state - both should be ready (no dependencies) const queueState = await harness.phaseDispatchManager.getPhaseQueueState(); expect(queueState.queued.length).toBe(2); expect(queueState.ready.length).toBe(2); expect(queueState.blocked.length).toBe(0); // Both phases should be dispatchable immediately const readyPhaseIds = queueState.ready.map((p) => p.phaseId); expect(readyPhaseIds).toContain(phaseA.id); expect(readyPhaseIds).toContain(phaseB.id); harness.clearEvents(); // Dispatch first phase const result1 = await harness.phaseDispatchManager.dispatchNextPhase(); expect(result1.success).toBe(true); // Dispatch second phase (parallel) const result2 = await harness.phaseDispatchManager.dispatchNextPhase(); expect(result2.success).toBe(true); // Verify both dispatched to different phases expect(result1.phaseId).not.toBe(result2.phaseId); // Verify phase:started events const startedEvents = harness.getEventsByType('phase:started'); expect(startedEvents.length).toBe(2); // Verify both phases are now in_progress const updatedPhaseA = await harness.phaseRepository.findById(phaseA.id); const updatedPhaseB = await harness.phaseRepository.findById(phaseB.id); expect(updatedPhaseA?.status).toBe('in_progress'); expect(updatedPhaseB?.status).toBe('in_progress'); }); }); // =========================================================================== // Test 2: Dependent phase waits for prerequisite // =========================================================================== describe('Dependent phase waits for prerequisite', () => { it('only dispatches phase A first, then B after A completes', async () => { // Create phases: A, B (depends on A) const initiative = await harness.initiativeRepository.create({ name: 'Sequential Phases Test', status: 'active', }); const phaseA = await harness.phaseRepository.create({ initiativeId: initiative.id, name: 'Phase A', content: 'First phase', status: 'pending', }); const phaseB = await harness.phaseRepository.create({ initiativeId: initiative.id, name: 'Phase B', content: 'Second phase, depends on A', status: 'pending', }); // Approve phases before queuing await harness.phaseRepository.update(phaseA.id, { status: 'approved' as const }); await harness.phaseRepository.update(phaseB.id, { status: 'approved' as const }); // Create dependency: B depends on A await harness.phaseRepository.createDependency(phaseB.id, phaseA.id); // Queue both phases await harness.phaseDispatchManager.queuePhase(phaseA.id); await harness.phaseDispatchManager.queuePhase(phaseB.id); // Check queue state - only A should be ready const queueState1 = await harness.phaseDispatchManager.getPhaseQueueState(); expect(queueState1.queued.length).toBe(2); expect(queueState1.ready.length).toBe(1); expect(queueState1.ready[0].phaseId).toBe(phaseA.id); harness.clearEvents(); // Dispatch - should get phase A const result1 = await harness.phaseDispatchManager.dispatchNextPhase(); expect(result1.success).toBe(true); expect(result1.phaseId).toBe(phaseA.id); // Try to dispatch again - should fail (B is blocked by A) const result2 = await harness.phaseDispatchManager.dispatchNextPhase(); expect(result2.success).toBe(false); expect(result2.reason).toBe('No dispatchable phases'); // Verify phase B still in queue but not ready const queueState2 = await harness.phaseDispatchManager.getPhaseQueueState(); expect(queueState2.queued.length).toBe(1); expect(queueState2.ready.length).toBe(0); // Complete phase A await harness.phaseDispatchManager.completePhase(phaseA.id); // Verify phase:completed event for A const completedEvents = harness.getEventsByType('phase:completed'); expect(completedEvents.length).toBe(1); expect((completedEvents[0] as PhaseCompletedEvent).payload.phaseId).toBe(phaseA.id); // Now B should be ready const queueState3 = await harness.phaseDispatchManager.getPhaseQueueState(); expect(queueState3.ready.length).toBe(1); expect(queueState3.ready[0].phaseId).toBe(phaseB.id); harness.clearEvents(); // Dispatch - should get phase B const result3 = await harness.phaseDispatchManager.dispatchNextPhase(); expect(result3.success).toBe(true); expect(result3.phaseId).toBe(phaseB.id); // Verify phase B is now in_progress const updatedPhaseB = await harness.phaseRepository.findById(phaseB.id); expect(updatedPhaseB?.status).toBe('in_progress'); }); }); // =========================================================================== // Test 3: Diamond dependency pattern // =========================================================================== describe('Diamond dependency pattern', () => { it('handles diamond: A -> B,C -> D correctly', async () => { // Create phases: A, B (depends on A), C (depends on A), D (depends on B, C) const initiative = await harness.initiativeRepository.create({ name: 'Diamond Pattern Test', status: 'active', }); const phaseA = await harness.phaseRepository.create({ initiativeId: initiative.id, name: 'Phase A', content: 'Root phase', status: 'pending', }); const phaseB = await harness.phaseRepository.create({ initiativeId: initiative.id, name: 'Phase B', content: 'Depends on A', status: 'pending', }); const phaseC = await harness.phaseRepository.create({ initiativeId: initiative.id, name: 'Phase C', content: 'Depends on A', status: 'pending', }); const phaseD = await harness.phaseRepository.create({ initiativeId: initiative.id, name: 'Phase D', content: 'Depends on B and C', status: 'pending', }); // Approve all phases before queuing await harness.phaseRepository.update(phaseA.id, { status: 'approved' as const }); await harness.phaseRepository.update(phaseB.id, { status: 'approved' as const }); await harness.phaseRepository.update(phaseC.id, { status: 'approved' as const }); await harness.phaseRepository.update(phaseD.id, { status: 'approved' as const }); // Create dependencies await harness.phaseRepository.createDependency(phaseB.id, phaseA.id); await harness.phaseRepository.createDependency(phaseC.id, phaseA.id); await harness.phaseRepository.createDependency(phaseD.id, phaseB.id); await harness.phaseRepository.createDependency(phaseD.id, phaseC.id); // Queue all phases await harness.phaseDispatchManager.queuePhase(phaseA.id); await harness.phaseDispatchManager.queuePhase(phaseB.id); await harness.phaseDispatchManager.queuePhase(phaseC.id); await harness.phaseDispatchManager.queuePhase(phaseD.id); // Step 1: Only A should be ready const state1 = await harness.phaseDispatchManager.getPhaseQueueState(); expect(state1.queued.length).toBe(4); expect(state1.ready.length).toBe(1); expect(state1.ready[0].phaseId).toBe(phaseA.id); // Dispatch A const resultA = await harness.phaseDispatchManager.dispatchNextPhase(); expect(resultA.success).toBe(true); expect(resultA.phaseId).toBe(phaseA.id); // Step 2: After A completes, B and C should be ready (parallel) await harness.phaseDispatchManager.completePhase(phaseA.id); const state2 = await harness.phaseDispatchManager.getPhaseQueueState(); expect(state2.queued.length).toBe(3); // B, C, D still queued expect(state2.ready.length).toBe(2); // B and C ready const readyIds = state2.ready.map((p) => p.phaseId); expect(readyIds).toContain(phaseB.id); expect(readyIds).toContain(phaseC.id); expect(readyIds).not.toContain(phaseD.id); // Dispatch B and C in parallel const resultB = await harness.phaseDispatchManager.dispatchNextPhase(); expect(resultB.success).toBe(true); const resultC = await harness.phaseDispatchManager.dispatchNextPhase(); expect(resultC.success).toBe(true); // Verify D is still not ready (needs both B and C complete) const state3 = await harness.phaseDispatchManager.getPhaseQueueState(); expect(state3.ready.length).toBe(0); expect(state3.queued.length).toBe(1); expect(state3.queued[0].phaseId).toBe(phaseD.id); // Step 3: Complete B only - D still not ready await harness.phaseDispatchManager.completePhase(resultB.phaseId); const state4 = await harness.phaseDispatchManager.getPhaseQueueState(); expect(state4.ready.length).toBe(0); // D still blocked by C // Step 4: Complete C - now D should be ready await harness.phaseDispatchManager.completePhase(resultC.phaseId); const state5 = await harness.phaseDispatchManager.getPhaseQueueState(); expect(state5.ready.length).toBe(1); expect(state5.ready[0].phaseId).toBe(phaseD.id); // Dispatch D const resultD = await harness.phaseDispatchManager.dispatchNextPhase(); expect(resultD.success).toBe(true); expect(resultD.phaseId).toBe(phaseD.id); // Verify D is now in_progress const updatedPhaseD = await harness.phaseRepository.findById(phaseD.id); expect(updatedPhaseD?.status).toBe('in_progress'); }); }); // =========================================================================== // Test 4: Approval gate rejects non-approved phases // =========================================================================== describe('Approval gate rejects non-approved phases', () => { it('rejects queuePhase for pending phase', async () => { const initiative = await harness.initiativeRepository.create({ name: 'Approval Gate Test', status: 'active', }); const phase = await harness.phaseRepository.create({ initiativeId: initiative.id, name: 'Unapproved Phase', status: 'pending', }); await expect( harness.phaseDispatchManager.queuePhase(phase.id) ).rejects.toThrow('must be approved before queuing'); }); it('rejects queuePhase for in_progress phase', async () => { const initiative = await harness.initiativeRepository.create({ name: 'Approval Gate Test 2', status: 'active', }); const phase = await harness.phaseRepository.create({ initiativeId: initiative.id, name: 'In Progress Phase', status: 'in_progress', }); await expect( harness.phaseDispatchManager.queuePhase(phase.id) ).rejects.toThrow('must be approved before queuing'); }); }); // =========================================================================== // Test 5: Blocked phase doesn't dispatch // =========================================================================== describe('Blocked phase does not dispatch', () => { it('prevents dispatch of blocked phase even if dependencies complete', async () => { // Create phases: A, B (depends on A) const initiative = await harness.initiativeRepository.create({ name: 'Blocked Phase Test', status: 'active', }); const phaseA = await harness.phaseRepository.create({ initiativeId: initiative.id, name: 'Phase A', content: 'First phase that will be blocked', status: 'pending', }); const phaseB = await harness.phaseRepository.create({ initiativeId: initiative.id, name: 'Phase B', content: 'Second phase, depends on A', status: 'pending', }); // Approve phases before queuing await harness.phaseRepository.update(phaseA.id, { status: 'approved' as const }); await harness.phaseRepository.update(phaseB.id, { status: 'approved' as const }); // Create dependency: B depends on A await harness.phaseRepository.createDependency(phaseB.id, phaseA.id); // Queue phase A await harness.phaseDispatchManager.queuePhase(phaseA.id); // Block phase A await harness.phaseDispatchManager.blockPhase(phaseA.id, 'External dependency unavailable'); // Verify phase:blocked event const blockedEvents = harness.getEventsByType('phase:blocked'); expect(blockedEvents.length).toBe(1); expect((blockedEvents[0] as PhaseBlockedEvent).payload.phaseId).toBe(phaseA.id); expect((blockedEvents[0] as PhaseBlockedEvent).payload.reason).toBe( 'External dependency unavailable' ); // Try to dispatch - should fail const result = await harness.phaseDispatchManager.dispatchNextPhase(); expect(result.success).toBe(false); expect(result.reason).toBe('No dispatchable phases'); // Verify queue state shows A as blocked const queueState = await harness.phaseDispatchManager.getPhaseQueueState(); expect(queueState.blocked.length).toBe(1); expect(queueState.blocked[0].phaseId).toBe(phaseA.id); expect(queueState.blocked[0].reason).toBe('External dependency unavailable'); // Queue phase B await harness.phaseDispatchManager.queuePhase(phaseB.id); // B should never become ready because A is blocked (not completed) const queueState2 = await harness.phaseDispatchManager.getPhaseQueueState(); expect(queueState2.ready.length).toBe(0); expect(queueState2.queued.length).toBe(1); // Only B is queued (A is blocked, not queued) expect(queueState2.queued[0].phaseId).toBe(phaseB.id); // Try to dispatch B - should fail const resultB = await harness.phaseDispatchManager.dispatchNextPhase(); expect(resultB.success).toBe(false); expect(resultB.reason).toBe('No dispatchable phases'); // Verify phase A status is blocked in database const updatedPhaseA = await harness.phaseRepository.findById(phaseA.id); expect(updatedPhaseA?.status).toBe('blocked'); }); it('blocked phase prevents all downstream phases from dispatching', async () => { // Create chain: A -> B -> C, then block A const initiative = await harness.initiativeRepository.create({ name: 'Chain Block Test', status: 'active', }); const phaseA = await harness.phaseRepository.create({ initiativeId: initiative.id, name: 'Phase A', content: 'Root phase', status: 'pending', }); const phaseB = await harness.phaseRepository.create({ initiativeId: initiative.id, name: 'Phase B', content: 'Depends on A', status: 'pending', }); const phaseC = await harness.phaseRepository.create({ initiativeId: initiative.id, name: 'Phase C', content: 'Depends on B', status: 'pending', }); // Approve all phases before queuing await harness.phaseRepository.update(phaseA.id, { status: 'approved' as const }); await harness.phaseRepository.update(phaseB.id, { status: 'approved' as const }); await harness.phaseRepository.update(phaseC.id, { status: 'approved' as const }); // Create dependency chain: A -> B -> C await harness.phaseRepository.createDependency(phaseB.id, phaseA.id); await harness.phaseRepository.createDependency(phaseC.id, phaseB.id); // Queue all phases await harness.phaseDispatchManager.queuePhase(phaseA.id); await harness.phaseDispatchManager.queuePhase(phaseB.id); await harness.phaseDispatchManager.queuePhase(phaseC.id); // Block phase A await harness.phaseDispatchManager.blockPhase(phaseA.id, 'Resource unavailable'); // Verify only B and C are in queue (A is blocked) const queueState = await harness.phaseDispatchManager.getPhaseQueueState(); expect(queueState.queued.length).toBe(2); expect(queueState.ready.length).toBe(0); // Neither B nor C can dispatch expect(queueState.blocked.length).toBe(1); // Try to dispatch any phase - should fail for all const result = await harness.phaseDispatchManager.dispatchNextPhase(); expect(result.success).toBe(false); expect(result.reason).toBe('No dispatchable phases'); }); }); });