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:
@@ -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")
|
||||
|
||||
+86
-62
@@ -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)
|
||||
|
||||
+11
-10
@@ -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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user