7936251d6b
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)
213 lines
7.4 KiB
Kotlin
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)
|
|
}
|
|
}
|