Files
correx/testing/projections/src/test/kotlin/DefaultSessionReducerTest.kt
T

345 lines
10 KiB
Kotlin

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.ProjectProfileBoundEvent
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 leaves ACTIVE 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.ACTIVE, result.status)
}
@Test
fun `WorkflowFailedEvent leaves ACTIVE 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.ACTIVE, 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)
}
private fun initialState() =
SessionState(
status = SessionStatus.CREATED
)
private fun activeState() =
SessionState(
status = SessionStatus.ACTIVE
)
private fun pausedState() =
SessionState(
status = SessionStatus.PAUSED
)
}