import com.correx.core.events.events.OrchestrationPausedEvent import com.correx.core.events.events.OrchestrationResumedEvent import com.correx.core.events.events.SessionWorkspaceBoundEvent import com.correx.core.events.events.StageCompletedEvent import com.correx.core.events.events.StageFailedEvent import com.correx.core.events.events.TransitionExecutedEvent 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.events.types.SessionId import com.correx.core.events.types.StageId import com.correx.core.events.types.TransitionId import com.correx.core.events.events.AgentInstructionsBoundEvent import com.correx.core.events.events.OperatorProfileBoundEvent import com.correx.core.events.events.ProjectProfileBoundEvent import com.correx.core.sessions.BoundAgentInstructions import com.correx.core.sessions.BoundProfile import com.correx.core.sessions.BoundProjectProfile import com.correx.core.sessions.BoundWorkspace import com.correx.core.sessions.DefaultSessionReducer import com.correx.core.sessions.SessionState import com.correx.core.sessions.SessionStatus import com.correx.testing.fixtures.EventFixtures.stored import kotlinx.datetime.Instant import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test class DefaultSessionReducerTest { private val reducer = DefaultSessionReducer() private val sessionId = SessionId("session-1") @Test fun `WorkflowStartedEvent leaves CREATED status`() { val state = initialState() val result = reducer.reduce( state = state, event = stored( sessionId = sessionId, payload = WorkflowStartedEvent(sessionId, workflowId = "test-wf", startStageId = StageId("st-1")) ) ) assertEquals(SessionStatus.CREATED, result.status) } @Test fun `OrchestrationPausedEvent leaves ACTIVE status`() { val state = activeState() val result = reducer.reduce( state = state, event = stored( sessionId = sessionId, payload = OrchestrationPausedEvent(sessionId, stageId = StageId("st-1"), reason = "APPROVAL_PENDING") ) ) assertEquals(SessionStatus.ACTIVE, result.status) } @Test fun `OrchestrationResumedEvent leaves PAUSED status`() { val state = pausedState() val result = reducer.reduce( state = state, event = stored( sessionId = sessionId, payload = OrchestrationResumedEvent(sessionId, stageId = StageId("st-1")) ) ) assertEquals(SessionStatus.PAUSED, result.status) } @Test fun `WorkflowCompletedEvent flips to COMPLETED status`() { val state = activeState() val result = reducer.reduce( state = state, event = stored( sessionId = sessionId, payload = WorkflowCompletedEvent(sessionId, terminalStageId = StageId("st-1"), totalStages = 0) ) ) assertEquals(SessionStatus.COMPLETED, result.status) } @Test fun `WorkflowFailedEvent flips to FAILED status`() { val state = activeState() val result = reducer.reduce( state = state, event = stored( sessionId = sessionId, payload = WorkflowFailedEvent(sessionId, stageId = StageId("st-1"), reason = "error", retryExhausted = false) ) ) assertEquals(SessionStatus.FAILED, result.status) } @Test fun `TransitionExecutedEvent from StageStartedEvent sets ACTIVE status`() { val state = pausedState() val result = reducer.reduce( state = state, event = stored( sessionId = sessionId, payload = TransitionExecutedEvent( sessionId, from = StageId("st-1"), to = StageId("st-1"), transitionId = TransitionId("transition-a") ) ) ) assertEquals(SessionStatus.ACTIVE, result.status) } @Test fun `StageCompletedEvent sets ACTIVE status`() { val state = pausedState() val result = reducer.reduce( state = state, event = stored( sessionId = sessionId, payload = StageCompletedEvent( sessionId, stageId = StageId("stage-a"), transitionId = TransitionId("transition-a") ) ) ) assertEquals(SessionStatus.ACTIVE, result.status) } @Test fun `StageFailedEvent sets FAILED status`() { val state = activeState() val result = reducer.reduce( state = state, event = stored( sessionId = sessionId, payload = StageFailedEvent( sessionId, stageId = StageId("stage-a"), transitionId = TransitionId("transition-a"), reason = "boom" ) ) ) assertEquals(SessionStatus.FAILED, result.status) } @Test fun `TransitionExecutedEvent sets ACTIVE status`() { val state = pausedState() val result = reducer.reduce( state = state, event = stored( sessionId = sessionId, payload = TransitionExecutedEvent( sessionId, from = StageId("stage-a"), to = StageId("stage-b"), transitionId = TransitionId("transition-a"), ) ) ) assertEquals(SessionStatus.ACTIVE, result.status) } @Test fun `createdAt is initialized once`() { val timestamp = Instant.parse("2026-01-01T00:00:00Z") val result = reducer.reduce( state = initialState(), event = stored( payload = WorkflowStartedEvent(sessionId, workflowId = "test-wf", startStageId = StageId("st-1")), timestamp = timestamp ) ) assertEquals(timestamp, result.createdAt) } @Test fun `createdAt is preserved after initialization`() { val createdAt = Instant.parse("2026-01-01T00:00:00Z") val updatedAt = Instant.parse("2026-01-02T00:00:00Z") val state = activeState().copy( createdAt = createdAt ) val result = reducer.reduce( state = state, event = stored( payload = OrchestrationPausedEvent(sessionId, stageId = StageId("st-1"), reason = "APPROVAL_PENDING"), timestamp = updatedAt ) ) assertEquals(createdAt, result.createdAt) } @Test fun `updatedAt always reflects latest event timestamp`() { val timestamp = Instant.parse("2026-01-02T00:00:00Z") val result = reducer.reduce( state = activeState(), event = stored( payload = OrchestrationPausedEvent(sessionId, stageId = StageId("st-1"), reason = "APPROVAL_PENDING"), timestamp = timestamp ) ) assertEquals(timestamp, result.updatedAt) } @Test fun `SessionWorkspaceBoundEvent sets boundWorkspace in state`() { val result = reducer.reduce( state = initialState(), event = stored( sessionId = sessionId, payload = SessionWorkspaceBoundEvent( sessionId = sessionId, workspaceRoot = "/home/user/project", allowedPaths = listOf("/home/user/project"), ) ) ) assertEquals( BoundWorkspace( workspaceRoot = "/home/user/project", allowedPaths = listOf("/home/user/project"), ), result.boundWorkspace, ) } @Test fun `boundWorkspace is preserved by subsequent unrelated events`() { val withWorkspace = initialState().copy( boundWorkspace = BoundWorkspace( workspaceRoot = "/home/user/project", allowedPaths = listOf("/home/user/project"), ) ) val result = reducer.reduce( state = withWorkspace, event = stored( sessionId = sessionId, payload = WorkflowStartedEvent(sessionId, workflowId = "wf", startStageId = StageId("s1")) ) ) assertEquals( BoundWorkspace( workspaceRoot = "/home/user/project", allowedPaths = listOf("/home/user/project"), ), result.boundWorkspace, ) } @Test fun `ProjectProfileBoundEvent reduces into boundProjectProfile`() { val result = reducer.reduce( state = initialState(), event = stored( sessionId = sessionId, payload = ProjectProfileBoundEvent( sessionId = sessionId, workspaceRoot = "/repo", about = "kernel project", conventions = listOf("no bare try-catch"), commands = mapOf("test" to "./gradlew check"), ), ), ) assertEquals( BoundProjectProfile( about = "kernel project", conventions = listOf("no bare try-catch"), commands = mapOf("test" to "./gradlew check"), ), result.boundProjectProfile, ) } @Test fun `boundProjectProfile is preserved by subsequent unrelated events`() { val withProfile = initialState().copy( boundProjectProfile = BoundProjectProfile( about = "kernel project", conventions = listOf("no bare try-catch"), commands = emptyMap(), ), ) val result = reducer.reduce( state = withProfile, event = stored( sessionId = sessionId, payload = WorkflowStartedEvent(sessionId, workflowId = "wf", startStageId = StageId("s1")), ), ) assertEquals("kernel project", result.boundProjectProfile?.about) } @Test fun `AgentInstructionsBoundEvent reduces into boundAgentInstructions`() { val result = reducer.reduce( state = initialState(), event = stored( sessionId = sessionId, payload = AgentInstructionsBoundEvent( sessionId = sessionId, workspaceRoot = "/repo", sources = listOf("CLAUDE.md", "AGENTS.md"), content = "# CLAUDE.md\n\nbe careful", ), ), ) assertEquals( BoundAgentInstructions( sources = listOf("CLAUDE.md", "AGENTS.md"), content = "# CLAUDE.md\n\nbe careful", ), result.boundAgentInstructions, ) } @Test fun `boundAgentInstructions is preserved by subsequent unrelated events`() { val withInstructions = initialState().copy( boundAgentInstructions = BoundAgentInstructions( sources = listOf("CLAUDE.md"), content = "# CLAUDE.md\n\nrules", ), ) val result = reducer.reduce( state = withInstructions, event = stored( sessionId = sessionId, payload = WorkflowStartedEvent(sessionId, workflowId = "wf", startStageId = StageId("s1")), ), ) assertEquals(listOf("CLAUDE.md"), result.boundAgentInstructions?.sources) } private fun initialState() = SessionState( status = SessionStatus.CREATED ) @Test fun `OperatorProfileBoundEvent populates boundProfile`() { val result = reducer.reduce( state = activeState(), event = stored( sessionId = sessionId, payload = OperatorProfileBoundEvent( sessionId = sessionId, about = "Expert Kotlin engineer", approvalMode = "auto", preferredModels = listOf("model-x"), conventions = listOf("no var"), ), ), ) val profile = result.boundProfile assertEquals("Expert Kotlin engineer", profile?.about) assertEquals("auto", profile?.approvalMode) assertEquals(listOf("model-x"), profile?.preferredModels) assertEquals(listOf("no var"), profile?.conventions) } @Test fun `unrelated event leaves boundProfile unchanged`() { val state = activeState().copy( boundProfile = BoundProfile( about = "original", approvalMode = "", preferredModels = emptyList(), conventions = emptyList(), ), ) val result = reducer.reduce( state = state, event = stored( sessionId = sessionId, payload = SessionWorkspaceBoundEvent( sessionId = sessionId, workspaceRoot = "/ws", allowedPaths = listOf("/ws"), ), ), ) assertEquals("original", result.boundProfile?.about) } @Test fun `WorkflowCompletedEvent for freestyle planning stays ACTIVE (hands off to execution)`() { val result = reducer.reduce( state = activeState(), event = stored( sessionId = sessionId, payload = WorkflowCompletedEvent( sessionId, terminalStageId = StageId("architect"), totalStages = 2, workflowId = "freestyle_planning", ), ), ) assertEquals(SessionStatus.ACTIVE, result.status) } private fun activeState() = SessionState( status = SessionStatus.ACTIVE ) private fun pausedState() = SessionState( status = SessionStatus.PAUSED ) }