import com.correx.core.events.events.OrchestrationPausedEvent import com.correx.core.events.events.OrchestrationResumedEvent import com.correx.core.events.events.SteeringNoteAddedEvent import com.correx.core.events.events.StageCompletedEvent import com.correx.core.events.events.StageFailedEvent import com.correx.core.events.events.ToolInvokedEvent import com.correx.core.events.events.WorkflowCompletedEvent import com.correx.core.events.events.WorkflowFailedEvent import com.correx.core.events.events.WorkflowStartedEvent import com.correx.core.router.DefaultRouterReducer import com.correx.core.router.model.StageOutcomeKind import com.correx.core.router.model.WorkflowStatus import com.correx.core.events.types.SessionId import com.correx.core.events.types.StageId import com.correx.testing.fixtures.EventFixtures.stored import kotlinx.datetime.Instant import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertNotNull import org.junit.jupiter.api.Assertions.assertNull import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test class RouterReducerTest { private val reducer = DefaultRouterReducer() private val sessionId = SessionId("s1") private val stageId = StageId("stage-1") @Test fun `initial state has IDLE status and empty collections`() { val state = reducer.initial assertEquals(WorkflowStatus.IDLE, state.workflowStatus) assertNull(state.sessionId) assertNull(state.currentStageId) assertTrue(state.l2Memory.isEmpty()) assertTrue(state.conversationHistory.isEmpty()) } @Test fun `WorkflowStartedEvent sets sessionId, RUNNING status and startStageId`() { val state = reducer.initial val updated = reducer.reduce( state, stored(payload = WorkflowStartedEvent(sessionId, stageId)), ) assertEquals(sessionId, updated.sessionId) assertEquals(WorkflowStatus.RUNNING, updated.workflowStatus) assertEquals(stageId, updated.currentStageId) } @Test fun `WorkflowCompletedEvent sets COMPLETED and clears currentStageId`() { val initial = reducer.initial val started = reducer.reduce( initial, stored(payload = WorkflowStartedEvent(sessionId, stageId)), ) val completed = reducer.reduce( started, stored(payload = WorkflowCompletedEvent(sessionId, stageId, 3)), ) assertEquals(WorkflowStatus.COMPLETED, completed.workflowStatus) assertNull(completed.currentStageId) assertEquals(sessionId, completed.sessionId) } @Test fun `WorkflowFailedEvent sets FAILED and clears currentStageId`() { val initial = reducer.initial val started = reducer.reduce( initial, stored(payload = WorkflowStartedEvent(sessionId, stageId)), ) val failed = reducer.reduce( started, stored(payload = WorkflowFailedEvent(sessionId, stageId, "timeout", false)), ) assertEquals(WorkflowStatus.FAILED, failed.workflowStatus) assertNull(failed.currentStageId) } @Test fun `OrchestrationPausedEvent sets PAUSED status`() { val initial = reducer.initial val started = reducer.reduce( initial, stored(payload = WorkflowStartedEvent(sessionId, stageId)), ) val paused = reducer.reduce( started, stored(payload = OrchestrationPausedEvent(sessionId, stageId, "APPROVAL_PENDING")), ) assertEquals(WorkflowStatus.PAUSED, paused.workflowStatus) assertEquals(sessionId, paused.sessionId) } @Test fun `OrchestrationResumedEvent sets RUNNING status from PAUSED`() { val initial = reducer.initial val started = reducer.reduce( initial, stored(payload = WorkflowStartedEvent(sessionId, stageId)), ) val paused = reducer.reduce( started, stored(payload = OrchestrationPausedEvent(sessionId, stageId, "APPROVAL_PENDING")), ) val resumed = reducer.reduce( paused, stored(payload = OrchestrationResumedEvent(sessionId, stageId)), ) assertEquals(WorkflowStatus.RUNNING, resumed.workflowStatus) } @Test fun `StageCompletedEvent appends RouterL2Entry with SUCCESS to l2Memory`() { val state = reducer.initial val storedEvent = stored(payload = StageCompletedEvent(sessionId, stageId, StageId("t1"))) val updated = reducer.reduce(state, storedEvent) assertEquals(1, updated.l2Memory.size) val entry = updated.l2Memory[0] assertEquals(stageId, entry.stageId) assertEquals(StageOutcomeKind.SUCCESS, entry.outcome) assertTrue(entry.summary.contains("completed")) assertNotNull(entry.timestamp) } @Test fun `StageCompletedEvent preserves other state fields`() { val initial = reducer.initial val started = reducer.reduce( initial, stored(payload = WorkflowStartedEvent(sessionId, stageId)), ) val updated = reducer.reduce( started, stored(payload = StageCompletedEvent(sessionId, StageId("stage-2"), StageId("t1"))), ) assertEquals(sessionId, updated.sessionId) assertEquals(WorkflowStatus.RUNNING, updated.workflowStatus) assertEquals(stageId, updated.currentStageId) } @Test fun `StageFailedEvent appends RouterL2Entry with FAILURE and clears currentStageId`() { val initial = reducer.initial val started = reducer.reduce( initial, stored(payload = WorkflowStartedEvent(sessionId, stageId)), ) val storedEvent = stored(payload = StageFailedEvent(sessionId, stageId, StageId("t1"), "timeout")) val updated = reducer.reduce(started, storedEvent) assertEquals(1, updated.l2Memory.size) val entry = updated.l2Memory[0] assertEquals(stageId, entry.stageId) assertEquals(StageOutcomeKind.FAILURE, entry.outcome) assertTrue(entry.summary.contains("timeout")) assertNull(updated.currentStageId) } @Test fun `multiple StageCompletedEvents append to l2Memory sequentially`() { val state = reducer.initial val s1 = reducer.reduce(state, stored(payload = StageCompletedEvent(sessionId, StageId("a"), StageId("t1")))) val s2 = reducer.reduce(s1, stored(payload = StageCompletedEvent(sessionId, StageId("b"), StageId("t2")))) val s3 = reducer.reduce(s2, stored(payload = StageCompletedEvent(sessionId, StageId("c"), StageId("t3")))) assertEquals(3, s3.l2Memory.size) assertEquals(StageId("a"), s3.l2Memory[0].stageId) assertEquals(StageId("b"), s3.l2Memory[1].stageId) assertEquals(StageId("c"), s3.l2Memory[2].stageId) s3.l2Memory.forEach { assertEquals(StageOutcomeKind.SUCCESS, it.outcome) } } @Test fun `SteeringNoteAddedEvent does not change state`() { val state = reducer.initial val updated = reducer.reduce( state, stored(payload = SteeringNoteAddedEvent(sessionId, "do something different")), ) assertEquals(state, updated) assertTrue(updated.conversationHistory.isEmpty()) } @Test fun `SteeringNoteAddedEvent on non-initial state does not change state`() { val initial = reducer.initial val started = reducer.reduce( initial, stored(payload = WorkflowStartedEvent(sessionId, stageId)), ) val withNote = reducer.reduce( started, stored(payload = SteeringNoteAddedEvent(sessionId, "shift focus")), ) assertEquals(started, withNote) assertEquals(WorkflowStatus.RUNNING, withNote.workflowStatus) assertEquals(stageId, withNote.currentStageId) } @Test fun `unknown event type returns unchanged state`() { val state = reducer.initial val updated = reducer.reduce(state, stored(payload = ToolInvokedEvent("test"))) assertEquals(state, updated) } @Test fun `l2Memory entry preserves timestamp from event metadata`() { val storedEvent = stored( payload = StageCompletedEvent(sessionId, stageId, StageId("t1")), timestamp = Instant.parse("2026-06-15T12:00:00Z"), ) val updated = reducer.reduce(reducer.initial, storedEvent) assertEquals(1, updated.l2Memory.size) assertNotNull(updated.l2Memory[0].timestamp) } @Test fun `full lifecycle start complete pause resume start another stage fail`() { val s0 = reducer.initial assertEquals(WorkflowStatus.IDLE, s0.workflowStatus) val s1 = reducer.reduce(s0, stored(payload = WorkflowStartedEvent(sessionId, StageId("stage-A")))) assertEquals(WorkflowStatus.RUNNING, s1.workflowStatus) assertEquals(StageId("stage-A"), s1.currentStageId) val s2 = reducer.reduce(s1, stored(payload = StageCompletedEvent(sessionId, StageId("stage-A"), StageId("t1")))) assertEquals(1, s2.l2Memory.size) assertEquals(StageOutcomeKind.SUCCESS, s2.l2Memory[0].outcome) val s3 = reducer.reduce(s2, stored(payload = WorkflowCompletedEvent(sessionId, StageId("stage-A"), 1))) assertEquals(WorkflowStatus.COMPLETED, s3.workflowStatus) assertNull(s3.currentStageId) val s4 = reducer.reduce(s3, stored(payload = OrchestrationPausedEvent(sessionId, StageId("stage-A"), "manual pause"))) assertEquals(WorkflowStatus.PAUSED, s4.workflowStatus) val s5 = reducer.reduce(s4, stored(payload = OrchestrationResumedEvent(sessionId, StageId("stage-A")))) assertEquals(WorkflowStatus.RUNNING, s5.workflowStatus) val s6 = reducer.reduce( s5, stored(payload = StageFailedEvent(sessionId, StageId("stage-B"), StageId("t2"), "llm error")), ) assertEquals(StageOutcomeKind.FAILURE, s6.l2Memory[s6.l2Memory.size - 1].outcome) assertNull(s6.currentStageId) } }