epic-12: after epic audit and init commit
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
plugins {
|
||||
id 'java-library'
|
||||
id 'org.jetbrains.kotlin.jvm'
|
||||
id 'org.jetbrains.kotlin.plugin.serialization'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
testImplementation(project(":core:events"))
|
||||
testImplementation(project(":core:sessions"))
|
||||
testImplementation(project(":core:transitions"))
|
||||
testImplementation(project(":core:context"))
|
||||
testImplementation(project(":core:inference"))
|
||||
testImplementation(project(":core:kernel"))
|
||||
testImplementation(project(":core:artifacts"))
|
||||
implementation(project(":testing:fixtures"))
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package com.correx.testing.projections
|
||||
|
||||
object Module
|
||||
@@ -0,0 +1,128 @@
|
||||
import com.correx.core.artifacts.ArtifactState
|
||||
import com.correx.core.artifacts.DefaultArtifactReducer
|
||||
import com.correx.core.events.events.ArtifactArchivedEvent
|
||||
import com.correx.core.events.events.ArtifactCreatedEvent
|
||||
import com.correx.core.events.events.ArtifactRejectedEvent
|
||||
import com.correx.core.events.events.ArtifactRelationshipAddedEvent
|
||||
import com.correx.core.events.events.ArtifactSupersededEvent
|
||||
import com.correx.core.events.events.ArtifactValidatedEvent
|
||||
import com.correx.core.events.events.ArtifactValidatingEvent
|
||||
import com.correx.core.events.types.ArtifactId
|
||||
import com.correx.core.events.types.ArtifactLifecyclePhase
|
||||
import com.correx.core.events.types.ArtifactRelationshipType
|
||||
import com.correx.core.events.types.SessionId
|
||||
import com.correx.core.events.types.StageId
|
||||
import com.correx.testing.fixtures.EventFixtures.stored
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class ArtifactReducerTest {
|
||||
|
||||
private lateinit var reducer: DefaultArtifactReducer
|
||||
|
||||
private val artifactId = ArtifactId("art1")
|
||||
private val sessionId = SessionId("sess1")
|
||||
private val stageId = StageId("stage1")
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
reducer = DefaultArtifactReducer()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `CREATED to VALIDATING is valid`() {
|
||||
val state = ArtifactState(phase = ArtifactLifecyclePhase.CREATED)
|
||||
val event = stored(payload = ArtifactValidatingEvent(artifactId, sessionId, stageId))
|
||||
val result = reducer.reduce(state, event)
|
||||
assertTrue(result.isSuccess)
|
||||
assertEquals(ArtifactLifecyclePhase.VALIDATING, result.getOrThrow().phase)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `VALIDATING to VALIDATED is valid`() {
|
||||
val state = ArtifactState(phase = ArtifactLifecyclePhase.VALIDATING)
|
||||
val event = stored(payload = ArtifactValidatedEvent(artifactId, sessionId, stageId))
|
||||
val result = reducer.reduce(state, event)
|
||||
assertTrue(result.isSuccess)
|
||||
assertEquals(ArtifactLifecyclePhase.VALIDATED, result.getOrThrow().phase)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `VALIDATING to REJECTED is valid`() {
|
||||
val state = ArtifactState(phase = ArtifactLifecyclePhase.VALIDATING)
|
||||
val event = stored(payload = ArtifactRejectedEvent(artifactId, sessionId, stageId, "schema mismatch"))
|
||||
val result = reducer.reduce(state, event)
|
||||
assertTrue(result.isSuccess)
|
||||
assertEquals(ArtifactLifecyclePhase.REJECTED, result.getOrThrow().phase)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `VALIDATED to SUPERSEDED is valid`() {
|
||||
val state = ArtifactState(phase = ArtifactLifecyclePhase.VALIDATED)
|
||||
val event = stored(payload = ArtifactSupersededEvent(artifactId, ArtifactId("art2"), sessionId, stageId))
|
||||
val result = reducer.reduce(state, event)
|
||||
assertTrue(result.isSuccess)
|
||||
assertEquals(ArtifactLifecyclePhase.SUPERSEDED, result.getOrThrow().phase)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `VALIDATED to ARCHIVED is valid`() {
|
||||
val state = ArtifactState(phase = ArtifactLifecyclePhase.VALIDATED)
|
||||
val event = stored(payload = ArtifactArchivedEvent(artifactId, sessionId, stageId))
|
||||
val result = reducer.reduce(state, event)
|
||||
assertTrue(result.isSuccess)
|
||||
assertEquals(ArtifactLifecyclePhase.ARCHIVED, result.getOrThrow().phase)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `REJECTED to ARCHIVED is valid`() {
|
||||
val state = ArtifactState(phase = ArtifactLifecyclePhase.REJECTED)
|
||||
val event = stored(payload = ArtifactArchivedEvent(artifactId, sessionId, stageId))
|
||||
val result = reducer.reduce(state, event)
|
||||
assertTrue(result.isSuccess)
|
||||
assertEquals(ArtifactLifecyclePhase.ARCHIVED, result.getOrThrow().phase)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `CREATED to VALIDATED is invalid`() {
|
||||
val state = ArtifactState(phase = ArtifactLifecyclePhase.CREATED)
|
||||
val event = stored(payload = ArtifactValidatedEvent(artifactId, sessionId, stageId))
|
||||
val result = reducer.reduce(state, event)
|
||||
assertTrue(result.isFailure)
|
||||
assertTrue(result.exceptionOrNull() is IllegalStateException)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `VALIDATED to VALIDATING is invalid`() {
|
||||
val state = ArtifactState(phase = ArtifactLifecyclePhase.VALIDATED)
|
||||
val event = stored(payload = ArtifactValidatingEvent(artifactId, sessionId, stageId))
|
||||
val result = reducer.reduce(state, event)
|
||||
assertTrue(result.isFailure)
|
||||
assertTrue(result.exceptionOrNull() is IllegalStateException)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ARCHIVED to anything is invalid`() {
|
||||
val state = ArtifactState(phase = ArtifactLifecyclePhase.ARCHIVED)
|
||||
val event = stored(payload = ArtifactValidatingEvent(artifactId, sessionId, stageId))
|
||||
val result = reducer.reduce(state, event)
|
||||
assertTrue(result.isFailure)
|
||||
assertTrue(result.exceptionOrNull() is IllegalStateException)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `relationship added event appends to lineage`() {
|
||||
val state = ArtifactState(phase = ArtifactLifecyclePhase.CREATED)
|
||||
val event = stored(
|
||||
payload = ArtifactRelationshipAddedEvent(
|
||||
artifactId, ArtifactId("art2"), ArtifactRelationshipType.DERIVED_FROM, sessionId
|
||||
)
|
||||
)
|
||||
val result = reducer.reduce(state, event)
|
||||
assertTrue(result.isSuccess)
|
||||
assertEquals(1, result.getOrThrow().lineage.relationships.size)
|
||||
assertEquals(ArtifactRelationshipType.DERIVED_FROM, result.getOrThrow().lineage.relationships.first().type)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import com.correx.core.context.ContextProjector
|
||||
import com.correx.core.context.DefaultContextReducer
|
||||
import com.correx.core.events.events.ContextBuildingStartedEvent
|
||||
import com.correx.core.events.events.ContextPackBuiltEvent
|
||||
import com.correx.core.events.types.ContextPackId
|
||||
import com.correx.core.events.types.SessionId
|
||||
import com.correx.core.events.types.StageId
|
||||
import com.correx.testing.fixtures.EventFixtures.stored
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class ContextProjectorTest {
|
||||
|
||||
private val projector = ContextProjector(DefaultContextReducer())
|
||||
private val sessionId = SessionId("s1")
|
||||
private val stageId = StageId("stage-1")
|
||||
private val packId = ContextPackId("pack-1")
|
||||
|
||||
@Test
|
||||
fun `initial state has no packs and is not building`() {
|
||||
val state = projector.initial()
|
||||
assertEquals(0, state.builtPackIds.size)
|
||||
assertEquals(false, state.buildingInProgress)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `replay is deterministic`() {
|
||||
val events = listOf(
|
||||
stored(payload = ContextBuildingStartedEvent(sessionId, stageId)),
|
||||
stored(payload = ContextPackBuiltEvent(packId, sessionId, stageId, 100, 4000))
|
||||
)
|
||||
val state1 = events.fold(projector.initial()) { s, e -> projector.apply(s, e) }
|
||||
val state2 = events.fold(projector.initial()) { s, e -> projector.apply(s, e) }
|
||||
assertEquals(state1, state2)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import com.correx.core.context.DefaultContextReducer
|
||||
import com.correx.core.context.state.ContextState
|
||||
import com.correx.core.events.events.ContextBuildingFailedEvent
|
||||
import com.correx.core.events.events.ContextBuildingInterruptedEvent
|
||||
import com.correx.core.events.events.ContextBuildingStartedEvent
|
||||
import com.correx.core.events.events.ContextPackBuiltEvent
|
||||
import com.correx.core.events.types.ContextPackId
|
||||
import com.correx.core.events.types.SessionId
|
||||
import com.correx.core.events.types.StageId
|
||||
import com.correx.testing.fixtures.EventFixtures.stored
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertFalse
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class DefaultContextReducerTest {
|
||||
|
||||
private val reducer = DefaultContextReducer()
|
||||
private val sessionId = SessionId("s1")
|
||||
private val stageId = StageId("stage-1")
|
||||
private val packId = ContextPackId("pack-1")
|
||||
|
||||
@Test
|
||||
fun `ContextBuildingStartedEvent sets buildingInProgress to true`() {
|
||||
val state = reducer.reduce(
|
||||
ContextState(),
|
||||
stored(payload = ContextBuildingStartedEvent(sessionId, stageId))
|
||||
)
|
||||
assertTrue(state.buildingInProgress)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ContextPackBuiltEvent records pack and clears buildingInProgress`() {
|
||||
val started = reducer.reduce(
|
||||
ContextState(),
|
||||
stored(payload = ContextBuildingStartedEvent(sessionId, stageId))
|
||||
)
|
||||
val built = reducer.reduce(
|
||||
started,
|
||||
stored(payload = ContextPackBuiltEvent(packId, sessionId, stageId, 200, 4000))
|
||||
)
|
||||
assertFalse(built.buildingInProgress)
|
||||
assertEquals(1, built.builtPackIds.size)
|
||||
assertTrue(built.builtPackIds.contains(packId))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ContextBuildingFailedEvent clears buildingInProgress`() {
|
||||
val started = reducer.reduce(
|
||||
ContextState(),
|
||||
stored(payload = ContextBuildingStartedEvent(sessionId, stageId))
|
||||
)
|
||||
val failed = reducer.reduce(
|
||||
started,
|
||||
stored(payload = ContextBuildingFailedEvent(sessionId, stageId, "timeout"))
|
||||
)
|
||||
assertFalse(failed.buildingInProgress)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ContextBuildingInterruptedEvent clears buildingInProgress and sets interrupted`() {
|
||||
val started = reducer.reduce(
|
||||
ContextState(),
|
||||
stored(payload = ContextBuildingStartedEvent(sessionId, stageId))
|
||||
)
|
||||
val interrupted = reducer.reduce(
|
||||
started,
|
||||
stored(payload = ContextBuildingInterruptedEvent(sessionId, stageId))
|
||||
)
|
||||
assertFalse(interrupted.buildingInProgress)
|
||||
assertTrue(interrupted.interrupted)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ContextBuildingFailedEvent does not set interrupted`() {
|
||||
val started = reducer.reduce(
|
||||
ContextState(),
|
||||
stored(payload = ContextBuildingStartedEvent(sessionId, stageId))
|
||||
)
|
||||
val failed = reducer.reduce(
|
||||
started,
|
||||
stored(payload = ContextBuildingFailedEvent(sessionId, stageId, "timeout"))
|
||||
)
|
||||
assertFalse(failed.interrupted)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `unrelated events leave state unchanged`() {
|
||||
val state = ContextState()
|
||||
val after = reducer.reduce(state, stored())
|
||||
assertEquals(state, after)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
import com.correx.core.events.events.SessionCompletedEvent
|
||||
import com.correx.core.events.events.SessionFailedEvent
|
||||
import com.correx.core.events.events.SessionPausedEvent
|
||||
import com.correx.core.events.events.SessionResumedEvent
|
||||
import com.correx.core.events.events.SessionStartedEvent
|
||||
import com.correx.core.events.events.StageCompletedEvent
|
||||
import com.correx.core.events.events.StageFailedEvent
|
||||
import com.correx.core.events.events.StageStartedEvent
|
||||
import com.correx.core.events.events.TransitionExecutedEvent
|
||||
import com.correx.core.events.types.SessionId
|
||||
import com.correx.core.sessions.DefaultSessionReducer
|
||||
import com.correx.core.sessions.SessionState
|
||||
import com.correx.core.sessions.SessionStatus
|
||||
import com.correx.core.events.types.StageId
|
||||
import com.correx.core.events.types.TransitionId
|
||||
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 `SessionStartedEvent sets ACTIVE status`() {
|
||||
val state = initialState()
|
||||
|
||||
val result = reducer.reduce(
|
||||
state = state,
|
||||
event = stored(
|
||||
sessionId = sessionId,
|
||||
payload = SessionStartedEvent(sessionId)
|
||||
)
|
||||
)
|
||||
|
||||
assertEquals(SessionStatus.ACTIVE, result.status)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `SessionPausedEvent sets PAUSED status`() {
|
||||
val state = activeState()
|
||||
|
||||
val result = reducer.reduce(
|
||||
state = state,
|
||||
event = stored(
|
||||
sessionId = sessionId,
|
||||
payload = SessionPausedEvent(sessionId)
|
||||
)
|
||||
)
|
||||
|
||||
assertEquals(SessionStatus.PAUSED, result.status)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `SessionResumedEvent sets ACTIVE status`() {
|
||||
val state = pausedState()
|
||||
|
||||
val result = reducer.reduce(
|
||||
state = state,
|
||||
event = stored(
|
||||
sessionId = sessionId,
|
||||
payload = SessionResumedEvent(sessionId)
|
||||
)
|
||||
)
|
||||
|
||||
assertEquals(SessionStatus.ACTIVE, result.status)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `SessionCompletedEvent sets COMPLETED status`() {
|
||||
val state = activeState()
|
||||
|
||||
val result = reducer.reduce(
|
||||
state = state,
|
||||
event = stored(
|
||||
sessionId = sessionId,
|
||||
payload = SessionCompletedEvent(sessionId)
|
||||
)
|
||||
)
|
||||
|
||||
assertEquals(SessionStatus.COMPLETED, result.status)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `SessionFailedEvent sets FAILED status`() {
|
||||
val state = activeState()
|
||||
|
||||
val result = reducer.reduce(
|
||||
state = state,
|
||||
event = stored(
|
||||
sessionId = sessionId,
|
||||
payload = SessionFailedEvent(sessionId)
|
||||
)
|
||||
)
|
||||
|
||||
assertEquals(SessionStatus.FAILED, result.status)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `StageStartedEvent sets ACTIVE status`() {
|
||||
val state = pausedState()
|
||||
|
||||
val result = reducer.reduce(
|
||||
state = state,
|
||||
event = stored(
|
||||
sessionId = sessionId,
|
||||
payload = StageStartedEvent(
|
||||
sessionId,
|
||||
stageId = StageId("stage-a"),
|
||||
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 = SessionStartedEvent(sessionId),
|
||||
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 = SessionPausedEvent(sessionId),
|
||||
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 = SessionPausedEvent(sessionId),
|
||||
timestamp = timestamp
|
||||
)
|
||||
)
|
||||
|
||||
assertEquals(timestamp, result.updatedAt)
|
||||
}
|
||||
|
||||
private fun initialState() =
|
||||
SessionState(
|
||||
status = SessionStatus.CREATED
|
||||
)
|
||||
|
||||
private fun activeState() =
|
||||
SessionState(
|
||||
status = SessionStatus.ACTIVE
|
||||
)
|
||||
|
||||
private fun pausedState() =
|
||||
SessionState(
|
||||
status = SessionStatus.PAUSED
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import com.correx.core.events.events.InferenceCompletedEvent
|
||||
import com.correx.core.events.events.InferenceStartedEvent
|
||||
import com.correx.core.events.types.InferenceRequestId
|
||||
import com.correx.core.events.types.ProviderId
|
||||
import com.correx.core.events.types.SessionId
|
||||
import com.correx.core.events.types.StageId
|
||||
import com.correx.core.inference.InferenceProjector
|
||||
import com.correx.core.inference.InferenceRecord
|
||||
import com.correx.core.inference.InferenceState
|
||||
import com.correx.core.inference.InferenceStatus
|
||||
import com.correx.core.inference.TokenUsage
|
||||
import com.correx.testing.fixtures.EventFixtures.stored
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class InferenceProjectorTest {
|
||||
|
||||
private lateinit var projector: InferenceProjector
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
projector = InferenceProjector()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should initialize state with empty records`() {
|
||||
val initialState = projector.initial()
|
||||
assertEquals(0, initialState.records.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should start a new record on InferenceStartedEvent`() {
|
||||
val initialState = projector.initial()
|
||||
val event = stored(
|
||||
payload = InferenceStartedEvent(
|
||||
requestId = InferenceRequestId("req1"),
|
||||
sessionId = SessionId("sess1"),
|
||||
stageId = StageId("stageA"),
|
||||
providerId = ProviderId("providerX"),
|
||||
),
|
||||
)
|
||||
val newState = projector.apply(initialState, event)
|
||||
|
||||
assertEquals(1, newState.records.size)
|
||||
val record = newState.records.first()
|
||||
assertEquals(InferenceStatus.STARTED, record.status)
|
||||
assertEquals("req1", record.requestId.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should complete a record on InferenceCompletedEvent`() {
|
||||
val startedRecord = InferenceRecord(
|
||||
requestId = InferenceRequestId("req1"),
|
||||
sessionId = SessionId("sess1"),
|
||||
stageId = StageId("stageA"),
|
||||
providerId = ProviderId("providerX"),
|
||||
status = InferenceStatus.STARTED,
|
||||
)
|
||||
val initialState = InferenceState(listOf(startedRecord))
|
||||
|
||||
val event = stored(
|
||||
payload = InferenceCompletedEvent(
|
||||
requestId = InferenceRequestId("req1"),
|
||||
sessionId = SessionId("sess1"),
|
||||
stageId = StageId("stageA"),
|
||||
providerId = ProviderId("providerX"),
|
||||
tokensUsed = TokenUsage(50, 50),
|
||||
latencyMs = 500L,
|
||||
),
|
||||
)
|
||||
val newState = projector.apply(initialState, event)
|
||||
|
||||
assertEquals(1, newState.records.size)
|
||||
val record = newState.records.first()
|
||||
assertEquals(InferenceStatus.COMPLETED, record.status)
|
||||
assertEquals(100, record.tokensUsed?.totalTokens)
|
||||
assertEquals(500L, record.latencyMs)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should handle multiple events correctly`() {
|
||||
// 1. Start
|
||||
val initialState = projector.initial()
|
||||
val startEvent = stored(
|
||||
payload = InferenceStartedEvent(
|
||||
requestId = InferenceRequestId("req1"),
|
||||
sessionId = SessionId("sess1"),
|
||||
stageId = StageId("stageA"),
|
||||
providerId = ProviderId("providerX"),
|
||||
),
|
||||
)
|
||||
var currentState = projector.apply(initialState, startEvent)
|
||||
assertEquals(1, currentState.records.size)
|
||||
|
||||
// 2. Complete
|
||||
val completeEvent = stored(
|
||||
payload = InferenceCompletedEvent(
|
||||
requestId = InferenceRequestId("req1"),
|
||||
sessionId = SessionId("sess1"),
|
||||
stageId = StageId("stageA"),
|
||||
providerId = ProviderId("providerX"),
|
||||
tokensUsed = TokenUsage(100, 100),
|
||||
latencyMs = 1000L,
|
||||
),
|
||||
)
|
||||
currentState = projector.apply(currentState, completeEvent)
|
||||
assertEquals(1, currentState.records.size)
|
||||
assertEquals(InferenceStatus.COMPLETED, currentState.records.first().status)
|
||||
assertEquals(200, currentState.records.first().tokensUsed?.totalTokens)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
import com.correx.core.events.events.InferenceCompletedEvent
|
||||
import com.correx.core.events.events.InferenceFailedEvent
|
||||
import com.correx.core.events.events.InferenceStartedEvent
|
||||
import com.correx.core.events.events.InferenceTimeoutEvent
|
||||
import com.correx.core.events.events.ModelLoadedEvent
|
||||
import com.correx.core.events.types.InferenceRequestId
|
||||
import com.correx.core.events.types.ProviderId
|
||||
import com.correx.core.events.types.SessionId
|
||||
import com.correx.core.events.types.StageId
|
||||
import com.correx.core.inference.DefaultInferenceReducer
|
||||
import com.correx.core.inference.InferenceRecord
|
||||
import com.correx.core.inference.InferenceReducer
|
||||
import com.correx.core.inference.InferenceState
|
||||
import com.correx.core.inference.InferenceStatus
|
||||
import com.correx.core.inference.TokenUsage
|
||||
import com.correx.testing.fixtures.EventFixtures.stored
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class InferenceReducerTest {
|
||||
|
||||
private lateinit var reducer: InferenceReducer
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
reducer = DefaultInferenceReducer()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should start a new record on InferenceStartedEvent`() {
|
||||
val initialState = InferenceState(emptyList())
|
||||
val event = stored(
|
||||
payload = InferenceStartedEvent(
|
||||
requestId = InferenceRequestId("req1"),
|
||||
sessionId = SessionId("sess1"),
|
||||
stageId = StageId("stageA"),
|
||||
providerId = ProviderId("providerX"),
|
||||
),
|
||||
)
|
||||
val newState = reducer.reduce(initialState, event)
|
||||
|
||||
assertEquals(1, newState.records.size)
|
||||
val record = newState.records.first()
|
||||
assertEquals(InferenceStatus.STARTED, record.status)
|
||||
assertEquals("req1", record.requestId.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should complete a record on InferenceCompletedEvent`() {
|
||||
val startedRecord = InferenceRecord(
|
||||
requestId = InferenceRequestId("req1"),
|
||||
sessionId = SessionId("sess1"),
|
||||
stageId = StageId("stageA"),
|
||||
providerId = ProviderId("providerX"),
|
||||
status = InferenceStatus.STARTED,
|
||||
)
|
||||
val initialState = InferenceState(listOf(startedRecord))
|
||||
|
||||
val event = stored(
|
||||
payload = InferenceCompletedEvent(
|
||||
requestId = InferenceRequestId("req1"),
|
||||
sessionId = SessionId("sess1"),
|
||||
stageId = StageId("stageA"),
|
||||
providerId = ProviderId("providerX"),
|
||||
tokensUsed = TokenUsage(50, 50),
|
||||
latencyMs = 500L,
|
||||
),
|
||||
)
|
||||
val newState = reducer.reduce(initialState, event)
|
||||
|
||||
assertEquals(1, newState.records.size)
|
||||
val record = newState.records.first()
|
||||
assertEquals(InferenceStatus.COMPLETED, record.status)
|
||||
assertEquals(100, record.tokensUsed?.totalTokens)
|
||||
assertEquals(500L, record.latencyMs)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should fail a record on InferenceFailedEvent`() {
|
||||
val startedRecord = InferenceRecord(
|
||||
requestId = InferenceRequestId("req1"),
|
||||
sessionId = SessionId("sess1"),
|
||||
stageId = StageId("stageA"),
|
||||
providerId = ProviderId("providerX"),
|
||||
status = InferenceStatus.STARTED,
|
||||
)
|
||||
val initialState = InferenceState(listOf(startedRecord))
|
||||
|
||||
val event = stored(
|
||||
payload = InferenceFailedEvent(
|
||||
requestId = InferenceRequestId("req1"),
|
||||
sessionId = SessionId("sess1"),
|
||||
stageId = StageId("stageA"),
|
||||
providerId = ProviderId("providerX"),
|
||||
reason = "Network failure",
|
||||
),
|
||||
)
|
||||
val newState = reducer.reduce(initialState, event)
|
||||
|
||||
assertEquals(1, newState.records.size)
|
||||
val record = newState.records.first()
|
||||
assertEquals(InferenceStatus.FAILED, record.status)
|
||||
assertEquals("Network failure", record.failureReason)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should timeout a record on InferenceTimeoutEvent`() {
|
||||
val startedRecord = InferenceRecord(
|
||||
requestId = InferenceRequestId("req1"),
|
||||
sessionId = SessionId("sess1"),
|
||||
stageId = StageId("stageA"),
|
||||
providerId = ProviderId("providerX"),
|
||||
status = InferenceStatus.STARTED,
|
||||
)
|
||||
val initialState = InferenceState(listOf(startedRecord))
|
||||
|
||||
val event = stored(
|
||||
payload = InferenceTimeoutEvent(
|
||||
requestId = InferenceRequestId("req1"),
|
||||
sessionId = SessionId("sess1"),
|
||||
stageId = StageId("stageA"),
|
||||
providerId = ProviderId("providerX"),
|
||||
timeoutMs = 10000L,
|
||||
),
|
||||
)
|
||||
val newState = reducer.reduce(initialState, event)
|
||||
|
||||
assertEquals(1, newState.records.size)
|
||||
val record = newState.records.first()
|
||||
assertEquals(InferenceStatus.TIMED_OUT, record.status)
|
||||
assertEquals(10000L, record.latencyMs)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should pass state unchanged on unrelated events`() {
|
||||
val startedRecord = InferenceRecord(
|
||||
requestId = InferenceRequestId("req1"),
|
||||
sessionId = SessionId("sess1"),
|
||||
stageId = StageId("stageA"),
|
||||
providerId = ProviderId("providerX"),
|
||||
status = InferenceStatus.STARTED,
|
||||
)
|
||||
val initialState = InferenceState(listOf(startedRecord))
|
||||
|
||||
val event = stored(
|
||||
payload = ModelLoadedEvent(
|
||||
sessionId = SessionId("sess1"),
|
||||
providerId = ProviderId("providerX"),
|
||||
modelId = "model",
|
||||
),
|
||||
)
|
||||
val newState = reducer.reduce(initialState, event)
|
||||
|
||||
assertEquals(1, newState.records.size)
|
||||
assertEquals(InferenceStatus.STARTED, newState.records.first().status)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should not modify record if requestId does not match`() {
|
||||
val startedRecord = InferenceRecord(
|
||||
requestId = InferenceRequestId("req1"),
|
||||
sessionId = SessionId("sess1"),
|
||||
stageId = StageId("stageA"),
|
||||
providerId = ProviderId("providerX"),
|
||||
status = InferenceStatus.STARTED,
|
||||
)
|
||||
val initialState = InferenceState(listOf(startedRecord))
|
||||
|
||||
// Event for a different request
|
||||
val event = stored(
|
||||
payload = InferenceCompletedEvent(
|
||||
requestId = InferenceRequestId("req2"),
|
||||
sessionId = SessionId("sess1"),
|
||||
stageId = StageId("stageA"),
|
||||
providerId = ProviderId("providerX"),
|
||||
tokensUsed = TokenUsage(50, 50),
|
||||
latencyMs = 500L,
|
||||
),
|
||||
)
|
||||
val newState = reducer.reduce(initialState, event)
|
||||
|
||||
// The record should remain unchanged
|
||||
assertEquals(1, newState.records.size)
|
||||
assertEquals(InferenceStatus.STARTED, newState.records.first().status)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import com.correx.core.events.events.OrchestrationPausedEvent
|
||||
import com.correx.core.events.events.OrchestrationResumedEvent
|
||||
import com.correx.core.events.events.RetryAttemptedEvent
|
||||
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.orchestration.OrchestrationStatus
|
||||
import com.correx.core.kernel.orchestration.DefaultOrchestrationReducer
|
||||
import com.correx.core.kernel.orchestration.OrchestrationProjector
|
||||
import com.correx.testing.fixtures.EventFixtures.stored
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertFalse
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class OrchestrationProjectorTest {
|
||||
|
||||
private val reducer = DefaultOrchestrationReducer()
|
||||
private val projector = OrchestrationProjector(reducer)
|
||||
private val sessionId = SessionId("s1")
|
||||
private val stageId = StageId("stage-1")
|
||||
|
||||
@Test
|
||||
fun `should initialize orchestration with IDLE status`() {
|
||||
val initialState = projector.initial()
|
||||
assertEquals(OrchestrationStatus.IDLE, initialState.status)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should set RUNNING on WorkflowStartedEvent`() {
|
||||
val initialState = projector.initial()
|
||||
val started =
|
||||
projector.apply(initialState, stored(payload = WorkflowStartedEvent(sessionId, stageId)))
|
||||
assertEquals(OrchestrationStatus.RUNNING, started.status)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should set RUNNING after resume on pause`() {
|
||||
val initialState = projector.initial()
|
||||
val started =
|
||||
projector.apply(initialState, stored(payload = WorkflowStartedEvent(sessionId, stageId)))
|
||||
assertEquals(OrchestrationStatus.RUNNING, started.status)
|
||||
val paused =
|
||||
projector.apply(started, stored(payload = OrchestrationPausedEvent(sessionId, stageId, "Tool approval")))
|
||||
assertEquals(OrchestrationStatus.PAUSED, paused.status)
|
||||
assertTrue(paused.pendingApproval)
|
||||
val resumed =
|
||||
projector.apply(paused, stored(payload = OrchestrationResumedEvent(sessionId, stageId)))
|
||||
assertEquals(OrchestrationStatus.RUNNING, resumed.status)
|
||||
assertFalse(resumed.pendingApproval)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should set FAILED on WorkflowFailedEvent`() {
|
||||
val initialState = projector.initial()
|
||||
val started =
|
||||
projector.apply(initialState, stored(payload = WorkflowStartedEvent(sessionId, stageId)))
|
||||
val failed =
|
||||
projector.apply(
|
||||
started,
|
||||
stored(payload = WorkflowFailedEvent(sessionId, stageId, "Something went wrong", false)),
|
||||
)
|
||||
assertEquals(OrchestrationStatus.FAILED, failed.status)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `same events produce same result`() {
|
||||
val initialState = projector.initial()
|
||||
val events = listOf(
|
||||
stored(payload = WorkflowStartedEvent(sessionId, stageId)),
|
||||
stored(payload = WorkflowFailedEvent(sessionId, stageId, "Something went wrong", false)),
|
||||
stored(
|
||||
payload = RetryAttemptedEvent(
|
||||
sessionId,
|
||||
stageId,
|
||||
attemptNumber = 1,
|
||||
maxAttempts = 3,
|
||||
failureReason = "Something went wrong",
|
||||
),
|
||||
),
|
||||
stored(payload = WorkflowCompletedEvent(sessionId, stageId, 1)),
|
||||
)
|
||||
|
||||
val result1 = events.fold(initialState) { state, event ->
|
||||
projector.apply(state, event)
|
||||
}
|
||||
val result2 = events.fold(initialState) { state, event ->
|
||||
projector.apply(state, event)
|
||||
}
|
||||
|
||||
assertEquals(result1, result2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `full pause-resume lifecycle`() {
|
||||
val s0 = projector.initial()
|
||||
val s1 = projector.apply(s0, stored(payload = WorkflowStartedEvent(sessionId, stageId)))
|
||||
val s2 =
|
||||
projector.apply(s1, stored(payload = OrchestrationPausedEvent(sessionId, stageId, "approval required")))
|
||||
val s3 = projector.apply(s2, stored(payload = OrchestrationResumedEvent(sessionId, stageId)))
|
||||
val s4 = projector.apply(s3, stored(payload = WorkflowCompletedEvent(sessionId, stageId, 1)))
|
||||
|
||||
assertEquals(OrchestrationStatus.RUNNING, s1.status)
|
||||
assertEquals(OrchestrationStatus.PAUSED, s2.status)
|
||||
assertTrue(s2.pendingApproval)
|
||||
assertEquals(OrchestrationStatus.RUNNING, s3.status)
|
||||
assertFalse(s3.pendingApproval)
|
||||
assertEquals(OrchestrationStatus.COMPLETED, s4.status)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
import com.correx.core.events.events.OrchestrationPausedEvent
|
||||
import com.correx.core.events.events.OrchestrationResumedEvent
|
||||
import com.correx.core.events.events.RetryAttemptedEvent
|
||||
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.orchestration.OrchestrationState
|
||||
import com.correx.core.events.orchestration.OrchestrationStatus
|
||||
import com.correx.core.kernel.orchestration.DefaultOrchestrationReducer
|
||||
import com.correx.testing.fixtures.EventFixtures.stored
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertFalse
|
||||
import org.junit.jupiter.api.Assertions.assertNull
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class OrchestrationReducerTest {
|
||||
|
||||
private val reducer = DefaultOrchestrationReducer()
|
||||
private val sessionId = SessionId("s1")
|
||||
private val stageId = StageId("stage-1")
|
||||
private val state = OrchestrationState(stageId)
|
||||
|
||||
@Test
|
||||
fun `WorkflowStartedEvent sets status to RUNNING`() {
|
||||
val state = reducer.reduce(
|
||||
state,
|
||||
stored(payload = WorkflowStartedEvent(sessionId, stageId)),
|
||||
)
|
||||
assertEquals(OrchestrationStatus.RUNNING, state.status)
|
||||
assertEquals(stageId, state.currentStageId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WorkflowFailedEvent sets status to FAILED and failure reason`() {
|
||||
val started = reducer.reduce(
|
||||
state,
|
||||
stored(payload = WorkflowStartedEvent(sessionId, stageId)),
|
||||
)
|
||||
|
||||
val failed = reducer.reduce(
|
||||
started,
|
||||
stored(payload = WorkflowFailedEvent(sessionId, stageId, "Something went wrong", false)),
|
||||
)
|
||||
assertFalse(failed.failureReason.isNullOrBlank())
|
||||
assertEquals(OrchestrationStatus.FAILED, failed.status)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WorkflowCompletedEvent sets status to COMPLETED`() {
|
||||
val started = reducer.reduce(
|
||||
state,
|
||||
stored(payload = WorkflowStartedEvent(sessionId, stageId)),
|
||||
)
|
||||
|
||||
val completed = reducer.reduce(
|
||||
started,
|
||||
stored(payload = WorkflowCompletedEvent(sessionId, stageId, 1)),
|
||||
)
|
||||
assertTrue(completed.failureReason.isNullOrBlank())
|
||||
assertEquals(OrchestrationStatus.COMPLETED, completed.status)
|
||||
assertEquals(stageId, completed.currentStageId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `OrchestrationPausedEvent sets status to PAUSED with reason and pending approval`() {
|
||||
val started = reducer.reduce(
|
||||
state,
|
||||
stored(payload = WorkflowStartedEvent(sessionId, stageId)),
|
||||
)
|
||||
|
||||
val paused = reducer.reduce(
|
||||
started,
|
||||
stored(payload = OrchestrationPausedEvent(sessionId, stageId, "Requires user approval")),
|
||||
)
|
||||
assertFalse(paused.pauseReason.isNullOrBlank())
|
||||
assertEquals(OrchestrationStatus.PAUSED, paused.status)
|
||||
assertTrue(paused.pendingApproval)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `OrchestrationResumedEvent sets status to RUNNING with null reason and no pending approval`() {
|
||||
val started = reducer.reduce(
|
||||
state,
|
||||
stored(payload = WorkflowStartedEvent(sessionId, stageId)),
|
||||
)
|
||||
|
||||
val paused = reducer.reduce(
|
||||
started,
|
||||
stored(payload = OrchestrationPausedEvent(sessionId, stageId, "Requires user approval")),
|
||||
)
|
||||
|
||||
val resumed = reducer.reduce(
|
||||
paused,
|
||||
stored(payload = OrchestrationResumedEvent(sessionId, stageId)),
|
||||
)
|
||||
assertTrue(resumed.pauseReason.isNullOrBlank())
|
||||
assertEquals(OrchestrationStatus.RUNNING, resumed.status)
|
||||
assertFalse(resumed.pendingApproval)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `RetryAttemptedEvent sets status to RUNNING with null reason and no pending approval`() {
|
||||
val started = reducer.reduce(
|
||||
state,
|
||||
stored(payload = WorkflowStartedEvent(sessionId, stageId)),
|
||||
)
|
||||
|
||||
val failed = reducer.reduce(
|
||||
started,
|
||||
stored(payload = WorkflowFailedEvent(sessionId, stageId, "Something went wrong", false)),
|
||||
)
|
||||
|
||||
val retrying1 = reducer.reduce(
|
||||
failed,
|
||||
stored(payload = RetryAttemptedEvent(sessionId, stageId, 1, 3, "Something went wrong")),
|
||||
)
|
||||
|
||||
val retrying2 = reducer.reduce(
|
||||
retrying1,
|
||||
stored(payload = RetryAttemptedEvent(sessionId, stageId, 2, 3, "Something went wrong")),
|
||||
)
|
||||
|
||||
val retrying3 = reducer.reduce(
|
||||
retrying2,
|
||||
stored(payload = RetryAttemptedEvent(sessionId, stageId, 3, 3, "Something went wrong")),
|
||||
)
|
||||
assertTrue(retrying1.retryCount < retrying2.retryCount && retrying2.retryCount < retrying3.retryCount)
|
||||
assertEquals(OrchestrationStatus.RUNNING, retrying1.status)
|
||||
assertNull(retrying1.failureReason)
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun `unrelated event does nothing`() {
|
||||
val started = reducer.reduce(
|
||||
state,
|
||||
stored(payload = WorkflowStartedEvent(sessionId, stageId)),
|
||||
)
|
||||
|
||||
val unrelated = reducer.reduce(started, stored())
|
||||
assertEquals(started, unrelated)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
import com.correx.core.events.events.SessionCompletedEvent
|
||||
import com.correx.core.events.events.SessionPausedEvent
|
||||
import com.correx.core.events.events.SessionResumedEvent
|
||||
import com.correx.core.events.events.SessionStartedEvent
|
||||
import com.correx.core.events.events.StageCompletedEvent
|
||||
import com.correx.core.events.events.StageFailedEvent
|
||||
import com.correx.core.events.events.StageStartedEvent
|
||||
import com.correx.core.events.events.TransitionExecutedEvent
|
||||
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.testing.fixtures.EventFixtures.stored
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class SessionProjectorTest {
|
||||
|
||||
private val projector = SessionProjector(DefaultSessionReducer())
|
||||
|
||||
@Test
|
||||
fun `full lifecycle transitions correctly`() {
|
||||
var state = projector.initial()
|
||||
|
||||
val events = listOf(
|
||||
stored(eventId = EventId("e1"), payload = SessionStartedEvent(SessionId("s1"))),
|
||||
stored(eventId = EventId("e2"), payload = SessionPausedEvent(SessionId("s1"))),
|
||||
stored(eventId = EventId("e3"), payload = SessionResumedEvent(SessionId("s1"))),
|
||||
stored(eventId = EventId("e4"), payload = SessionCompletedEvent(SessionId("s1")))
|
||||
)
|
||||
|
||||
events.forEach {
|
||||
state = projector.apply(state, it)
|
||||
}
|
||||
|
||||
assertEquals(SessionStatus.COMPLETED, state.status)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `failed stage transitions session to FAILED`() {
|
||||
var state = projector.initial()
|
||||
|
||||
val events = listOf(
|
||||
stored(
|
||||
eventId = EventId("e1"),
|
||||
payload = SessionStartedEvent(SessionId("s1"))
|
||||
),
|
||||
stored(
|
||||
eventId = EventId("e2"),
|
||||
payload = StageStartedEvent(
|
||||
sessionId = SessionId("s1"),
|
||||
stageId = StageId("draft"),
|
||||
transitionId = TransitionId("t1")
|
||||
)
|
||||
),
|
||||
stored(
|
||||
eventId = EventId("e3"),
|
||||
payload = StageFailedEvent(
|
||||
sessionId = SessionId("s1"),
|
||||
stageId = StageId("draft"),
|
||||
transitionId = TransitionId("t1"),
|
||||
reason = "boom"
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
events.forEach {
|
||||
state = projector.apply(state, it)
|
||||
}
|
||||
|
||||
assertEquals(SessionStatus.FAILED, state.status)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `transition events keep session ACTIVE`() {
|
||||
var state = projector.initial()
|
||||
|
||||
val events = listOf(
|
||||
stored(
|
||||
eventId = EventId("e1"),
|
||||
payload = SessionStartedEvent(SessionId("s1"))
|
||||
),
|
||||
stored(
|
||||
eventId = EventId("e2"),
|
||||
payload = TransitionExecutedEvent(
|
||||
sessionId = SessionId("s1"),
|
||||
from = StageId("a"),
|
||||
to = StageId("b"),
|
||||
transitionId = TransitionId("t1")
|
||||
)
|
||||
),
|
||||
stored(
|
||||
eventId = EventId("e3"),
|
||||
payload = StageStartedEvent(
|
||||
sessionId = SessionId("s1"),
|
||||
stageId = StageId("b"),
|
||||
transitionId = TransitionId("t1")
|
||||
)
|
||||
),
|
||||
stored(
|
||||
eventId = EventId("e4"),
|
||||
payload = StageCompletedEvent(
|
||||
sessionId = SessionId("s1"),
|
||||
stageId = StageId("b"),
|
||||
transitionId = TransitionId("t1")
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
events.forEach {
|
||||
state = projector.apply(state, it)
|
||||
}
|
||||
|
||||
assertEquals(SessionStatus.ACTIVE, state.status)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `projection replay is deterministic`() {
|
||||
|
||||
val events = listOf(
|
||||
stored(
|
||||
eventId = EventId("e1"),
|
||||
payload = SessionStartedEvent(SessionId("s1"))
|
||||
),
|
||||
stored(
|
||||
eventId = EventId("e2"),
|
||||
payload = SessionCompletedEvent(SessionId("s1"))
|
||||
)
|
||||
)
|
||||
|
||||
val state1 = events.fold(projector.initial()) { state, event ->
|
||||
projector.apply(state, event)
|
||||
}
|
||||
|
||||
val state2 = events.fold(projector.initial()) { state, event ->
|
||||
projector.apply(state, event)
|
||||
}
|
||||
|
||||
assertEquals(state1, state2)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user