epic-12: after epic audit and init commit

This commit is contained in:
2026-05-16 11:42:00 +04:00
commit c77277af0b
461 changed files with 28958 additions and 0 deletions
+16
View File
@@ -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)
}
}