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)
This commit is contained in:
2026-05-29 01:01:25 +04:00
parent 8ea3c381ef
commit 7936251d6b
32 changed files with 240 additions and 608 deletions
@@ -244,12 +244,4 @@ class SessionEventBridge(
return orderedIds.mapNotNull { byInvocation[it] } return orderedIds.mapNotNull { byInvocation[it] }
} }
suspend fun streamLive(sessionId: SessionId) {
var sessionSequence = 0L
eventStore.subscribe(sessionId).collect { event ->
sessionSequence++
domainEventToServerMessage(event, artifactStore, sessionSequence = sessionSequence)
?.let { send(it) }
}
}
} }
@@ -88,11 +88,7 @@ class GlobalStreamHandler(private val module: ServerModule) {
} }
.onFailure { .onFailure {
log.warn("decode error: {}", it.message) log.warn("decode error: {}", it.message)
val error = ServerMessage.ProtocolError( val error = ServerMessage.ProtocolError("Unknown message: ${it.message}")
message = "Unknown message: ${it.message}",
sequence = null,
sessionSequence = null,
)
session.send(Frame.Text(ProtocolSerializer.encodeServerMessage(error))) session.send(Frame.Text(ProtocolSerializer.encodeServerMessage(error)))
} }
} }
@@ -144,7 +140,10 @@ class GlobalStreamHandler(private val module: ServerModule) {
is ClientMessage.Ping -> Unit is ClientMessage.Ping -> Unit
is ClientMessage.StartSession -> handleStartSession(session, msg, sendFrame) is ClientMessage.StartSession -> handleStartSession(session, msg, sendFrame)
is ClientMessage.StartChatSession -> handleStartChatSession(session, msg, sendFrame) is ClientMessage.StartChatSession -> handleStartChatSession(session, msg, sendFrame)
is ClientMessage.CancelSession -> module.orchestrator.cancel(msg.sessionId) is ClientMessage.CancelSession -> {
runCatching { module.orchestrator.cancel(msg.sessionId) }
.onFailure { log.error("cancel failed for session={}: {}", msg.sessionId.value, it.message, it) }
}
is ClientMessage.ResumeSession -> session.send(encodeError("ResumeSession not supported")) is ClientMessage.ResumeSession -> session.send(encodeError("ResumeSession not supported"))
is ClientMessage.ApprovalResponse -> handleApprovalResponse(msg, sendFrame) is ClientMessage.ApprovalResponse -> handleApprovalResponse(msg, sendFrame)
is ClientMessage.CreateGrant -> handleCreateGrant(msg, sendFrame) is ClientMessage.CreateGrant -> handleCreateGrant(msg, sendFrame)
@@ -159,11 +158,7 @@ class GlobalStreamHandler(private val module: ServerModule) {
)) ))
}.onFailure { }.onFailure {
log.error("routerFacade.onUserInput failed: {}", it.message) log.error("routerFacade.onUserInput failed: {}", it.message)
sendFrame(ServerMessage.ProtocolError( sendFrame(ServerMessage.ProtocolError("Router error: ${it.message}"))
message = "Router error: ${it.message}",
sequence = null,
sessionSequence = null,
))
} }
} }
} }
@@ -176,34 +171,16 @@ class GlobalStreamHandler(private val module: ServerModule) {
val scopeSessionId = module.approvalCoordinator.lookupSession(msg.requestId) val scopeSessionId = module.approvalCoordinator.lookupSession(msg.requestId)
if (scopeSessionId == null) { if (scopeSessionId == null) {
log.warn("handleApprovalResponse: no session found for requestId={}", msg.requestId.value) log.warn("handleApprovalResponse: no session found for requestId={}", msg.requestId.value)
sendFrame( sendFrame(ServerMessage.ProtocolError("Unknown approval request"))
ServerMessage.ProtocolError(
message = "Unknown approval request",
sequence = null,
sessionSequence = null,
),
)
return return
} }
module.approvalCoordinator.handleResponse(msg, scopeSessionId)?.let { sendFrame(it) } module.approvalCoordinator.handleResponse(msg, scopeSessionId)?.let { sendFrame(it) }
} }
private fun errorResponse(message: String) = ServerMessage.ProtocolError( private fun errorResponse(message: String) = ServerMessage.ProtocolError(message)
message = message,
sequence = null,
sessionSequence = null,
)
private fun encodeError(message: String): Frame.Text = private fun encodeError(message: String): Frame.Text =
Frame.Text( Frame.Text(ProtocolSerializer.encodeServerMessage(ServerMessage.ProtocolError(message)))
ProtocolSerializer.encodeServerMessage(
ServerMessage.ProtocolError(
message = message,
sequence = null,
sessionSequence = null,
),
),
)
private suspend fun handleCreateGrant( private suspend fun handleCreateGrant(
msg: ClientMessage.CreateGrant, msg: ClientMessage.CreateGrant,
@@ -10,7 +10,7 @@ import com.correx.core.events.events.InferenceCompletedEvent
import com.correx.core.events.events.InferenceStartedEvent import com.correx.core.events.events.InferenceStartedEvent
import com.correx.core.events.events.InferenceTimeoutEvent import com.correx.core.events.events.InferenceTimeoutEvent
import com.correx.core.events.events.OrchestrationPausedEvent import com.correx.core.events.events.OrchestrationPausedEvent
import com.correx.core.events.events.SessionStartedEvent import com.correx.core.events.events.SteeringNoteAddedEvent
import com.correx.core.events.events.StageCompletedEvent import com.correx.core.events.events.StageCompletedEvent
import com.correx.core.events.events.StageFailedEvent import com.correx.core.events.events.StageFailedEvent
import com.correx.core.events.events.StoredEvent import com.correx.core.events.events.StoredEvent
@@ -371,7 +371,7 @@ class DomainEventMapperTest {
@Test @Test
fun `unmapped event returns null`(): Unit = runTest { fun `unmapped event returns null`(): Unit = runTest {
val event = storedEvent(SessionStartedEvent(sessionId = sessionId)) val event = storedEvent(SteeringNoteAddedEvent(sessionId = sessionId, content = "test"))
val result = domainEventToServerMessage(event, noopStore, sessionSequence = 0L) val result = domainEventToServerMessage(event, noopStore, sessionSequence = 0L)
assertNull(result) assertNull(result)
} }
@@ -392,12 +392,12 @@ class DomainEventMapperTest {
val origLevel = logger.level val origLevel = logger.level
logger.level = Level.DEBUG logger.level = Level.DEBUG
try { try {
val event = storedEvent(SessionStartedEvent(sessionId = sessionId)) val event = storedEvent(SteeringNoteAddedEvent(sessionId = sessionId, content = "test"))
val result = domainEventToServerMessage(event, noopStore, sessionSequence = 0L) val result = domainEventToServerMessage(event, noopStore, sessionSequence = 0L)
assertNull(result) assertNull(result)
assertEquals(1, appender.events.size) assertEquals(1, appender.events.size)
assertEquals(Level.DEBUG, appender.events[0].level) assertEquals(Level.DEBUG, appender.events[0].level)
assertTrue(appender.events[0].message.formattedMessage.contains("SessionStartedEvent")) assertTrue(appender.events[0].message.formattedMessage.contains("SteeringNoteAddedEvent"))
} finally { } finally {
logger.removeAppender(appender) logger.removeAppender(appender)
appender.stop() appender.stop()
@@ -25,10 +25,8 @@ import com.correx.core.kernel.orchestration.OrchestrationRepository
import com.correx.core.tools.contract.Tool import com.correx.core.tools.contract.Tool
import com.correx.core.tools.registry.ToolRegistry import com.correx.core.tools.registry.ToolRegistry
import com.correx.core.sessions.projections.replay.EventReplayer import com.correx.core.sessions.projections.replay.EventReplayer
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import kotlinx.datetime.Instant import kotlinx.datetime.Instant
import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertEquals
@@ -186,36 +184,6 @@ class SessionEventBridgeTest {
assertEquals(ServerMessage.SnapshotComplete, sent[1]) assertEquals(ServerMessage.SnapshotComplete, sent[1])
} }
@Test
fun `streamLive sends mapped messages from live flow`() = runTest {
val events = listOf(
storedEvent(WorkflowStartedEvent(sessionId, workflowId, stageId), seq = 1L),
storedEvent(WorkflowCompletedEvent(sessionId, stageId, 1), seq = 2L),
)
val liveFlow: Flow<StoredEvent> = flow { events.forEach { emit(it) } }
val store = fakeEventStore(liveFlow = liveFlow)
val sent = mutableListOf<ServerMessage>()
val bridge = SessionEventBridge(
store,
noopArtifactStore,
activeOrchestrationRepository(),
noopWorkflowRegistry,
noopToolRegistry,
) { sent.add(it) }
bridge.streamLive(sessionId)
assertEquals(2, sent.size)
assertEquals(
ServerMessage.SessionStarted(sessionId, workflowId, 1L, 1L),
sent[0],
)
assertEquals(
ServerMessage.SessionCompleted(sessionId, 2L, 2L),
sent[1],
)
}
@Test @Test
fun `replaySnapshot with no sessions emits only SnapshotComplete`() = runTest { fun `replaySnapshot with no sessions emits only SnapshotComplete`() = runTest {
val store = fakeEventStore(allEventsList = emptyList()) val store = fakeEventStore(allEventsList = emptyList())
@@ -266,29 +234,4 @@ class SessionEventBridgeTest {
assertEquals(3L, snapshot.lastSessionSequence) assertEquals(3L, snapshot.lastSessionSequence)
} }
@Test
fun `streamLive cancellation stops collection`() = runTest {
val channel = Channel<StoredEvent>(Channel.UNLIMITED)
val liveFlow: Flow<StoredEvent> = flow {
for (event in channel) emit(event)
}
val store = fakeEventStore(liveFlow = liveFlow)
val sent = mutableListOf<ServerMessage>()
val bridge = SessionEventBridge(
store,
noopArtifactStore,
activeOrchestrationRepository(),
noopWorkflowRegistry,
noopToolRegistry,
) { sent.add(it) }
channel.send(storedEvent(WorkflowStartedEvent(sessionId, workflowId, stageId), seq = 1L))
val job = launch { bridge.streamLive(sessionId) }
channel.send(storedEvent(WorkflowCompletedEvent(sessionId, stageId, 1), seq = 2L))
channel.close()
job.join()
assertEquals(2, sent.size)
}
} }
@@ -1,6 +1,5 @@
package com.correx.apps.tui.components package com.correx.apps.tui.components
import com.correx.apps.tui.state.ToolDisplayStatus
import com.correx.apps.tui.state.TuiState import com.correx.apps.tui.state.TuiState
import dev.tamboui.style.Style import dev.tamboui.style.Style
import dev.tamboui.text.Line import dev.tamboui.text.Line
@@ -62,12 +62,14 @@ object RootReducer {
log.debug("sessions state before reducers={}", state.sessions) log.debug("sessions state before reducers={}", state.sessions)
val (sessions, se) = SessionsReducer.reduce( val (sessions, se) = SessionsReducer.reduce(
sessions = afterInput.sessions, SessionsReducerContext(
displayState = afterInput.displayState, sessions = afterInput.sessions,
inputMode = prevInputMode, displayState = afterInput.displayState,
inputText = prevInputBuffer, inputMode = prevInputMode,
action = action, inputText = prevInputBuffer,
clock = clock, action = action,
clock = clock,
),
) )
log.debug("connection state before reducers={}", state.connection) log.debug("connection state before reducers={}", state.connection)
val (connection, ce) = ConnectionReducer.reduce(afterInput.connection, action) val (connection, ce) = ConnectionReducer.reduce(afterInput.connection, action)
@@ -22,40 +22,42 @@ import java.time.format.DateTimeFormatter
private val log = LoggerFactory.getLogger(SessionsReducer::class.simpleName) private val log = LoggerFactory.getLogger(SessionsReducer::class.simpleName)
data class SessionsReducerContext(
val sessions: SessionsState,
val displayState: DisplayState,
val inputMode: InputMode,
val inputText: String,
val action: Action,
val clock: () -> Long = System::currentTimeMillis,
)
object SessionsReducer { object SessionsReducer {
private val timeFormatter = DateTimeFormatter.ofPattern("HH:mm:ss").withZone(ZoneOffset.UTC) private val timeFormatter = DateTimeFormatter.ofPattern("HH:mm:ss").withZone(ZoneOffset.UTC)
fun reduce( fun reduce(ctx: SessionsReducerContext): Pair<SessionsState, List<Effect>> = when (ctx.action) {
sessions: SessionsState,
displayState: DisplayState,
inputMode: InputMode,
inputText: String,
action: Action,
clock: () -> Long = System::currentTimeMillis,
): Pair<SessionsState, List<Effect>> = when (action) {
is Action.NavigateUp -> when { is Action.NavigateUp -> when {
inputMode == InputMode.FILTER -> navigateUp(sessions) to emptyList() ctx.inputMode == InputMode.FILTER -> navigateUp(ctx.sessions) to emptyList()
displayState == DisplayState.IDLE -> navigateUp(sessions) to emptyList() ctx.displayState == DisplayState.IDLE -> navigateUp(ctx.sessions) to emptyList()
displayState == DisplayState.IN_SESSION -> sessions to emptyList() ctx.displayState == DisplayState.IN_SESSION -> ctx.sessions to emptyList()
else -> sessions to emptyList() else -> ctx.sessions to emptyList()
} }
is Action.NavigateDown -> when { is Action.NavigateDown -> when {
inputMode == InputMode.FILTER -> navigateDown(sessions) to emptyList() ctx.inputMode == InputMode.FILTER -> navigateDown(ctx.sessions) to emptyList()
displayState == DisplayState.IDLE -> navigateDown(sessions) to emptyList() ctx.displayState == DisplayState.IDLE -> navigateDown(ctx.sessions) to emptyList()
displayState == DisplayState.IN_SESSION -> sessions to emptyList() ctx.displayState == DisplayState.IN_SESSION -> ctx.sessions to emptyList()
else -> sessions to emptyList() else -> ctx.sessions to emptyList()
} }
is Action.SubmitInput -> when { is Action.SubmitInput -> when {
inputMode == InputMode.FILTER -> sessions.copy(filter = inputText) to emptyList() ctx.inputMode == InputMode.FILTER -> ctx.sessions.copy(filter = ctx.inputText) to emptyList()
displayState == DisplayState.IDLE -> { ctx.displayState == DisplayState.IDLE -> {
val text = inputText.trim() val text = ctx.inputText.trim()
val wfIndex = sessions.selectedWorkflowIndex val wfIndex = ctx.sessions.selectedWorkflowIndex
when { when {
wfIndex in sessions.workflows.indices -> { wfIndex in ctx.sessions.workflows.indices -> {
val wf = sessions.workflows[wfIndex] val wf = ctx.sessions.workflows[wfIndex]
sessions.copy(selectedWorkflowIndex = -1) to listOf( ctx.sessions.copy(selectedWorkflowIndex = -1) to listOf(
Effect.SendWs(ClientMessage.StartSession(workflowId = wf.workflowId, config = null)), Effect.SendWs(ClientMessage.StartSession(workflowId = wf.workflowId, config = null)),
) )
} }
@@ -67,56 +69,56 @@ object SessionsReducer {
status = "STARTING", status = "STARTING",
workflowId = "chat", workflowId = "chat",
name = "chat", name = "chat",
lastEventAt = clock(), lastEventAt = ctx.clock(),
) )
sessions.copy( ctx.sessions.copy(
sessions = sessions.sessions + optimistic, sessions = ctx.sessions.sessions + optimistic,
selectedId = sessionId.value, selectedId = sessionId.value,
) to listOf( ) to listOf(
Effect.SendWs(ClientMessage.StartChatSession(sessionId = sessionId, text = text)), Effect.SendWs(ClientMessage.StartChatSession(sessionId = sessionId, text = text)),
) )
} }
else -> sessions to emptyList() else -> ctx.sessions to emptyList()
} }
} }
displayState == DisplayState.IN_SESSION -> { ctx.displayState == DisplayState.IN_SESSION -> ctx.sessions.selectedId?.let { sid ->
sessions to listOf( ctx.sessions to listOf(
Effect.SendWs( Effect.SendWs(
ClientMessage.ChatInput( ClientMessage.ChatInput(
sessionId = SessionId(sessions.selectedId!!), sessionId = SessionId(sid),
text = inputText, text = ctx.inputText,
mode = ChatMode.CHAT, mode = ChatMode.CHAT,
), ),
), ),
) )
} } ?: (ctx.sessions to emptyList())
else -> sessions to emptyList() else -> ctx.sessions to emptyList()
} }
is Action.CancelInput -> if (inputMode == InputMode.FILTER) { is Action.CancelInput -> if (ctx.inputMode == InputMode.FILTER) {
sessions.copy(filter = "") to emptyList() ctx.sessions.copy(filter = "") to emptyList()
} else { } else {
sessions to emptyList() ctx.sessions to emptyList()
} }
is Action.CancelSelectedSession -> { is Action.CancelSelectedSession -> {
val id = sessions.selectedId val id = ctx.sessions.selectedId
if (id != null) { if (id != null) {
sessions to listOf(Effect.SendWs(ClientMessage.CancelSession(sessionId = SessionId(id)))) ctx.sessions to listOf(Effect.SendWs(ClientMessage.CancelSession(sessionId = SessionId(id))))
} else { } else {
sessions to emptyList() ctx.sessions to emptyList()
} }
} }
is Action.ToggleWorkflows -> sessions.copy( is Action.ToggleWorkflows -> ctx.sessions.copy(
workflowsVisible = !sessions.workflowsVisible, workflowsVisible = !ctx.sessions.workflowsVisible,
) to emptyList() ) to emptyList()
is Action.ServerEventReceived -> applyServerMessage(sessions, action.message, clock) is Action.ServerEventReceived -> applyServerMessage(ctx.sessions, ctx.action.message, ctx.clock)
else -> sessions to emptyList() else -> ctx.sessions to emptyList()
} }
private fun filteredSessions(sessions: SessionsState): List<SessionSummary> = private fun filteredSessions(sessions: SessionsState): List<SessionSummary> =
@@ -311,7 +313,6 @@ object SessionsReducer {
tier = tool.tier, tier = tool.tier,
status = displayStatus, status = displayStatus,
argsPreview = null, argsPreview = null,
diff = tool.diff,
) )
} }
val summary = SessionSummary( val summary = SessionSummary(
@@ -409,7 +410,7 @@ object SessionsReducer {
if (s.id == msg.sessionId.value) { if (s.id == msg.sessionId.value) {
val updatedTools = s.tools.map { t -> val updatedTools = s.tools.map { t ->
if (t.name == msg.toolName && t.status == ToolDisplayStatus.STARTED) { if (t.name == msg.toolName && t.status == ToolDisplayStatus.STARTED) {
t.copy(status = ToolDisplayStatus.COMPLETED, diff = msg.diff) t.copy(status = ToolDisplayStatus.COMPLETED)
} else { } else {
t t
} }
@@ -5,7 +5,7 @@ import com.correx.apps.server.protocol.ServerMessage
enum class InputMode { ROUTER, FILTER } enum class InputMode { ROUTER, FILTER }
enum class ToolDisplayStatus { REQUESTED, STARTED, COMPLETED, FAILED, REJECTED } enum class ToolDisplayStatus { STARTED, COMPLETED, FAILED, REJECTED }
enum class ProviderType { LOCAL, REMOTE } enum class ProviderType { LOCAL, REMOTE }
@@ -14,7 +14,6 @@ data class TuiToolRecord(
val tier: Int, val tier: Int,
val status: ToolDisplayStatus, val status: ToolDisplayStatus,
val argsPreview: String?, val argsPreview: String?,
val diff: String? = null,
) )
data class TuiEventEntry( data class TuiEventEntry(
@@ -41,7 +41,16 @@ class SessionsReducerTest {
inputMode: InputMode = InputMode.ROUTER, inputMode: InputMode = InputMode.ROUTER,
inputText: String = "", inputText: String = "",
action: Action, action: Action,
) = SessionsReducer.reduce(sessions, displayState, inputMode, inputText, action, fixedClock) ) = SessionsReducer.reduce(
SessionsReducerContext(
sessions = sessions,
displayState = displayState,
inputMode = inputMode,
inputText = inputText,
action = action,
clock = fixedClock,
),
)
@Test @Test
fun `NavigateUp wraps from top to bottom`() { fun `NavigateUp wraps from top to bottom`() {
@@ -1,11 +1,7 @@
package com.correx.core.artifacts package com.correx.core.artifacts
import com.correx.core.artifacts.model.ArtifactRelationship import com.correx.core.artifacts.model.ArtifactRelationship
import com.correx.core.events.events.ArtifactArchivedEvent
import com.correx.core.events.events.ArtifactCreatedEvent 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.ArtifactValidatedEvent
import com.correx.core.events.events.ArtifactValidatingEvent import com.correx.core.events.events.ArtifactValidatingEvent
import com.correx.core.events.events.StoredEvent import com.correx.core.events.events.StoredEvent
@@ -25,23 +21,6 @@ class DefaultArtifactReducer : ArtifactReducer {
transition(state, ArtifactLifecyclePhase.VALIDATING, ArtifactLifecyclePhase.CREATED) transition(state, ArtifactLifecyclePhase.VALIDATING, ArtifactLifecyclePhase.CREATED)
is ArtifactValidatedEvent -> is ArtifactValidatedEvent ->
transition(state, ArtifactLifecyclePhase.VALIDATED, ArtifactLifecyclePhase.VALIDATING) transition(state, ArtifactLifecyclePhase.VALIDATED, ArtifactLifecyclePhase.VALIDATING)
is ArtifactRejectedEvent ->
transition(state, ArtifactLifecyclePhase.REJECTED, ArtifactLifecyclePhase.VALIDATING)
is ArtifactSupersededEvent ->
transition(state, ArtifactLifecyclePhase.SUPERSEDED, ArtifactLifecyclePhase.VALIDATED)
is ArtifactArchivedEvent ->
transitionFromAny(
state,
ArtifactLifecyclePhase.ARCHIVED,
ArtifactLifecyclePhase.VALIDATED,
ArtifactLifecyclePhase.REJECTED,
)
is ArtifactRelationshipAddedEvent -> {
val rel = ArtifactRelationship(p.sourceId, p.targetId, p.relationshipType)
Result.success(
state.copy(lineage = state.lineage.copy(relationships = state.lineage.relationships + rel))
)
}
else -> Result.success(state) else -> Result.success(state)
} }
@@ -59,19 +38,4 @@ class DefaultArtifactReducer : ArtifactReducer {
) )
) )
} }
private fun transitionFromAny(
state: ArtifactState,
to: ArtifactLifecyclePhase,
vararg validFrom: ArtifactLifecyclePhase,
): Result<ArtifactState> =
if (state.phase in validFrom) {
Result.success(state.copy(phase = to))
} else {
Result.failure(
IllegalStateException(
"Invalid artifact transition: ${state.phase}$to (expected one of ${validFrom.toList()})"
)
)
}
} }
@@ -1,23 +1,8 @@
package com.correx.core.context package com.correx.core.context
import com.correx.core.context.state.ContextState 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.events.StoredEvent import com.correx.core.events.events.StoredEvent
class DefaultContextReducer : ContextReducer { class DefaultContextReducer : ContextReducer {
override fun reduce(state: ContextState, event: StoredEvent): ContextState = override fun reduce(state: ContextState, event: StoredEvent): ContextState = state
when (val p = event.payload) {
is ContextBuildingStartedEvent -> state.copy(buildingInProgress = true, interrupted = false)
is ContextPackBuiltEvent -> state.copy(
buildingInProgress = false,
interrupted = false,
builtPackIds = state.builtPackIds + p.contextPackId,
)
is ContextBuildingFailedEvent -> state.copy(buildingInProgress = false, interrupted = false)
is ContextBuildingInterruptedEvent -> state.copy(buildingInProgress = false, interrupted = true)
else -> state
}
} }
@@ -1,7 +1,6 @@
package com.correx.core.events.events package com.correx.core.events.events
import com.correx.core.events.types.ArtifactId import com.correx.core.events.types.ArtifactId
import com.correx.core.events.types.ArtifactRelationshipType
import com.correx.core.events.types.SessionId import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId import com.correx.core.events.types.StageId
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
@@ -23,33 +22,6 @@ data class ArtifactValidatedEvent(
val stageId: StageId, val stageId: StageId,
) : EventPayload ) : EventPayload
@Serializable
@SerialName("ArtifactSuperseded")
data class ArtifactSupersededEvent(
val artifactId: ArtifactId,
val supersededById: ArtifactId,
val sessionId: SessionId,
val stageId: StageId,
) : EventPayload
@Serializable
@SerialName("ArtifactRelationshipAdded")
data class ArtifactRelationshipAddedEvent(
val sourceId: ArtifactId,
val targetId: ArtifactId,
val relationshipType: ArtifactRelationshipType,
val sessionId: SessionId,
) : EventPayload
@Serializable
@SerialName("ArtifactRejected")
data class ArtifactRejectedEvent(
val artifactId: ArtifactId,
val sessionId: SessionId,
val stageId: StageId,
val reason: String,
) : EventPayload
@Serializable @Serializable
@SerialName("ArtifactCreated") @SerialName("ArtifactCreated")
data class ArtifactCreatedEvent( data class ArtifactCreatedEvent(
@@ -58,11 +30,3 @@ data class ArtifactCreatedEvent(
val stageId: StageId, val stageId: StageId,
val schemaVersion: Int, val schemaVersion: Int,
) : EventPayload ) : EventPayload
@Serializable
@SerialName("ArtifactArchived")
data class ArtifactArchivedEvent(
val artifactId: ArtifactId,
val sessionId: SessionId,
val stageId: StageId,
) : EventPayload
@@ -1,62 +1,10 @@
package com.correx.core.events.events package com.correx.core.events.events
import com.correx.core.events.types.ContextPackId
import com.correx.core.events.types.SessionId import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId import com.correx.core.events.types.StageId
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable
@SerialName("LayerTruncated")
data class LayerTruncatedEvent(
val contextPackId: ContextPackId,
val layer: String,
val entriesDropped: Int,
val reason: String,
) : EventPayload
@Serializable
@SerialName("ContextBuildingStarted")
data class ContextBuildingStartedEvent(
val sessionId: SessionId,
val stageId: StageId,
) : EventPayload
@Serializable
@SerialName("ContextBuildingFailed")
data class ContextBuildingFailedEvent(
val sessionId: SessionId,
val stageId: StageId,
val reason: String,
) : EventPayload
// Emitted during replay when a session ended with buildingInProgress=true and no completion/failure event followed.
@Serializable
@SerialName("ContextBuildingInterrupted")
data class ContextBuildingInterruptedEvent(
val sessionId: SessionId,
val stageId: StageId,
) : EventPayload
@Serializable
@SerialName("CompressionApplied")
data class CompressionAppliedEvent(
val contextPackId: ContextPackId,
val layer: String,
val entriesRemoved: Int,
val strategyApplied: String,
) : EventPayload
@Serializable
@SerialName("ContextPackBuilt")
data class ContextPackBuiltEvent(
val contextPackId: ContextPackId,
val sessionId: SessionId,
val stageId: StageId,
val budgetUsed: Int,
val budgetLimit: Int,
) : EventPayload
@Serializable @Serializable
@SerialName("SteeringNoteAdded") @SerialName("SteeringNoteAdded")
data class SteeringNoteAddedEvent( data class SteeringNoteAddedEvent(
@@ -12,6 +12,11 @@ data class EventMetadata(
val eventId: EventId, val eventId: EventId,
val sessionId: SessionId, val sessionId: SessionId,
val timestamp: Instant, val timestamp: Instant,
/**
* Reserved for future event schema migration. Currently always hardcoded to `1`.
* No version-dispatch logic exists yet; this field is persisted for forward compatibility.
* When migration is needed, wire a version-aware deserialization path before bumping.
*/
val schemaVersion: Int, val schemaVersion: Int,
val causationId: CausationId?, val causationId: CausationId?,
val correlationId: CorrelationId? val correlationId: CorrelationId?
@@ -4,41 +4,6 @@ import com.correx.core.events.types.SessionId
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable
@SerialName("SessionStarted")
data class SessionStartedEvent(
val sessionId: SessionId,
val initialContextId: String? = null
) : EventPayload
@Serializable
@SerialName("SessionPaused")
data class SessionPausedEvent(
val sessionId: SessionId,
val reason: String? = null
) : EventPayload
@Serializable
@SerialName("SessionResumed")
data class SessionResumedEvent(
val sessionId: SessionId,
) : EventPayload
@Serializable
@SerialName("SessionCompleted")
data class SessionCompletedEvent(
val sessionId: SessionId,
val summary: String? = null
) : EventPayload
@Serializable
@SerialName("SessionFailed")
data class SessionFailedEvent(
val sessionId: SessionId,
val errorCode: String? = null,
val errorMessage: String? = null
) : EventPayload
@Serializable @Serializable
@SerialName("ChatSessionStarted") @SerialName("ChatSessionStarted")
data class ChatSessionStartedEvent( data class ChatSessionStartedEvent(
@@ -6,14 +6,6 @@ import com.correx.core.events.types.TransitionId
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable
@SerialName("StageStarted")
data class StageStartedEvent(
val sessionId: SessionId,
val stageId: StageId,
val transitionId: TransitionId
) : EventPayload
@Serializable @Serializable
@SerialName("StageCompleted") @SerialName("StageCompleted")
data class StageCompletedEvent( data class StageCompletedEvent(
@@ -4,39 +4,23 @@ import com.correx.core.events.events.ApprovalDecisionResolvedEvent
import com.correx.core.events.events.ApprovalGrantCreatedEvent import com.correx.core.events.events.ApprovalGrantCreatedEvent
import com.correx.core.events.events.ApprovalGrantExpiredEvent import com.correx.core.events.events.ApprovalGrantExpiredEvent
import com.correx.core.events.events.ApprovalRequestedEvent import com.correx.core.events.events.ApprovalRequestedEvent
import com.correx.core.events.events.ArtifactArchivedEvent
import com.correx.core.events.events.ArtifactCreatedEvent 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.ArtifactValidatedEvent
import com.correx.core.events.events.ArtifactValidatingEvent import com.correx.core.events.events.ArtifactValidatingEvent
import com.correx.core.events.events.CompressionAppliedEvent import com.correx.core.events.events.ChatSessionStartedEvent
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.events.EventPayload import com.correx.core.events.events.EventPayload
import com.correx.core.events.events.InferenceCompletedEvent import com.correx.core.events.events.InferenceCompletedEvent
import com.correx.core.events.events.InferenceFailedEvent import com.correx.core.events.events.InferenceFailedEvent
import com.correx.core.events.events.InferenceStartedEvent import com.correx.core.events.events.InferenceStartedEvent
import com.correx.core.events.events.InferenceTimeoutEvent import com.correx.core.events.events.InferenceTimeoutEvent
import com.correx.core.events.events.LayerTruncatedEvent
import com.correx.core.events.events.ModelLoadedEvent import com.correx.core.events.events.ModelLoadedEvent
import com.correx.core.events.events.ModelUnloadedEvent import com.correx.core.events.events.ModelUnloadedEvent
import com.correx.core.events.events.OrchestrationPausedEvent import com.correx.core.events.events.OrchestrationPausedEvent
import com.correx.core.events.events.OrchestrationResumedEvent import com.correx.core.events.events.OrchestrationResumedEvent
import com.correx.core.events.events.RetryAttemptedEvent import com.correx.core.events.events.RetryAttemptedEvent
import com.correx.core.events.events.RiskAssessedEvent import com.correx.core.events.events.RiskAssessedEvent
import com.correx.core.events.events.SessionCompletedEvent
import com.correx.core.events.events.SessionFailedEvent
import com.correx.core.events.events.ChatSessionStartedEvent
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.StageCompletedEvent
import com.correx.core.events.events.StageFailedEvent import com.correx.core.events.events.StageFailedEvent
import com.correx.core.events.events.StageStartedEvent
import com.correx.core.events.events.SteeringNoteAddedEvent import com.correx.core.events.events.SteeringNoteAddedEvent
import com.correx.core.events.events.ToolExecutionCompletedEvent import com.correx.core.events.events.ToolExecutionCompletedEvent
import com.correx.core.events.events.ToolExecutionFailedEvent import com.correx.core.events.events.ToolExecutionFailedEvent
@@ -62,13 +46,7 @@ val eventModule = SerializersModule {
subclass(ToolExecutionCompletedEvent::class) subclass(ToolExecutionCompletedEvent::class)
subclass(ToolExecutionFailedEvent::class) subclass(ToolExecutionFailedEvent::class)
subclass(ToolExecutionRejectedEvent::class) subclass(ToolExecutionRejectedEvent::class)
subclass(SessionStartedEvent::class)
subclass(SessionPausedEvent::class)
subclass(SessionResumedEvent::class)
subclass(SessionCompletedEvent::class)
subclass(SessionFailedEvent::class)
subclass(SteeringNoteAddedEvent::class) subclass(SteeringNoteAddedEvent::class)
subclass(StageStartedEvent::class)
subclass(StageFailedEvent::class) subclass(StageFailedEvent::class)
subclass(StageCompletedEvent::class) subclass(StageCompletedEvent::class)
subclass(TransitionExecutedEvent::class) subclass(TransitionExecutedEvent::class)
@@ -76,19 +54,9 @@ val eventModule = SerializersModule {
subclass(ApprovalDecisionResolvedEvent::class) subclass(ApprovalDecisionResolvedEvent::class)
subclass(ApprovalGrantCreatedEvent::class) subclass(ApprovalGrantCreatedEvent::class)
subclass(ApprovalGrantExpiredEvent::class) subclass(ApprovalGrantExpiredEvent::class)
subclass(ContextBuildingStartedEvent::class)
subclass(ContextPackBuiltEvent::class)
subclass(CompressionAppliedEvent::class)
subclass(LayerTruncatedEvent::class)
subclass(ContextBuildingFailedEvent::class)
subclass(ContextBuildingInterruptedEvent::class)
subclass(ArtifactCreatedEvent::class) subclass(ArtifactCreatedEvent::class)
subclass(ArtifactValidatingEvent::class) subclass(ArtifactValidatingEvent::class)
subclass(ArtifactValidatedEvent::class) subclass(ArtifactValidatedEvent::class)
subclass(ArtifactRejectedEvent::class)
subclass(ArtifactSupersededEvent::class)
subclass(ArtifactArchivedEvent::class)
subclass(ArtifactRelationshipAddedEvent::class)
subclass(InferenceFailedEvent::class) subclass(InferenceFailedEvent::class)
subclass(InferenceCompletedEvent::class) subclass(InferenceCompletedEvent::class)
subclass(InferenceStartedEvent::class) subclass(InferenceStartedEvent::class)
@@ -1,6 +1,7 @@
package com.correx.core.utils package com.correx.core.utils
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import java.util.UUID
@JvmInline @JvmInline
@Serializable @Serializable
@@ -11,4 +12,12 @@ value class TypeId(val value: String) {
} }
override fun toString(): String = value override fun toString(): String = value
companion object {
/** Sentinel value used when no real stage/entity is available. */
val NONE = TypeId("none")
/** Create a new random ID using a UUID string. */
fun random(): TypeId = TypeId(UUID.randomUUID().toString())
}
} }
@@ -2,21 +2,20 @@ package com.correx.core.events
import com.correx.core.approvals.ApprovalOutcome import com.correx.core.approvals.ApprovalOutcome
import com.correx.core.approvals.ApprovalStatus import com.correx.core.approvals.ApprovalStatus
import com.correx.core.approvals.GrantScope
import com.correx.core.approvals.Tier import com.correx.core.approvals.Tier
import com.correx.core.events.events.ApprovalDecisionResolvedEvent import com.correx.core.events.events.ApprovalDecisionResolvedEvent
import com.correx.core.events.events.ContextPackBuiltEvent
import com.correx.core.events.events.EventPayload import com.correx.core.events.events.EventPayload
import com.correx.core.events.events.OrchestrationPausedEvent import com.correx.core.events.events.OrchestrationPausedEvent
import com.correx.core.events.events.RiskAssessedEvent import com.correx.core.events.events.RiskAssessedEvent
import com.correx.core.events.events.SessionStartedEvent import com.correx.core.events.events.SteeringNoteAddedEvent
import com.correx.core.events.events.ToolExecutionFailedEvent import com.correx.core.events.events.ToolExecutionFailedEvent
import com.correx.core.events.events.WorkflowCompletedEvent
import com.correx.core.events.events.WorkflowStartedEvent
import com.correx.core.events.risk.RiskAction import com.correx.core.events.risk.RiskAction
import com.correx.core.events.risk.RiskLevel import com.correx.core.events.risk.RiskLevel
import com.correx.core.events.serialization.eventJson import com.correx.core.events.serialization.eventJson
import com.correx.core.events.types.ApprovalDecisionId import com.correx.core.events.types.ApprovalDecisionId
import com.correx.core.events.types.ApprovalRequestId import com.correx.core.events.types.ApprovalRequestId
import com.correx.core.events.types.ContextPackId
import com.correx.core.events.types.RiskSummaryId import com.correx.core.events.types.RiskSummaryId
import com.correx.core.events.types.SessionId import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId import com.correx.core.events.types.StageId
@@ -35,7 +34,6 @@ class EventSerializationHardeningTest {
private val ts = Instant.parse("2026-01-01T00:00:00Z") private val ts = Instant.parse("2026-01-01T00:00:00Z")
private val payloads: List<Pair<String, EventPayload>> = listOf( private val payloads: List<Pair<String, EventPayload>> = listOf(
"SessionStarted" to SessionStartedEvent(sessionId = sessionId),
"ToolExecutionFailed" to ToolExecutionFailedEvent( "ToolExecutionFailed" to ToolExecutionFailedEvent(
invocationId = ToolInvocationId("inv-1"), invocationId = ToolInvocationId("inv-1"),
sessionId = sessionId, sessionId = sessionId,
@@ -51,12 +49,10 @@ class EventSerializationHardeningTest {
resolutionTimestamp = ts, resolutionTimestamp = ts,
reason = null reason = null
), ),
"ContextPackBuilt" to ContextPackBuiltEvent( "SteeringNoteAdded" to SteeringNoteAddedEvent(
contextPackId = ContextPackId("cp-1"),
sessionId = sessionId, sessionId = sessionId,
content = "test steering note",
stageId = stageId, stageId = stageId,
budgetUsed = 100,
budgetLimit = 4096
), ),
"OrchestrationPaused" to OrchestrationPausedEvent( "OrchestrationPaused" to OrchestrationPausedEvent(
sessionId = sessionId, sessionId = sessionId,
@@ -69,7 +65,17 @@ class EventSerializationHardeningTest {
riskSummaryId = RiskSummaryId("rs-1"), riskSummaryId = RiskSummaryId("rs-1"),
level = RiskLevel.MEDIUM, level = RiskLevel.MEDIUM,
action = RiskAction.PROCEED action = RiskAction.PROCEED
) ),
"WorkflowStarted" to WorkflowStartedEvent(
sessionId = sessionId,
workflowId = "test-wf",
startStageId = stageId,
),
"WorkflowCompleted" to WorkflowCompletedEvent(
sessionId = sessionId,
terminalStageId = stageId,
totalStages = 1,
),
) )
@Test @Test
@@ -97,7 +103,7 @@ class EventSerializationHardeningTest {
@Test @Test
fun `unknown fields in JSON are tolerated (ignoreUnknownKeys=true)`() { fun `unknown fields in JSON are tolerated (ignoreUnknownKeys=true)`() {
val event = SessionStartedEvent(sessionId = sessionId) val event = WorkflowStartedEvent(sessionId = sessionId, workflowId = "twf", startStageId = stageId)
val json = eventJson.encodeToString(EventPayload.serializer(), event) val json = eventJson.encodeToString(EventPayload.serializer(), event)
val withExtra = json.replace("{", "{\"bogusField\":123,") val withExtra = json.replace("{", "{\"bogusField\":123,")
assertDoesNotThrow { assertDoesNotThrow {
@@ -103,7 +103,7 @@ class DefaultRouterContextBuilder(
return ContextPack( return ContextPack(
id = ContextPackId("${state.sessionId?.value ?: "unknown"}-router-pack"), id = ContextPackId("${state.sessionId?.value ?: "unknown"}-router-pack"),
sessionId = state.sessionId ?: SessionId("unknown"), sessionId = state.sessionId ?: SessionId("unknown"),
stageId = state.currentStageId ?: StageId("none"), stageId = state.currentStageId ?: StageId.NONE,
layers = layers, layers = layers,
budgetUsed = budgetUsed, budgetUsed = budgetUsed,
budgetLimit = budget.limit, budgetLimit = budget.limit,
@@ -46,7 +46,7 @@ class DefaultRouterFacade(
history.add(RouterTurn(role = TurnRole.USER, content = input, timestamp = Clock.System.now())) history.add(RouterTurn(role = TurnRole.USER, content = input, timestamp = Clock.System.now()))
val stateWithHistory = state.copy(conversationHistory = history.toList()) val stateWithHistory = state.copy(conversationHistory = history.toList())
val effectiveStageId = state.currentStageId ?: StageId("none") val effectiveStageId = state.currentStageId ?: StageId.NONE
val contextPack = routerContextBuilder.build(stateWithHistory, config.tokenBudget) val contextPack = routerContextBuilder.build(stateWithHistory, config.tokenBudget)
val provider = inferenceRouter.route(effectiveStageId, setOf(ModelCapability.General)) val provider = inferenceRouter.route(effectiveStageId, setOf(ModelCapability.General))
@@ -1,13 +1,7 @@
package com.correx.core.sessions package com.correx.core.sessions
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.StageCompletedEvent
import com.correx.core.events.events.StageFailedEvent import com.correx.core.events.events.StageFailedEvent
import com.correx.core.events.events.StageStartedEvent
import com.correx.core.events.events.StoredEvent import com.correx.core.events.events.StoredEvent
import com.correx.core.events.events.TransitionExecutedEvent import com.correx.core.events.events.TransitionExecutedEvent
@@ -21,24 +15,9 @@ class DefaultSessionReducer : SessionReducer {
val payload = event.payload val payload = event.payload
val newStatus = when (payload) { val newStatus = when (payload) {
is SessionStartedEvent ->
SessionStatus.ACTIVE
is SessionPausedEvent ->
SessionStatus.PAUSED
is SessionResumedEvent ->
SessionStatus.ACTIVE
is SessionCompletedEvent ->
SessionStatus.COMPLETED
is SessionFailedEvent,
is StageFailedEvent -> is StageFailedEvent ->
SessionStatus.FAILED SessionStatus.FAILED
is StageStartedEvent,
is StageCompletedEvent, is StageCompletedEvent,
is TransitionExecutedEvent -> is TransitionExecutedEvent ->
SessionStatus.ACTIVE SessionStatus.ACTIVE
@@ -4,11 +4,7 @@ import com.correx.core.artifacts.ArtifactProjector
import com.correx.core.artifacts.ArtifactReducer import com.correx.core.artifacts.ArtifactReducer
import com.correx.core.artifacts.ArtifactState import com.correx.core.artifacts.ArtifactState
import com.correx.core.artifacts.repository.ArtifactRepository import com.correx.core.artifacts.repository.ArtifactRepository
import com.correx.core.events.events.ArtifactArchivedEvent
import com.correx.core.events.events.ArtifactCreatedEvent 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.ArtifactValidatedEvent
import com.correx.core.events.events.ArtifactValidatingEvent import com.correx.core.events.events.ArtifactValidatingEvent
import com.correx.core.events.events.StoredEvent import com.correx.core.events.events.StoredEvent
@@ -86,10 +82,6 @@ class LiveArtifactRepository(
is ArtifactCreatedEvent -> p.artifactId is ArtifactCreatedEvent -> p.artifactId
is ArtifactValidatingEvent -> p.artifactId is ArtifactValidatingEvent -> p.artifactId
is ArtifactValidatedEvent -> p.artifactId is ArtifactValidatedEvent -> p.artifactId
is ArtifactRejectedEvent -> p.artifactId
is ArtifactSupersededEvent -> p.artifactId
is ArtifactArchivedEvent -> p.artifactId
is ArtifactRelationshipAddedEvent -> p.sourceId
else -> null else -> null
} }
} }
@@ -1,11 +1,12 @@
import com.correx.core.events.events.EventMetadata import com.correx.core.events.events.EventMetadata
import com.correx.core.events.events.NewEvent import com.correx.core.events.events.NewEvent
import com.correx.core.events.events.SessionPausedEvent import com.correx.core.events.events.OrchestrationPausedEvent
import com.correx.core.events.events.SessionResumedEvent import com.correx.core.events.events.OrchestrationResumedEvent
import com.correx.core.events.events.SessionStartedEvent import com.correx.core.events.events.WorkflowStartedEvent
import com.correx.core.events.stores.EventStore import com.correx.core.events.stores.EventStore
import com.correx.core.events.types.EventId import com.correx.core.events.types.EventId
import com.correx.core.events.types.SessionId import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId
import com.correx.core.sessions.DefaultSessionReducer import com.correx.core.sessions.DefaultSessionReducer
import com.correx.core.sessions.SessionProjector import com.correx.core.sessions.SessionProjector
import com.correx.core.sessions.projections.replay.DefaultEventReplayer import com.correx.core.sessions.projections.replay.DefaultEventReplayer
@@ -30,14 +31,19 @@ class SessionReplayDeterminismTest {
val store2 = InMemoryEventStore() val store2 = InMemoryEventStore()
val events = mapOf( val events = mapOf(
EventMetadata(EventId("start"), sessionId, Clock.System.now(), 1, null, null) to SessionStartedEvent( EventMetadata(EventId("start"), sessionId, Clock.System.now(), 1, null, null) to WorkflowStartedEvent(
sessionId, sessionId,
workflowId = "test",
startStageId = StageId("stage-1"),
), ),
EventMetadata(EventId("paused"), sessionId, Clock.System.now(), 1, null, null) to SessionPausedEvent( EventMetadata(EventId("paused"), sessionId, Clock.System.now(), 1, null, null) to OrchestrationPausedEvent(
sessionId, sessionId,
stageId = StageId("stage-1"),
reason = "APPROVAL_PENDING",
), ),
EventMetadata(EventId("resumed"), sessionId, Clock.System.now(), 1, null, null) to SessionResumedEvent( EventMetadata(EventId("resumed"), sessionId, Clock.System.now(), 1, null, null) to OrchestrationResumedEvent(
sessionId, sessionId,
stageId = StageId("stage-1"),
), ),
) )
@@ -1,14 +1,9 @@
import com.correx.core.artifacts.ArtifactState import com.correx.core.artifacts.ArtifactState
import com.correx.core.artifacts.DefaultArtifactReducer import com.correx.core.artifacts.DefaultArtifactReducer
import com.correx.core.events.events.ArtifactArchivedEvent
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.ArtifactValidatedEvent
import com.correx.core.events.events.ArtifactValidatingEvent import com.correx.core.events.events.ArtifactValidatingEvent
import com.correx.core.events.types.ArtifactId import com.correx.core.events.types.ArtifactId
import com.correx.core.events.types.ArtifactLifecyclePhase 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.SessionId
import com.correx.core.events.types.StageId import com.correx.core.events.types.StageId
import com.correx.testing.fixtures.EventFixtures.stored import com.correx.testing.fixtures.EventFixtures.stored
@@ -48,42 +43,6 @@ class ArtifactReducerTest {
assertEquals(ArtifactLifecyclePhase.VALIDATED, result.getOrThrow().phase) 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 @Test
fun `CREATED to VALIDATED is invalid`() { fun `CREATED to VALIDATED is invalid`() {
val state = ArtifactState(phase = ArtifactLifecyclePhase.CREATED) val state = ArtifactState(phase = ArtifactLifecyclePhase.CREATED)
@@ -110,18 +69,4 @@ class ArtifactReducerTest {
assertTrue(result.isFailure) assertTrue(result.isFailure)
assertTrue(result.exceptionOrNull() is IllegalStateException) 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)
}
} }
@@ -1,8 +1,6 @@
import com.correx.core.context.ContextProjector import com.correx.core.context.ContextProjector
import com.correx.core.context.DefaultContextReducer import com.correx.core.context.DefaultContextReducer
import com.correx.core.events.events.ContextBuildingStartedEvent import com.correx.core.events.events.WorkflowStartedEvent
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.SessionId
import com.correx.core.events.types.StageId import com.correx.core.events.types.StageId
import com.correx.testing.fixtures.EventFixtures.stored import com.correx.testing.fixtures.EventFixtures.stored
@@ -14,7 +12,6 @@ class ContextProjectorTest {
private val projector = ContextProjector(DefaultContextReducer()) private val projector = ContextProjector(DefaultContextReducer())
private val sessionId = SessionId("s1") private val sessionId = SessionId("s1")
private val stageId = StageId("stage-1") private val stageId = StageId("stage-1")
private val packId = ContextPackId("pack-1")
@Test @Test
fun `initial state has no packs and is not building`() { fun `initial state has no packs and is not building`() {
@@ -26,8 +23,8 @@ class ContextProjectorTest {
@Test @Test
fun `replay is deterministic`() { fun `replay is deterministic`() {
val events = listOf( val events = listOf(
stored(payload = ContextBuildingStartedEvent(sessionId, stageId)), stored(payload = WorkflowStartedEvent(sessionId, workflowId = "test-wf", startStageId = stageId)),
stored(payload = ContextPackBuiltEvent(packId, sessionId, stageId, 100, 4000)) stored(payload = WorkflowStartedEvent(sessionId, workflowId = "test-wf", startStageId = stageId))
) )
val state1 = events.fold(projector.initial()) { s, e -> projector.apply(s, e) } val state1 = events.fold(projector.initial()) { s, e -> projector.apply(s, e) }
val state2 = events.fold(projector.initial()) { s, e -> projector.apply(s, e) } val state2 = events.fold(projector.initial()) { s, e -> projector.apply(s, e) }
@@ -1,16 +1,11 @@
import com.correx.core.context.DefaultContextReducer import com.correx.core.context.DefaultContextReducer
import com.correx.core.context.state.ContextState import com.correx.core.context.state.ContextState
import com.correx.core.events.events.ContextBuildingFailedEvent import com.correx.core.events.events.WorkflowStartedEvent
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.SessionId
import com.correx.core.events.types.StageId import com.correx.core.events.types.StageId
import com.correx.testing.fixtures.EventFixtures.stored import com.correx.testing.fixtures.EventFixtures.stored
import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
class DefaultContextReducerTest { class DefaultContextReducerTest {
@@ -18,70 +13,49 @@ class DefaultContextReducerTest {
private val reducer = DefaultContextReducer() private val reducer = DefaultContextReducer()
private val sessionId = SessionId("s1") private val sessionId = SessionId("s1")
private val stageId = StageId("stage-1") private val stageId = StageId("stage-1")
private val packId = ContextPackId("pack-1")
@Test @Test
fun `ContextBuildingStartedEvent sets buildingInProgress to true`() { fun `WorkflowStartedEvent leaves buildingInProgress unchanged`() {
val state = reducer.reduce( val state = reducer.reduce(
ContextState(), ContextState(),
stored(payload = ContextBuildingStartedEvent(sessionId, stageId)) stored(payload = WorkflowStartedEvent(sessionId, workflowId = "test-wf", startStageId = stageId))
) )
assertTrue(state.buildingInProgress) assertFalse(state.buildingInProgress)
} }
@Test @Test
fun `ContextPackBuiltEvent records pack and clears buildingInProgress`() { fun `WorkflowStartedEvent leaves builtPackIds unchanged`() {
val started = reducer.reduce( val state = reducer.reduce(
ContextState(), ContextState(),
stored(payload = ContextBuildingStartedEvent(sessionId, stageId)) stored(payload = WorkflowStartedEvent(sessionId, workflowId = "test-wf", startStageId = stageId))
) )
val built = reducer.reduce( assertEquals(0, state.builtPackIds.size)
started,
stored(payload = ContextPackBuiltEvent(packId, sessionId, stageId, 200, 4000))
)
assertFalse(built.buildingInProgress)
assertEquals(1, built.builtPackIds.size)
assertTrue(built.builtPackIds.contains(packId))
} }
@Test @Test
fun `ContextBuildingFailedEvent clears buildingInProgress`() { fun `WorkflowStartedEvent leaves buildingInProgress false`() {
val started = reducer.reduce( val started = reducer.reduce(
ContextState(), ContextState(),
stored(payload = ContextBuildingStartedEvent(sessionId, stageId)) stored(payload = WorkflowStartedEvent(sessionId, workflowId = "test-wf", startStageId = stageId))
) )
val failed = reducer.reduce( val after = reducer.reduce(
started, started,
stored(payload = ContextBuildingFailedEvent(sessionId, stageId, "timeout")) stored(payload = WorkflowStartedEvent(sessionId, workflowId = "test-wf", startStageId = stageId))
) )
assertFalse(failed.buildingInProgress) assertFalse(after.buildingInProgress)
} }
@Test @Test
fun `ContextBuildingInterruptedEvent clears buildingInProgress and sets interrupted`() { fun `WorkflowStartedEvent leaves interrupted unchanged`() {
val started = reducer.reduce( val started = reducer.reduce(
ContextState(), ContextState(),
stored(payload = ContextBuildingStartedEvent(sessionId, stageId)) stored(payload = WorkflowStartedEvent(sessionId, workflowId = "test-wf", startStageId = stageId))
) )
val interrupted = reducer.reduce( val after = reducer.reduce(
started, started,
stored(payload = ContextBuildingInterruptedEvent(sessionId, stageId)) stored(payload = WorkflowStartedEvent(sessionId, workflowId = "test-wf", startStageId = stageId))
) )
assertFalse(interrupted.buildingInProgress) assertFalse(after.interrupted)
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 @Test
@@ -1,12 +1,11 @@
import com.correx.core.events.events.SessionCompletedEvent import com.correx.core.events.events.OrchestrationPausedEvent
import com.correx.core.events.events.SessionFailedEvent import com.correx.core.events.events.OrchestrationResumedEvent
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.StageCompletedEvent
import com.correx.core.events.events.StageFailedEvent 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.events.TransitionExecutedEvent
import com.correx.core.events.events.WorkflowCompletedEvent
import com.correx.core.events.events.WorkflowFailedEvent
import com.correx.core.events.events.WorkflowStartedEvent
import com.correx.core.events.types.SessionId import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId import com.correx.core.events.types.StageId
import com.correx.core.events.types.TransitionId import com.correx.core.events.types.TransitionId
@@ -25,14 +24,29 @@ class DefaultSessionReducerTest {
private val sessionId = SessionId("session-1") private val sessionId = SessionId("session-1")
@Test @Test
fun `SessionStartedEvent sets ACTIVE status`() { fun `WorkflowStartedEvent leaves CREATED status`() {
val state = initialState() val state = initialState()
val result = reducer.reduce( val result = reducer.reduce(
state = state, state = state,
event = stored( event = stored(
sessionId = sessionId, sessionId = sessionId,
payload = SessionStartedEvent(sessionId) payload = WorkflowStartedEvent(sessionId, workflowId = "test-wf", startStageId = StageId("st-1"))
)
)
assertEquals(SessionStatus.CREATED, result.status)
}
@Test
fun `OrchestrationPausedEvent leaves ACTIVE status`() {
val state = activeState()
val result = reducer.reduce(
state = state,
event = stored(
sessionId = sessionId,
payload = OrchestrationPausedEvent(sessionId, stageId = StageId("st-1"), reason = "APPROVAL_PENDING")
) )
) )
@@ -40,14 +54,14 @@ class DefaultSessionReducerTest {
} }
@Test @Test
fun `SessionPausedEvent sets PAUSED status`() { fun `OrchestrationResumedEvent leaves PAUSED status`() {
val state = activeState() val state = pausedState()
val result = reducer.reduce( val result = reducer.reduce(
state = state, state = state,
event = stored( event = stored(
sessionId = sessionId, sessionId = sessionId,
payload = SessionPausedEvent(sessionId) payload = OrchestrationResumedEvent(sessionId, stageId = StageId("st-1"))
) )
) )
@@ -55,14 +69,14 @@ class DefaultSessionReducerTest {
} }
@Test @Test
fun `SessionResumedEvent sets ACTIVE status`() { fun `WorkflowCompletedEvent leaves ACTIVE status`() {
val state = pausedState() val state = activeState()
val result = reducer.reduce( val result = reducer.reduce(
state = state, state = state,
event = stored( event = stored(
sessionId = sessionId, sessionId = sessionId,
payload = SessionResumedEvent(sessionId) payload = WorkflowCompletedEvent(sessionId, terminalStageId = StageId("st-1"), totalStages = 0)
) )
) )
@@ -70,46 +84,32 @@ class DefaultSessionReducerTest {
} }
@Test @Test
fun `SessionCompletedEvent sets COMPLETED status`() { fun `WorkflowFailedEvent leaves ACTIVE status`() {
val state = activeState() val state = activeState()
val result = reducer.reduce( val result = reducer.reduce(
state = state, state = state,
event = stored( event = stored(
sessionId = sessionId, sessionId = sessionId,
payload = SessionCompletedEvent(sessionId) payload = WorkflowFailedEvent(sessionId, stageId = StageId("st-1"), reason = "error", retryExhausted = false)
) )
) )
assertEquals(SessionStatus.COMPLETED, result.status) assertEquals(SessionStatus.ACTIVE, result.status)
} }
@Test @Test
fun `SessionFailedEvent sets FAILED status`() { fun `TransitionExecutedEvent from StageStartedEvent sets ACTIVE 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 state = pausedState()
val result = reducer.reduce( val result = reducer.reduce(
state = state, state = state,
event = stored( event = stored(
sessionId = sessionId, sessionId = sessionId,
payload = StageStartedEvent( payload = TransitionExecutedEvent(
sessionId, sessionId,
stageId = StageId("stage-a"), from = StageId("st-1"),
to = StageId("st-1"),
transitionId = TransitionId("transition-a") transitionId = TransitionId("transition-a")
) )
) )
@@ -184,7 +184,7 @@ class DefaultSessionReducerTest {
val result = reducer.reduce( val result = reducer.reduce(
state = initialState(), state = initialState(),
event = stored( event = stored(
payload = SessionStartedEvent(sessionId), payload = WorkflowStartedEvent(sessionId, workflowId = "test-wf", startStageId = StageId("st-1")),
timestamp = timestamp timestamp = timestamp
) )
) )
@@ -204,7 +204,7 @@ class DefaultSessionReducerTest {
val result = reducer.reduce( val result = reducer.reduce(
state = state, state = state,
event = stored( event = stored(
payload = SessionPausedEvent(sessionId), payload = OrchestrationPausedEvent(sessionId, stageId = StageId("st-1"), reason = "APPROVAL_PENDING"),
timestamp = updatedAt timestamp = updatedAt
) )
) )
@@ -219,7 +219,7 @@ class DefaultSessionReducerTest {
val result = reducer.reduce( val result = reducer.reduce(
state = activeState(), state = activeState(),
event = stored( event = stored(
payload = SessionPausedEvent(sessionId), payload = OrchestrationPausedEvent(sessionId, stageId = StageId("st-1"), reason = "APPROVAL_PENDING"),
timestamp = timestamp timestamp = timestamp
) )
) )
@@ -1,11 +1,10 @@
import com.correx.core.events.events.SessionCompletedEvent import com.correx.core.events.events.OrchestrationPausedEvent
import com.correx.core.events.events.SessionPausedEvent import com.correx.core.events.events.OrchestrationResumedEvent
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.StageCompletedEvent
import com.correx.core.events.events.StageFailedEvent 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.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.EventId
import com.correx.core.events.types.SessionId import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId import com.correx.core.events.types.StageId
@@ -26,17 +25,17 @@ class SessionProjectorTest {
var state = projector.initial() var state = projector.initial()
val events = listOf( val events = listOf(
stored(eventId = EventId("e1"), payload = SessionStartedEvent(SessionId("s1"))), stored(eventId = EventId("e1"), payload = WorkflowStartedEvent(SessionId("s1"), workflowId = "test-wf", startStageId = StageId("st-1"))),
stored(eventId = EventId("e2"), payload = SessionPausedEvent(SessionId("s1"))), stored(eventId = EventId("e2"), payload = OrchestrationPausedEvent(SessionId("s1"), stageId = StageId("st-1"), reason = "APPROVAL_PENDING")),
stored(eventId = EventId("e3"), payload = SessionResumedEvent(SessionId("s1"))), stored(eventId = EventId("e3"), payload = OrchestrationResumedEvent(SessionId("s1"), stageId = StageId("st-1"))),
stored(eventId = EventId("e4"), payload = SessionCompletedEvent(SessionId("s1"))) stored(eventId = EventId("e4"), payload = WorkflowCompletedEvent(SessionId("s1"), terminalStageId = StageId("st-1"), totalStages = 0))
) )
events.forEach { events.forEach {
state = projector.apply(state, it) state = projector.apply(state, it)
} }
assertEquals(SessionStatus.COMPLETED, state.status) assertEquals(SessionStatus.CREATED, state.status)
} }
@Test @Test
@@ -46,13 +45,14 @@ class SessionProjectorTest {
val events = listOf( val events = listOf(
stored( stored(
eventId = EventId("e1"), eventId = EventId("e1"),
payload = SessionStartedEvent(SessionId("s1")) payload = WorkflowStartedEvent(SessionId("s1"), workflowId = "test-wf", startStageId = StageId("st-1"))
), ),
stored( stored(
eventId = EventId("e2"), eventId = EventId("e2"),
payload = StageStartedEvent( payload = TransitionExecutedEvent(
sessionId = SessionId("s1"), sessionId = SessionId("s1"),
stageId = StageId("draft"), from = StageId("draft"),
to = StageId("draft"),
transitionId = TransitionId("t1") transitionId = TransitionId("t1")
) )
), ),
@@ -81,7 +81,7 @@ class SessionProjectorTest {
val events = listOf( val events = listOf(
stored( stored(
eventId = EventId("e1"), eventId = EventId("e1"),
payload = SessionStartedEvent(SessionId("s1")) payload = WorkflowStartedEvent(SessionId("s1"), workflowId = "test-wf", startStageId = StageId("st-1"))
), ),
stored( stored(
eventId = EventId("e2"), eventId = EventId("e2"),
@@ -94,9 +94,10 @@ class SessionProjectorTest {
), ),
stored( stored(
eventId = EventId("e3"), eventId = EventId("e3"),
payload = StageStartedEvent( payload = TransitionExecutedEvent(
sessionId = SessionId("s1"), sessionId = SessionId("s1"),
stageId = StageId("b"), from = StageId("b"),
to = StageId("b"),
transitionId = TransitionId("t1") transitionId = TransitionId("t1")
) )
), ),
@@ -123,11 +124,11 @@ class SessionProjectorTest {
val events = listOf( val events = listOf(
stored( stored(
eventId = EventId("e1"), eventId = EventId("e1"),
payload = SessionStartedEvent(SessionId("s1")) payload = WorkflowStartedEvent(SessionId("s1"), workflowId = "test-wf", startStageId = StageId("st-1"))
), ),
stored( stored(
eventId = EventId("e2"), eventId = EventId("e2"),
payload = SessionCompletedEvent(SessionId("s1")) payload = WorkflowCompletedEvent(SessionId("s1"), terminalStageId = StageId("st-1"), totalStages = 0)
) )
) )
@@ -1,11 +1,12 @@
import com.correx.core.events.events.EventMetadata import com.correx.core.events.events.EventMetadata
import com.correx.core.events.events.NewEvent import com.correx.core.events.events.NewEvent
import com.correx.core.events.events.SessionCompletedEvent import com.correx.core.events.events.OrchestrationPausedEvent
import com.correx.core.events.events.SessionPausedEvent import com.correx.core.events.events.OrchestrationResumedEvent
import com.correx.core.events.events.SessionResumedEvent import com.correx.core.events.events.WorkflowCompletedEvent
import com.correx.core.events.events.SessionStartedEvent import com.correx.core.events.events.WorkflowStartedEvent
import com.correx.core.events.types.EventId import com.correx.core.events.types.EventId
import com.correx.core.events.types.SessionId import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId
import com.correx.core.sessions.DefaultSessionReducer import com.correx.core.sessions.DefaultSessionReducer
import com.correx.core.sessions.SessionProjector import com.correx.core.sessions.SessionProjector
import com.correx.core.sessions.SessionStatus import com.correx.core.sessions.SessionStatus
@@ -26,14 +27,21 @@ class SessionReplayTest {
val sessionId = SessionId("s1") val sessionId = SessionId("s1")
val metadataToPayload = mapOf( val metadataToPayload = mapOf(
EventMetadata(EventId("start"), sessionId, Clock.System.now(), 1, null, null) to SessionStartedEvent( EventMetadata(EventId("start"), sessionId, Clock.System.now(), 1, null, null) to WorkflowStartedEvent(
sessionId, sessionId,
workflowId = "test",
startStageId = StageId("stage-1"),
), ),
EventMetadata(EventId("paused"), sessionId, Clock.System.now(), 1, null, null) to SessionPausedEvent( EventMetadata(EventId("paused"), sessionId, Clock.System.now(), 1, null, null) to OrchestrationPausedEvent(
sessionId, sessionId,
stageId = StageId("stage-1"),
reason = "APPROVAL_PENDING",
), ),
EventMetadata(EventId("resumed"), sessionId, Clock.System.now(), 1, null, null) to SessionResumedEvent( EventMetadata(
EventId("resumed"), sessionId, Clock.System.now(), 1, null, null,
) to OrchestrationResumedEvent(
sessionId, sessionId,
stageId = StageId("stage-1"),
), ),
EventMetadata( EventMetadata(
EventId("completed"), EventId("completed"),
@@ -42,7 +50,7 @@ class SessionReplayTest {
1, 1,
null, null,
null, null,
) to SessionCompletedEvent(sessionId), ) to WorkflowCompletedEvent(sessionId, terminalStageId = StageId("stage-1"), totalStages = 1),
) )
metadataToPayload.map { (meta, payload) -> metadataToPayload.map { (meta, payload) ->
@@ -51,6 +59,6 @@ class SessionReplayTest {
val state = replayer.rebuild(sessionId) val state = replayer.rebuild(sessionId)
assertEquals(SessionStatus.COMPLETED, state.status) assertEquals(SessionStatus.CREATED, state.status)
} }
} }
@@ -1,9 +1,8 @@
import com.correx.core.events.events.SessionCompletedEvent
import com.correx.core.events.events.SessionStartedEvent
import com.correx.core.events.events.StageCompletedEvent import com.correx.core.events.events.StageCompletedEvent
import com.correx.core.events.events.StageFailedEvent 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.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.EventId
import com.correx.core.events.types.SessionId import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId import com.correx.core.events.types.StageId
@@ -32,7 +31,7 @@ class TransitionReplayIntegrationTest {
newEvent( newEvent(
sessionId = sessionId, sessionId = sessionId,
eventId = EventId("event-1"), eventId = EventId("event-1"),
payload = SessionStartedEvent(sessionId) payload = WorkflowStartedEvent(sessionId, workflowId = "test", startStageId = StageId("stage-1"))
), ),
newEvent( newEvent(
sessionId = sessionId, sessionId = sessionId,
@@ -47,9 +46,10 @@ class TransitionReplayIntegrationTest {
newEvent( newEvent(
sessionId = sessionId, sessionId = sessionId,
eventId = EventId("event-3"), eventId = EventId("event-3"),
payload = StageStartedEvent( payload = TransitionExecutedEvent(
sessionId = sessionId, sessionId = sessionId,
stageId = StageId("review"), from = StageId("review"),
to = StageId("review"),
transitionId = TransitionId("transition-1") transitionId = TransitionId("transition-1")
) )
), ),
@@ -65,7 +65,7 @@ class TransitionReplayIntegrationTest {
newEvent( newEvent(
sessionId = sessionId, sessionId = sessionId,
eventId = EventId("event-5"), eventId = EventId("event-5"),
payload = SessionCompletedEvent(sessionId) payload = WorkflowCompletedEvent(sessionId, terminalStageId = StageId("stage-1"), totalStages = 1)
) )
) )
) )
@@ -80,7 +80,7 @@ class TransitionReplayIntegrationTest {
val state = replayer.rebuild(sessionId) val state = replayer.rebuild(sessionId)
assertEquals( assertEquals(
SessionStatus.COMPLETED, SessionStatus.ACTIVE,
state.status state.status
) )
} }
@@ -95,7 +95,7 @@ class TransitionReplayIntegrationTest {
newEvent( newEvent(
sessionId = sessionId, sessionId = sessionId,
eventId = EventId("event-1"), eventId = EventId("event-1"),
payload = SessionStartedEvent(sessionId) payload = WorkflowStartedEvent(sessionId, workflowId = "test", startStageId = StageId("stage-1"))
), ),
newEvent( newEvent(
sessionId = sessionId, sessionId = sessionId,
@@ -110,9 +110,10 @@ class TransitionReplayIntegrationTest {
newEvent( newEvent(
sessionId = sessionId, sessionId = sessionId,
eventId = EventId("event-3"), eventId = EventId("event-3"),
payload = StageStartedEvent( payload = TransitionExecutedEvent(
sessionId = sessionId, sessionId = sessionId,
stageId = StageId("review"), from = StageId("review"),
to = StageId("review"),
transitionId = TransitionId("transition-1") transitionId = TransitionId("transition-1")
) )
), ),
@@ -150,7 +151,7 @@ class TransitionReplayIntegrationTest {
newEvent( newEvent(
sessionId = sessionId, sessionId = sessionId,
eventId = EventId("event-1"), eventId = EventId("event-1"),
payload = SessionStartedEvent(sessionId) payload = WorkflowStartedEvent(sessionId, workflowId = "test", startStageId = StageId("stage-1"))
), ),
newEvent( newEvent(
sessionId = sessionId, sessionId = sessionId,
@@ -165,9 +166,10 @@ class TransitionReplayIntegrationTest {
newEvent( newEvent(
sessionId = sessionId, sessionId = sessionId,
eventId = EventId("event-3"), eventId = EventId("event-3"),
payload = StageStartedEvent( payload = TransitionExecutedEvent(
sessionId = sessionId, sessionId = sessionId,
stageId = StageId("review"), from = StageId("review"),
to = StageId("review"),
transitionId = TransitionId("transition-1") transitionId = TransitionId("transition-1")
) )
), ),
@@ -1,7 +1,6 @@
import com.correx.core.events.events.EventPayload import com.correx.core.events.events.EventPayload
import com.correx.core.events.events.StageCompletedEvent import com.correx.core.events.events.StageCompletedEvent
import com.correx.core.events.events.StageFailedEvent 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.events.TransitionExecutedEvent
import com.correx.core.events.serialization.JsonEventSerializer import com.correx.core.events.serialization.JsonEventSerializer
import com.correx.core.events.types.SessionId import com.correx.core.events.types.SessionId
@@ -13,10 +12,11 @@ import org.junit.jupiter.api.Test
class TransitionEventSerializationTest { class TransitionEventSerializationTest {
@Test @Test
fun `StageStartedEvent serializes and deserializes`() { fun `TransitionExecutedEvent serializes and deserializes (stage start equivalent)`() {
val event = StageStartedEvent( val event = TransitionExecutedEvent(
sessionId = SessionId("session-1"), sessionId = SessionId("session-1"),
stageId = StageId("stage-a"), from = StageId("from"),
to = StageId("stage-a"),
transitionId = TransitionId("transition-a") transitionId = TransitionId("transition-a")
) )