Files
correx/testing/replay/src/test/kotlin/TransitionReplayIntegrationTest.kt
T
kami 7936251d6b fix: complete all P2 audit findings — dead code, NPE guard, hygiene
P2-1: Guard SessionId NPE with safe-call in SessionsReducer
P2-2: Remove 16 dead event classes + reducer branches; replace with live orchestration events in tests
P2-3: Standardize ChatInput divergences — guard CancelSession, single-arg ProtocolError
P2-4: Remove dead TuiToolRecord.diff and ToolDisplayStatus.REQUESTED
P2-5: Remove dead streamLive from SessionEventBridge
P2-6: Extract SessionsReducerContext data class for 6+ param method
P2-7: Replace StageId("none") sentinel with TypeId.NONE
P2-9: Add TypeId.random() factory on all type-alias IDs
P2-10: Add KDoc on schemaVersion documenting reserved-for-migration
P2-8: Verified RouterReducer string-template already correct (TypeId value class toString)
2026-05-29 01:01:45 +04:00

213 lines
7.4 KiB
Kotlin

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.WorkflowStartedEvent
import com.correx.core.events.types.EventId
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.sessions.DefaultSessionReducer
import com.correx.core.sessions.SessionProjector
import com.correx.core.sessions.SessionStatus
import com.correx.core.sessions.projections.replay.DefaultEventReplayer
import com.correx.infrastructure.persistence.InMemoryEventStore
import com.correx.testing.fixtures.EventFixtures.newEvent
import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
class TransitionReplayIntegrationTest {
private val sessionId = SessionId("session-1")
@Test
fun `workflow replay reconstructs COMPLETED session`(): Unit = runBlocking {
val store = InMemoryEventStore()
store.appendAll(
listOf(
newEvent(
sessionId = sessionId,
eventId = EventId("event-1"),
payload = WorkflowStartedEvent(sessionId, workflowId = "test", startStageId = StageId("stage-1"))
),
newEvent(
sessionId = sessionId,
eventId = EventId("event-2"),
payload = TransitionExecutedEvent(
sessionId = sessionId,
from = StageId("draft"),
to = StageId("review"),
transitionId = TransitionId("transition-1")
)
),
newEvent(
sessionId = sessionId,
eventId = EventId("event-3"),
payload = TransitionExecutedEvent(
sessionId = sessionId,
from = StageId("review"),
to = StageId("review"),
transitionId = TransitionId("transition-1")
)
),
newEvent(
sessionId = sessionId,
eventId = EventId("event-4"),
payload = StageCompletedEvent(
sessionId = sessionId,
stageId = StageId("review"),
transitionId = TransitionId("transition-1")
)
),
newEvent(
sessionId = sessionId,
eventId = EventId("event-5"),
payload = WorkflowCompletedEvent(sessionId, terminalStageId = StageId("stage-1"), totalStages = 1)
)
)
)
val replayer = DefaultEventReplayer(
store = store,
projection = SessionProjector(
reducer = DefaultSessionReducer()
)
)
val state = replayer.rebuild(sessionId)
assertEquals(
SessionStatus.ACTIVE,
state.status
)
}
@Test
fun `workflow replay reconstructs FAILED session`(): Unit = runBlocking {
val store = InMemoryEventStore()
store.appendAll(
listOf(
newEvent(
sessionId = sessionId,
eventId = EventId("event-1"),
payload = WorkflowStartedEvent(sessionId, workflowId = "test", startStageId = StageId("stage-1"))
),
newEvent(
sessionId = sessionId,
eventId = EventId("event-2"),
payload = TransitionExecutedEvent(
sessionId = sessionId,
from = StageId("draft"),
to = StageId("review"),
transitionId = TransitionId("transition-1")
)
),
newEvent(
sessionId = sessionId,
eventId = EventId("event-3"),
payload = TransitionExecutedEvent(
sessionId = sessionId,
from = StageId("review"),
to = StageId("review"),
transitionId = TransitionId("transition-1")
)
),
newEvent(
sessionId = sessionId,
eventId = EventId("event-4"),
payload = StageFailedEvent(
sessionId = sessionId,
stageId = StageId("review"),
transitionId = TransitionId("transition-1"),
reason = "validation failed"
)
)
)
)
val replayer = DefaultEventReplayer(
store = store,
projection = SessionProjector(
reducer = DefaultSessionReducer()
)
)
val state = replayer.rebuild(sessionId)
assertEquals(
SessionStatus.FAILED,
state.status
)
}
@Test
fun `replay is deterministic for identical event stream`(): Unit = runBlocking {
val events = listOf(
newEvent(
sessionId = sessionId,
eventId = EventId("event-1"),
payload = WorkflowStartedEvent(sessionId, workflowId = "test", startStageId = StageId("stage-1"))
),
newEvent(
sessionId = sessionId,
eventId = EventId("event-2"),
payload = TransitionExecutedEvent(
sessionId = sessionId,
from = StageId("draft"),
to = StageId("review"),
transitionId = TransitionId("transition-1")
)
),
newEvent(
sessionId = sessionId,
eventId = EventId("event-3"),
payload = TransitionExecutedEvent(
sessionId = sessionId,
from = StageId("review"),
to = StageId("review"),
transitionId = TransitionId("transition-1")
)
),
newEvent(
sessionId = sessionId,
eventId = EventId("event-4"),
payload = StageCompletedEvent(
sessionId = sessionId,
stageId = StageId("review"),
transitionId = TransitionId("transition-1")
)
)
)
val store1 = InMemoryEventStore()
val store2 = InMemoryEventStore()
store1.appendAll(events)
store2.appendAll(events)
val projection = SessionProjector(
reducer = DefaultSessionReducer()
)
val replayer1 = DefaultEventReplayer(
store = store1,
projection = projection
)
val replayer2 = DefaultEventReplayer(
store = store2,
projection = projection
)
val state1 = replayer1.rebuild(sessionId)
val state2 = replayer2.rebuild(sessionId)
assertEquals(state1, state2)
}
}