chore(damn): detekt, build, tests, formatting

fixed detekt issues where possible.
fixed disttar failing build because tools is added twice in the server module.
added workflowId where required.
fixed some tests not being recognized because of runBlocking without explicit return type.
formatting + imports.
This commit is contained in:
2026-05-22 00:10:05 +04:00
parent 2c459da009
commit f827685ed0
185 changed files with 1134 additions and 832 deletions
+7
View File
@@ -8,6 +8,13 @@ application {
mainClass = 'com.correx.apps.server.MainKt'
}
tasks.named('distTar') {
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}
tasks.named('distZip') {
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}
dependencies {
implementation project(':core:config')
implementation project(':core:events')
@@ -9,7 +9,6 @@ import io.ktor.serialization.kotlinx.json.json
import io.ktor.server.application.Application
import io.ktor.server.application.install
import io.ktor.server.plugins.calllogging.CallLogging
import org.slf4j.event.Level
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
import io.ktor.server.plugins.statuspages.StatusPages
import io.ktor.server.response.respond
@@ -18,6 +17,7 @@ import io.ktor.server.routing.routing
import io.ktor.server.websocket.WebSockets
import io.ktor.server.websocket.webSocket
import kotlinx.serialization.json.Json
import org.slf4j.event.Level
fun Application.configureServer(module: ServerModule) {
install(CallLogging) { level = Level.INFO }
@@ -49,9 +49,20 @@ import java.nio.file.Paths
private val log = LoggerFactory.getLogger("com.correx.apps.server.Main")
fun main() {
log.info("=== correx server starting ===")
log.info(" port : 8080")
val artifactStore = InfrastructureModule.createArtifactStore()
val eventStore = LoggingEventStore(InfrastructureModule.createEventStore(artifactStore))
logStoresInfo(eventStore, artifactStore)
val infraRegistry = InfrastructureModule.createProviderRegistry(listOf(buildLlamaProvider()))
val modelId = System.getenv("CORREX_MODEL_ID") ?: "default"
val modelPath = System.getenv("CORREX_MODEL_PATH") ?: "(not set)"
val llamaUrl = System.getenv("CORREX_LLAMA_URL") ?: "http://127.0.0.1:10000"
logModelInfo(modelId, modelPath, llamaUrl, infraRegistry)
val repositories = buildRepositories(eventStore)
val approvalEngine = DefaultApprovalEngine()
val correxConfig = ConfigLoader.load()
@@ -78,6 +89,9 @@ fun main() {
eventDispatcher = EventDispatcher(eventStore),
workDir = sandboxRoot,
)
logToolInfo(sandboxRoot, workingDir, shellAllowedExecutables, toolRegistry)
val inferenceRouter = DefaultInferenceRouter(infraRegistry, FirstAvailableRoutingStrategy())
val engines = OrchestratorEngines(
transitionResolver = DefaultTransitionResolver { condition, ctx -> condition.evaluate(ctx) },
@@ -116,52 +130,46 @@ fun main() {
defaultOrchestrationConfig = defaultOrchestrationConfig,
routerFacade = routerFacade,
)
val modelId = System.getenv("CORREX_MODEL_ID") ?: "default"
val modelPath = System.getenv("CORREX_MODEL_PATH") ?: "(not set)"
val llamaUrl = System.getenv("CORREX_LLAMA_URL") ?: "http://127.0.0.1:10000"
logServerStartup(
modelId,
modelPath,
llamaUrl,
sandboxRoot,
workingDir,
shellAllowedExecutables,
eventStore,
artifactStore,
toolRegistry,
infraRegistry,
)
log.info("==============================")
embeddedServer(Netty, port = 8080) { configureServer(module) }.start(wait = true)
}
private fun logServerStartup(
private fun logModelInfo(
modelId: String,
modelPath: String,
llamaUrl: String,
sandboxRoot: Path?,
workingDir: Path?,
shellAllowedExecutables: Set<String>,
eventStore: LoggingEventStore,
artifactStore: ArtifactStore,
toolRegistry: ToolRegistry,
infraRegistry: DefaultProviderRegistry,
) {
log.info("=== correx server starting ===")
log.info(" port : 8080")
log.info("==========MODEL INFO==========")
log.info(" model id : {}", modelId)
log.info(" model path : {}", modelPath)
log.info(" llama url : {}", llamaUrl)
log.info(" providers : {} registered", infraRegistry.listAll().size)
}
private fun logToolInfo(
sandboxRoot: Path?,
workingDir: Path?,
shellAllowedExecutables: Set<String>,
toolRegistry: ToolRegistry,
) {
log.info("==========TOOLS INFO==========")
log.info(" sandbox root : {}", sandboxRoot)
log.info(" working dir : {}", workingDir)
if (shellAllowedExecutables.isEmpty()) {
log.warn(" shell tool : no executable allowlist configured — all executables allowed")
}
log.info(" tools : {}", toolRegistry.all().map { it.name })
}
private fun logStoresInfo(
eventStore: LoggingEventStore,
artifactStore: ArtifactStore,
) {
log.info("==========STORE INFO==========")
log.info(" event store : {}", eventStore::class.simpleName)
log.info(" artifact store: {}", artifactStore::class.simpleName)
log.info(" tools : {}", toolRegistry.all().map { it.name })
log.info(" providers : {} registered", infraRegistry.listAll().size)
log.info("==============================")
}
private fun buildLlamaProvider(): LlamaCppInferenceProvider = InfrastructureModule.createLlamaCppProvider(
@@ -6,8 +6,8 @@ import com.correx.core.artifactstore.ArtifactStore
import com.correx.core.events.stores.EventStore
import com.correx.core.kernel.orchestration.DefaultSessionOrchestrator
import com.correx.core.kernel.orchestration.OrchestrationConfig
import com.correx.core.sessions.DefaultSessionRepository
import com.correx.core.router.RouterFacade
import com.correx.core.sessions.DefaultSessionRepository
data class ServerModule(
val orchestrator: DefaultSessionOrchestrator,
@@ -10,7 +10,6 @@ import com.correx.core.approvals.ApprovalStatus
import com.correx.core.approvals.Tier
import com.correx.core.approvals.UserSteering
import com.correx.core.approvals.model.ApprovalContext
import com.correx.core.approvals.model.ApprovalDecision as DomainApprovalDecision
import com.correx.core.approvals.model.ApprovalScopeIdentity
import com.correx.core.events.events.ApprovalRequestedEvent
import com.correx.core.events.types.ApprovalRequestId
@@ -25,7 +24,8 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.datetime.Clock
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.*
import com.correx.core.approvals.model.ApprovalDecision as DomainApprovalDecision
class ApprovalCoordinator(
private val orchestrator: ApprovalGateway,
@@ -5,13 +5,13 @@ import com.correx.apps.server.protocol.RiskSummaryDto
import com.correx.apps.server.protocol.ServerMessage
import com.correx.core.artifactstore.ArtifactStore
import com.correx.core.events.events.ApprovalRequestedEvent
import com.correx.core.events.types.ArtifactId
import com.correx.core.events.events.InferenceCompletedEvent
import com.correx.core.events.events.InferenceStartedEvent
import com.correx.core.events.events.InferenceTimeoutEvent
import com.correx.core.events.events.OrchestrationPausedEvent
import com.correx.core.events.events.StageCompletedEvent
import com.correx.core.events.events.StageFailedEvent
import com.correx.core.events.events.StoredEvent
import com.correx.core.events.events.ToolExecutionCompletedEvent
import com.correx.core.events.events.ToolExecutionFailedEvent
import com.correx.core.events.events.ToolExecutionRejectedEvent
@@ -20,7 +20,7 @@ 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.events.StoredEvent
import com.correx.core.events.types.ArtifactId
class DomainEventMapper(private val artifactStore: ArtifactStore = NoopArtifactStore) {
suspend fun map(event: StoredEvent): ServerMessage? =
@@ -39,47 +39,80 @@ suspend fun domainEventToServerMessage(event: StoredEvent, artifactStore: Artifa
is WorkflowStartedEvent -> mapWorkflowStarted(p)
is WorkflowCompletedEvent -> ServerMessage.SessionCompleted(sessionId = p.sessionId)
is WorkflowFailedEvent -> ServerMessage.SessionFailed(sessionId = p.sessionId, reason = p.reason)
is TransitionExecutedEvent -> ServerMessage.StageStarted(sessionId = p.sessionId, stageId = p.to)
is StageCompletedEvent -> ServerMessage.StageCompleted(sessionId = p.sessionId, stageId = p.stageId)
is StageFailedEvent -> ServerMessage.StageFailed(
sessionId = p.sessionId, stageId = p.stageId, reason = p.reason,
is TransitionExecutedEvent -> ServerMessage.StageStarted(
sessionId = p.sessionId,
stageId = p.to,
occurredAt = event.metadata.timestamp.toEpochMilliseconds(),
)
is StageCompletedEvent -> ServerMessage.StageCompleted(
sessionId = p.sessionId,
stageId = p.stageId,
occurredAt = event.metadata.timestamp.toEpochMilliseconds(),
)
is StageFailedEvent -> ServerMessage.StageFailed(
sessionId = p.sessionId,
stageId = p.stageId,
reason = p.reason,
occurredAt = event.metadata.timestamp.toEpochMilliseconds(),
)
is OrchestrationPausedEvent -> mapOrchestrationPaused(p)
is InferenceStartedEvent -> ServerMessage.InferenceStarted(sessionId = p.sessionId, stageId = p.stageId)
is InferenceCompletedEvent -> mapInferenceCompleted(p, artifactStore)
is InferenceCompletedEvent -> mapInferenceCompleted(event, p, artifactStore)
is InferenceTimeoutEvent -> ServerMessage.InferenceTimedOut(
sessionId = p.sessionId, stageId = p.stageId, elapsedMs = p.timeoutMs,
)
is ToolInvocationRequestedEvent -> ServerMessage.ToolStarted(
sessionId = p.sessionId, toolName = p.toolName, tier = p.tier,
)
is ToolExecutionCompletedEvent -> ServerMessage.ToolCompleted(
sessionId = p.sessionId, toolName = p.toolName, outputSummary = p.receipt.outputSummary,
sessionId = p.sessionId,
toolName = p.toolName,
outputSummary = p.receipt.outputSummary,
occurredAt = event.metadata.timestamp.toEpochMilliseconds(),
)
is ToolExecutionFailedEvent -> ServerMessage.ToolFailed(
sessionId = p.sessionId, toolName = p.toolName, reason = p.reason,
sessionId = p.sessionId,
toolName = p.toolName,
reason = p.reason,
occurredAt = event.metadata.timestamp.toEpochMilliseconds(),
)
is ToolExecutionRejectedEvent -> ServerMessage.ToolRejected(
sessionId = p.sessionId, toolName = p.toolName, reason = p.reason,
)
is ApprovalRequestedEvent -> mapApprovalRequested(p)
else -> null
}
private fun mapWorkflowStarted(p: WorkflowStartedEvent): ServerMessage =
ServerMessage.SessionStarted(sessionId = p.sessionId, workflowId = p.startStageId.value)
ServerMessage.SessionStarted(sessionId = p.sessionId, workflowId = p.workflowId)
private fun mapOrchestrationPaused(p: OrchestrationPausedEvent): ServerMessage {
val reason = if (p.reason == "APPROVAL_PENDING") PauseReason.APPROVAL_PENDING else PauseReason.USER_REQUESTED
return ServerMessage.SessionPaused(sessionId = p.sessionId, reason = reason)
}
private suspend fun mapInferenceCompleted(p: InferenceCompletedEvent, artifactStore: ArtifactStore): ServerMessage {
private suspend fun mapInferenceCompleted(
event: StoredEvent,
p: InferenceCompletedEvent,
artifactStore: ArtifactStore,
): ServerMessage {
val text = runCatching {
artifactStore.get(p.responseArtifactId)?.toString(Charsets.UTF_8) ?: ""
}.getOrElse { "" }
return ServerMessage.InferenceCompleted(
sessionId = p.sessionId, stageId = p.stageId, outputSummary = text, responseText = text,
sessionId = p.sessionId,
stageId = p.stageId,
outputSummary = text,
responseText = text,
occurredAt = event.metadata.timestamp.toEpochMilliseconds(),
)
}
@@ -1,7 +1,7 @@
package com.correx.apps.server.protocol
import kotlinx.serialization.json.Json
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
class ProtocolException(message: String, cause: Throwable? = null) : RuntimeException(message, cause)
@@ -21,13 +21,13 @@ sealed class ServerMessage {
data class SessionFailed(val sessionId: SessionId, val reason: String) : ServerMessage()
@Serializable
data class StageStarted(val sessionId: SessionId, val stageId: StageId) : ServerMessage()
data class StageStarted(val sessionId: SessionId, val stageId: StageId, val occurredAt: Long) : ServerMessage()
@Serializable
data class StageCompleted(val sessionId: SessionId, val stageId: StageId) : ServerMessage()
data class StageCompleted(val sessionId: SessionId, val stageId: StageId, val occurredAt: Long) : ServerMessage()
@Serializable
data class StageFailed(val sessionId: SessionId, val stageId: StageId, val reason: String) :
data class StageFailed(val sessionId: SessionId, val stageId: StageId, val reason: String, val occurredAt: Long) :
ServerMessage()
@Serializable
@@ -39,6 +39,7 @@ sealed class ServerMessage {
val stageId: StageId,
val outputSummary: String,
val responseText: String = "",
val occurredAt: Long,
) : ServerMessage()
@Serializable
@@ -50,11 +51,15 @@ sealed class ServerMessage {
ServerMessage()
@Serializable
data class ToolCompleted(val sessionId: SessionId, val toolName: String, val outputSummary: String) :
ServerMessage()
data class ToolCompleted(
val sessionId: SessionId,
val toolName: String,
val outputSummary: String,
val occurredAt: Long,
) : ServerMessage()
@Serializable
data class ToolFailed(val sessionId: SessionId, val toolName: String, val reason: String) :
data class ToolFailed(val sessionId: SessionId, val toolName: String, val reason: String, val occurredAt: Long) :
ServerMessage()
@Serializable
@@ -1,8 +1,8 @@
package com.correx.apps.server.registry
import com.correx.core.events.types.ProviderId
import com.correx.core.inference.InferenceProvider
import com.correx.core.inference.ProviderHealth
import com.correx.core.events.types.ProviderId
interface ProviderRegistry {
fun listAll(): List<InferenceProvider>
@@ -10,7 +10,6 @@ import com.correx.core.events.types.EventId
import com.correx.core.events.types.SessionId
import com.correx.core.utils.TypeId
import io.ktor.http.HttpStatusCode
import org.slf4j.LoggerFactory
import io.ktor.server.request.receive
import io.ktor.server.response.respond
import io.ktor.server.routing.Route
@@ -21,7 +20,8 @@ import io.ktor.server.websocket.webSocket
import kotlinx.coroutines.launch
import kotlinx.datetime.Clock
import kotlinx.serialization.Serializable
import java.util.UUID
import org.slf4j.LoggerFactory
import java.util.*
@Serializable
data class StartSessionRequest(val workflowId: String, val config: SessionConfigDto? = null)
@@ -21,7 +21,7 @@ import kotlinx.coroutines.channels.ClosedReceiveChannelException
import kotlinx.coroutines.launch
import kotlinx.datetime.Clock
import org.slf4j.LoggerFactory
import java.util.UUID
import java.util.*
private val log = LoggerFactory.getLogger(GlobalStreamHandler::class.java)
@@ -10,12 +10,10 @@ import com.correx.apps.server.protocol.ServerMessage.RouterResponseMessage
import com.correx.core.approvals.ApprovalOutcome
import com.correx.core.approvals.ApprovalStatus
import com.correx.core.approvals.Tier
import com.correx.core.approvals.model.ApprovalContext
import com.correx.core.approvals.model.ApprovalDecision as DomainApprovalDecision
import com.correx.core.approvals.model.ApprovalScopeIdentity
import com.correx.core.approvals.UserSteering
import com.correx.core.approvals.model.ApprovalContext
import com.correx.core.approvals.model.ApprovalScopeIdentity
import com.correx.core.events.types.SessionId
import com.correx.core.router.RouterFacade
import com.correx.core.sessions.ApprovalMode
import com.correx.core.utils.TypeId
import io.ktor.server.websocket.DefaultWebSocketServerSession
@@ -24,6 +22,7 @@ import io.ktor.websocket.readText
import kotlinx.coroutines.channels.ClosedReceiveChannelException
import kotlinx.datetime.Clock
import org.slf4j.LoggerFactory
import com.correx.core.approvals.model.ApprovalDecision as DomainApprovalDecision
private val log = LoggerFactory.getLogger(SessionStreamHandler::class.java)
@@ -75,11 +74,20 @@ class SessionStreamHandler(private val module: ServerModule) {
runCatching { module.orchestrator.cancel(msg.sessionId) }
.onFailure { log.error("cancel failed for session={}: {}", msg.sessionId.value, it.message, it) }
}
is ClientMessage.ApprovalResponse -> {
val domainDecision = msg.toDomainDecision(msg.requestId.value)
runCatching { module.orchestrator.submitApprovalDecision(msg.requestId, domainDecision) }
.onFailure { log.error("submitApprovalDecision failed for request={}: {}", msg.requestId.value, it.message, it) }
.onFailure {
log.error(
"submitApprovalDecision failed for request={}: {}",
msg.requestId.value,
it.message,
it,
)
}
}
is ClientMessage.ChatInput -> {
runCatching {
module.routerFacade.onUserInput(
@@ -88,21 +96,24 @@ class SessionStreamHandler(private val module: ServerModule) {
mode = msg.mode,
)
}.onSuccess { response ->
session.send(Frame.Text(
ProtocolSerializer.encodeServerMessage(
RouterResponseMessage(
sessionId = msg.sessionId,
content = response.content,
steeringEmitted = response.steeringEmitted,
)
)
))
session.send(
Frame.Text(
ProtocolSerializer.encodeServerMessage(
RouterResponseMessage(
sessionId = msg.sessionId,
content = response.content,
steeringEmitted = response.steeringEmitted,
),
),
),
)
}.onFailure {
log.error("routerFacade.onUserInput failed for session={}: {}", msg.sessionId.value, it.message, it)
val error = ServerMessage.ProtocolError("Router error: ${it.message}")
session.send(Frame.Text(ProtocolSerializer.encodeServerMessage(error)))
}
}
else -> {
log.warn("unexpected message type={} in session stream", msg::class.simpleName)
val error = ServerMessage.ProtocolError("Unexpected message type in session stream")
@@ -24,8 +24,8 @@ 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.ArtifactId
import com.correx.core.events.types.ApprovalRequestId
import com.correx.core.events.types.ArtifactId
import com.correx.core.events.types.EventId
import com.correx.core.events.types.InferenceRequestId
import com.correx.core.events.types.ProviderId
@@ -35,8 +35,8 @@ import com.correx.core.events.types.ToolInvocationId
import com.correx.core.events.types.TransitionId
import com.correx.core.events.types.ValidationReportId
import com.correx.core.inference.TokenUsage
import kotlinx.datetime.Instant
import kotlinx.coroutines.test.runTest
import kotlinx.datetime.Instant
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Test
@@ -45,7 +45,9 @@ class DomainEventMapperTest {
private val sessionId = SessionId("session-1")
private val stageId = StageId("stage-1")
private val workflowId = "workflow-test"
private val timestamp = Instant.parse("2026-01-01T00:00:00Z")
private val occurredAt = timestamp.toEpochMilliseconds()
private val noopStore: ArtifactStore = object : ArtifactStore {
override suspend fun put(bytes: ByteArray): ArtifactId = ArtifactId("art-1")
override suspend fun get(id: ArtifactId): ByteArray? = null
@@ -68,15 +70,21 @@ class DomainEventMapperTest {
@Test
fun `WorkflowStartedEvent maps to SessionStarted`(): Unit = runTest {
val event = storedEvent(WorkflowStartedEvent(sessionId = sessionId, startStageId = stageId))
val event =
storedEvent(WorkflowStartedEvent(sessionId = sessionId, startStageId = stageId, workflowId = workflowId))
val result = domainEventToServerMessage(event, noopStore)
assertEquals(ServerMessage.SessionStarted(sessionId, stageId.value), result)
assertEquals(ServerMessage.SessionStarted(sessionId, workflowId), result)
}
@Test
fun `WorkflowCompletedEvent maps to SessionCompleted`(): Unit = runTest {
val event = storedEvent(
WorkflowCompletedEvent(sessionId = sessionId, terminalStageId = stageId, totalStages = 1),
WorkflowCompletedEvent(
sessionId = sessionId,
terminalStageId = stageId,
totalStages = 1,
workflowId = workflowId,
),
)
val result = domainEventToServerMessage(event, noopStore)
assertEquals(ServerMessage.SessionCompleted(sessionId), result)
@@ -99,7 +107,7 @@ class DomainEventMapperTest {
TransitionExecutedEvent(sessionId = sessionId, from = from, to = to, transitionId = TransitionId("t1")),
)
val result = domainEventToServerMessage(event, noopStore)
assertEquals(ServerMessage.StageStarted(sessionId, to), result)
assertEquals(ServerMessage.StageStarted(sessionId, to, occurredAt), result)
}
@Test
@@ -108,7 +116,7 @@ class DomainEventMapperTest {
StageCompletedEvent(sessionId = sessionId, stageId = stageId, transitionId = TransitionId("t1")),
)
val result = domainEventToServerMessage(event, noopStore)
assertEquals(ServerMessage.StageCompleted(sessionId, stageId), result)
assertEquals(ServerMessage.StageCompleted(sessionId, stageId, occurredAt), result)
}
@Test
@@ -119,7 +127,7 @@ class DomainEventMapperTest {
),
)
val result = domainEventToServerMessage(event, noopStore)
assertEquals(ServerMessage.StageFailed(sessionId, stageId, "err"), result)
assertEquals(ServerMessage.StageFailed(sessionId, stageId, "err", occurredAt), result)
}
@Test
@@ -142,13 +150,15 @@ class DomainEventMapperTest {
@Test
fun `InferenceStartedEvent maps to InferenceStarted`(): Unit = runTest {
val event = storedEvent(InferenceStartedEvent(
requestId = InferenceRequestId("req-1"),
sessionId = sessionId,
stageId = stageId,
providerId = ProviderId("p1"),
promptArtifactId = ArtifactId("art-prompt"),
))
val event = storedEvent(
InferenceStartedEvent(
requestId = InferenceRequestId("req-1"),
sessionId = sessionId,
stageId = stageId,
providerId = ProviderId("p1"),
promptArtifactId = ArtifactId("art-prompt"),
),
)
val result = domainEventToServerMessage(event, noopStore)
assertEquals(ServerMessage.InferenceStarted(sessionId, stageId), result)
}
@@ -161,45 +171,55 @@ class DomainEventMapperTest {
override suspend fun put(bytes: ByteArray): ArtifactId = ArtifactId("x")
override suspend fun get(id: ArtifactId): ByteArray? =
if (id == artifactId) responseText.toByteArray(Charsets.UTF_8) else null
override suspend fun flushBefore(commit: suspend () -> Unit) = commit()
}
val event = storedEvent(InferenceCompletedEvent(
requestId = InferenceRequestId("req-1"),
sessionId = sessionId,
stageId = stageId,
providerId = ProviderId("p1"),
tokensUsed = TokenUsage(10, 5),
latencyMs = 100L,
responseArtifactId = artifactId,
))
val event = storedEvent(
InferenceCompletedEvent(
requestId = InferenceRequestId("req-1"),
sessionId = sessionId,
stageId = stageId,
providerId = ProviderId("p1"),
tokensUsed = TokenUsage(10, 5),
latencyMs = 100L,
responseArtifactId = artifactId,
),
)
val result = domainEventToServerMessage(event, store)
assertEquals(ServerMessage.InferenceCompleted(sessionId, stageId, responseText, responseText), result)
assertEquals(
ServerMessage.InferenceCompleted(sessionId, stageId, responseText, responseText, occurredAt),
result,
)
}
@Test
fun `InferenceCompletedEvent falls back to empty string when artifact missing`(): Unit = runTest {
val event = storedEvent(InferenceCompletedEvent(
requestId = InferenceRequestId("req-1"),
sessionId = sessionId,
stageId = stageId,
providerId = ProviderId("p1"),
tokensUsed = TokenUsage(10, 5),
latencyMs = 100L,
responseArtifactId = ArtifactId("missing"),
))
val event = storedEvent(
InferenceCompletedEvent(
requestId = InferenceRequestId("req-1"),
sessionId = sessionId,
stageId = stageId,
providerId = ProviderId("p1"),
tokensUsed = TokenUsage(10, 5),
latencyMs = 100L,
responseArtifactId = ArtifactId("missing"),
),
)
val result = domainEventToServerMessage(event, noopStore)
assertEquals(ServerMessage.InferenceCompleted(sessionId, stageId, "", ""), result)
assertEquals(ServerMessage.InferenceCompleted(sessionId, stageId, "", "", occurredAt), result)
}
@Test
fun `InferenceTimeoutEvent maps to InferenceTimedOut`(): Unit = runTest {
val event = storedEvent(InferenceTimeoutEvent(
requestId = InferenceRequestId("req-1"),
sessionId = sessionId,
stageId = stageId,
providerId = ProviderId("p1"),
timeoutMs = 5000L,
))
val event = storedEvent(
InferenceTimeoutEvent(
requestId = InferenceRequestId("req-1"),
sessionId = sessionId,
stageId = stageId,
providerId = ProviderId("p1"),
timeoutMs = 5000L,
),
)
val result = domainEventToServerMessage(event, noopStore)
assertEquals(ServerMessage.InferenceTimedOut(sessionId, stageId, 5000L), result)
}
@@ -207,16 +227,18 @@ class DomainEventMapperTest {
@Test
fun `ToolInvocationRequestedEvent maps to ToolStarted`(): Unit = runTest {
val invId = ToolInvocationId("inv-1")
val event = storedEvent(ToolInvocationRequestedEvent(
invocationId = invId,
sessionId = sessionId,
stageId = stageId,
toolName = "file_write",
tier = Tier.T2,
request = ToolRequest(
invocationId = invId, sessionId = sessionId, stageId = stageId, toolName = "file_write",
val event = storedEvent(
ToolInvocationRequestedEvent(
invocationId = invId,
sessionId = sessionId,
stageId = stageId,
toolName = "file_write",
tier = Tier.T2,
request = ToolRequest(
invocationId = invId, sessionId = sessionId, stageId = stageId, toolName = "file_write",
),
),
))
)
val result = domainEventToServerMessage(event, noopStore)
assertEquals(ServerMessage.ToolStarted(sessionId, "file_write", Tier.T2), result)
}
@@ -239,7 +261,7 @@ class DomainEventMapperTest {
),
)
val result = domainEventToServerMessage(event, noopStore)
assertEquals(ServerMessage.ToolCompleted(sessionId, "file_write", "wrote 3 lines"), result)
assertEquals(ServerMessage.ToolCompleted(sessionId, "file_write", "wrote 3 lines", occurredAt), result)
}
@Test
@@ -253,7 +275,7 @@ class DomainEventMapperTest {
),
)
val result = domainEventToServerMessage(event, noopStore)
assertEquals(ServerMessage.ToolFailed(sessionId, "file_write", "disk full"), result)
assertEquals(ServerMessage.ToolFailed(sessionId, "file_write", "disk full", occurredAt), result)
}
@Test
@@ -274,15 +296,17 @@ class DomainEventMapperTest {
@Test
fun `ApprovalRequestedEvent maps to ApprovalRequired`(): Unit = runTest {
val requestId = ApprovalRequestId("req-1")
val event = storedEvent(ApprovalRequestedEvent(
requestId = requestId,
tier = Tier.T3,
validationReportId = ValidationReportId("vr-1"),
riskSummaryId = null,
sessionId = sessionId,
stageId = stageId,
projectId = null,
))
val event = storedEvent(
ApprovalRequestedEvent(
requestId = requestId,
tier = Tier.T3,
validationReportId = ValidationReportId("vr-1"),
riskSummaryId = null,
sessionId = sessionId,
stageId = stageId,
projectId = null,
),
)
val result = domainEventToServerMessage(event, noopStore) as ServerMessage.ApprovalRequired
assertEquals(requestId, result.requestId)
assertEquals(Tier.T3, result.tier)
@@ -5,8 +5,8 @@ import com.correx.core.artifactstore.ArtifactStore
import com.correx.core.events.events.EventMetadata
import com.correx.core.events.events.EventPayload
import com.correx.core.events.events.NewEvent
import com.correx.core.events.events.StoredEvent
import com.correx.core.events.events.OrchestrationResumedEvent
import com.correx.core.events.events.StoredEvent
import com.correx.core.events.events.WorkflowCompletedEvent
import com.correx.core.events.events.WorkflowStartedEvent
import com.correx.core.events.stores.EventStore
@@ -27,6 +27,7 @@ class SessionEventBridgeTest {
private val sessionId = SessionId("session-1")
private val stageId = StageId("stage-1")
private val workflowId = "workflow-test"
private val timestamp = Instant.parse("2026-01-01T00:00:00Z")
private val noopArtifactStore: ArtifactStore = object : ArtifactStore {
@@ -65,8 +66,8 @@ class SessionEventBridgeTest {
@Test
fun `replaySnapshot sends mapped messages for all events`() = runTest {
val events = listOf(
storedEvent(WorkflowStartedEvent(sessionId, stageId), seq = 1L),
storedEvent(WorkflowCompletedEvent(sessionId, stageId, 1), seq = 2L),
storedEvent(WorkflowStartedEvent(sessionId, workflowId, stageId), seq = 1L),
storedEvent(WorkflowCompletedEvent(sessionId, stageId, 1, workflowId), seq = 2L),
)
val store = fakeEventStore(allEventsList = events)
val sent = mutableListOf<ServerMessage>()
@@ -75,7 +76,7 @@ class SessionEventBridgeTest {
bridge.replaySnapshot()
assertEquals(2, sent.size)
assertEquals(ServerMessage.SessionStarted(sessionId, stageId.value), sent[0])
assertEquals(ServerMessage.SessionStarted(sessionId, workflowId), sent[0])
assertEquals(ServerMessage.SessionCompleted(sessionId), sent[1])
}
@@ -83,7 +84,7 @@ class SessionEventBridgeTest {
fun `replaySnapshot skips unmapped events`() = runTest {
val events = listOf(
storedEvent(OrchestrationResumedEvent(sessionId, stageId), seq = 1L),
storedEvent(WorkflowCompletedEvent(sessionId, stageId, 1), seq = 2L),
storedEvent(WorkflowCompletedEvent(sessionId, stageId, 1, workflowId), seq = 2L),
)
val store = fakeEventStore(allEventsList = events)
val sent = mutableListOf<ServerMessage>()
@@ -98,8 +99,8 @@ class SessionEventBridgeTest {
@Test
fun `streamLive sends mapped messages from live flow`() = runTest {
val events = listOf(
storedEvent(WorkflowStartedEvent(sessionId, stageId), seq = 1L),
storedEvent(WorkflowCompletedEvent(sessionId, stageId, 1), seq = 2L),
storedEvent(WorkflowStartedEvent(sessionId, workflowId, stageId), seq = 1L),
storedEvent(WorkflowCompletedEvent(sessionId, stageId, 1, workflowId), seq = 2L),
)
val liveFlow: Flow<StoredEvent> = flow { events.forEach { emit(it) } }
val store = fakeEventStore(liveFlow = liveFlow)
@@ -109,7 +110,7 @@ class SessionEventBridgeTest {
bridge.streamLive(sessionId)
assertEquals(2, sent.size)
assertEquals(ServerMessage.SessionStarted(sessionId, stageId.value), sent[0])
assertEquals(ServerMessage.SessionStarted(sessionId, workflowId), sent[0])
assertEquals(ServerMessage.SessionCompleted(sessionId), sent[1])
}
@@ -123,10 +124,10 @@ class SessionEventBridgeTest {
val sent = mutableListOf<ServerMessage>()
val bridge = SessionEventBridge(store, noopArtifactStore) { sent.add(it) }
channel.send(storedEvent(WorkflowStartedEvent(sessionId, stageId), seq = 1L))
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.send(storedEvent(WorkflowCompletedEvent(sessionId, stageId, 1, workflowId), seq = 2L))
channel.close()
job.join()