diff --git a/apps/server/build.gradle b/apps/server/build.gradle index aa1b7f97..6a436bf7 100644 --- a/apps/server/build.gradle +++ b/apps/server/build.gradle @@ -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') diff --git a/apps/server/src/main/kotlin/com/correx/apps/server/Application.kt b/apps/server/src/main/kotlin/com/correx/apps/server/Application.kt index df5991b4..ecd4824e 100644 --- a/apps/server/src/main/kotlin/com/correx/apps/server/Application.kt +++ b/apps/server/src/main/kotlin/com/correx/apps/server/Application.kt @@ -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 } diff --git a/apps/server/src/main/kotlin/com/correx/apps/server/Main.kt b/apps/server/src/main/kotlin/com/correx/apps/server/Main.kt index 1c50cdae..f66d97bf 100644 --- a/apps/server/src/main/kotlin/com/correx/apps/server/Main.kt +++ b/apps/server/src/main/kotlin/com/correx/apps/server/Main.kt @@ -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, - 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, + 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( diff --git a/apps/server/src/main/kotlin/com/correx/apps/server/ServerModule.kt b/apps/server/src/main/kotlin/com/correx/apps/server/ServerModule.kt index ccefc383..b1dac07a 100644 --- a/apps/server/src/main/kotlin/com/correx/apps/server/ServerModule.kt +++ b/apps/server/src/main/kotlin/com/correx/apps/server/ServerModule.kt @@ -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, diff --git a/apps/server/src/main/kotlin/com/correx/apps/server/approval/ApprovalCoordinator.kt b/apps/server/src/main/kotlin/com/correx/apps/server/approval/ApprovalCoordinator.kt index 62f9c108..4bf4e91d 100644 --- a/apps/server/src/main/kotlin/com/correx/apps/server/approval/ApprovalCoordinator.kt +++ b/apps/server/src/main/kotlin/com/correx/apps/server/approval/ApprovalCoordinator.kt @@ -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, diff --git a/apps/server/src/main/kotlin/com/correx/apps/server/bridge/DomainEventMapper.kt b/apps/server/src/main/kotlin/com/correx/apps/server/bridge/DomainEventMapper.kt index 85fb3d5b..aa2418fb 100644 --- a/apps/server/src/main/kotlin/com/correx/apps/server/bridge/DomainEventMapper.kt +++ b/apps/server/src/main/kotlin/com/correx/apps/server/bridge/DomainEventMapper.kt @@ -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(), ) } diff --git a/apps/server/src/main/kotlin/com/correx/apps/server/protocol/ProtocolSerializer.kt b/apps/server/src/main/kotlin/com/correx/apps/server/protocol/ProtocolSerializer.kt index ec641d42..10006a37 100644 --- a/apps/server/src/main/kotlin/com/correx/apps/server/protocol/ProtocolSerializer.kt +++ b/apps/server/src/main/kotlin/com/correx/apps/server/protocol/ProtocolSerializer.kt @@ -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) diff --git a/apps/server/src/main/kotlin/com/correx/apps/server/protocol/ServerMessage.kt b/apps/server/src/main/kotlin/com/correx/apps/server/protocol/ServerMessage.kt index bbdc8673..6006ff08 100644 --- a/apps/server/src/main/kotlin/com/correx/apps/server/protocol/ServerMessage.kt +++ b/apps/server/src/main/kotlin/com/correx/apps/server/protocol/ServerMessage.kt @@ -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 diff --git a/apps/server/src/main/kotlin/com/correx/apps/server/registry/ProviderRegistry.kt b/apps/server/src/main/kotlin/com/correx/apps/server/registry/ProviderRegistry.kt index 71d904b9..5b2b8737 100644 --- a/apps/server/src/main/kotlin/com/correx/apps/server/registry/ProviderRegistry.kt +++ b/apps/server/src/main/kotlin/com/correx/apps/server/registry/ProviderRegistry.kt @@ -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 diff --git a/apps/server/src/main/kotlin/com/correx/apps/server/routes/SessionRoutes.kt b/apps/server/src/main/kotlin/com/correx/apps/server/routes/SessionRoutes.kt index a29f30dc..17af7e3c 100644 --- a/apps/server/src/main/kotlin/com/correx/apps/server/routes/SessionRoutes.kt +++ b/apps/server/src/main/kotlin/com/correx/apps/server/routes/SessionRoutes.kt @@ -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) diff --git a/apps/server/src/main/kotlin/com/correx/apps/server/ws/GlobalStreamHandler.kt b/apps/server/src/main/kotlin/com/correx/apps/server/ws/GlobalStreamHandler.kt index 30027cf8..1fe5e5ae 100644 --- a/apps/server/src/main/kotlin/com/correx/apps/server/ws/GlobalStreamHandler.kt +++ b/apps/server/src/main/kotlin/com/correx/apps/server/ws/GlobalStreamHandler.kt @@ -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) diff --git a/apps/server/src/main/kotlin/com/correx/apps/server/ws/SessionStreamHandler.kt b/apps/server/src/main/kotlin/com/correx/apps/server/ws/SessionStreamHandler.kt index 7c1f6cd9..ec3c6cda 100644 --- a/apps/server/src/main/kotlin/com/correx/apps/server/ws/SessionStreamHandler.kt +++ b/apps/server/src/main/kotlin/com/correx/apps/server/ws/SessionStreamHandler.kt @@ -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") diff --git a/apps/server/src/test/kotlin/com/correx/apps/server/bridge/DomainEventMapperTest.kt b/apps/server/src/test/kotlin/com/correx/apps/server/bridge/DomainEventMapperTest.kt index 8caa10d6..9f746e16 100644 --- a/apps/server/src/test/kotlin/com/correx/apps/server/bridge/DomainEventMapperTest.kt +++ b/apps/server/src/test/kotlin/com/correx/apps/server/bridge/DomainEventMapperTest.kt @@ -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) diff --git a/apps/server/src/test/kotlin/com/correx/apps/server/bridge/SessionEventBridgeTest.kt b/apps/server/src/test/kotlin/com/correx/apps/server/bridge/SessionEventBridgeTest.kt index 7b29d5a2..34f7c03c 100644 --- a/apps/server/src/test/kotlin/com/correx/apps/server/bridge/SessionEventBridgeTest.kt +++ b/apps/server/src/test/kotlin/com/correx/apps/server/bridge/SessionEventBridgeTest.kt @@ -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() @@ -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() @@ -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 = 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() 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() diff --git a/apps/tui/src/main/kotlin/com/correx/apps/tui/TuiApp.kt b/apps/tui/src/main/kotlin/com/correx/apps/tui/TuiApp.kt index 5a67895b..194bab07 100644 --- a/apps/tui/src/main/kotlin/com/correx/apps/tui/TuiApp.kt +++ b/apps/tui/src/main/kotlin/com/correx/apps/tui/TuiApp.kt @@ -20,12 +20,12 @@ import dev.tamboui.terminal.Frame import dev.tamboui.tui.EventHandler import dev.tamboui.tui.Renderer import dev.tamboui.tui.TuiRunner -import dev.tamboui.tui.event.KeyEvent as TambouiKeyEvent import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking +import dev.tamboui.tui.event.KeyEvent as TambouiKeyEvent private const val DEFAULT_PORT = 8080 private const val STATUS_HEIGHT = 1 @@ -70,7 +70,7 @@ fun main(args: Array) { val handler = EventHandler { event, _ -> if (event is TambouiKeyEvent) { - mapKey(event, state.inputMode) + mapKey(event) ?.let { KeyResolver.resolve(it, state.inputMode, state.inputBuffer) } ?.let(::dispatch) } diff --git a/apps/tui/src/main/kotlin/com/correx/apps/tui/components/InputBar.kt b/apps/tui/src/main/kotlin/com/correx/apps/tui/components/InputBar.kt index f4b63c97..4ee8d878 100644 --- a/apps/tui/src/main/kotlin/com/correx/apps/tui/components/InputBar.kt +++ b/apps/tui/src/main/kotlin/com/correx/apps/tui/components/InputBar.kt @@ -23,50 +23,11 @@ fun inputBarWidget(state: TuiState): Paragraph { ?: "(no session)" val (row1, row2) = when (state.inputMode) { - InputMode.ROUTER -> { - val cursor = if (state.inputBuffer.isNotEmpty()) { - Line.from(Span.raw("▌ "), Span.raw(state.inputBuffer)) - } else { - Line.from(Span.raw("▌ "), Span.styled("Ask anything…", dimStyle)) - } - val hints = buildList { - add("$sessionName · router") - add("tab navigate") - if (hasApproval) { add("ctrl+a approve"); add("ctrl+r reject") } - if (hasSession) add("ctrl+s steer") - add("ctrl+e events") - add("ctrl+q") - }.joinToString(" ") - cursor to Line.from(Span.styled(hints, dimStyle)) - } - InputMode.NAVIGATE -> { - val row1Line = Line.from(Span.styled(" ↑↓ sessions enter select", dimStyle)) - val hints = buildList { - add("$sessionName · navigate") - add("tab router") - if (hasApproval) { add("ctrl+a approve"); add("ctrl+r reject") } - if (hasSession) add("ctrl+s steer") - add("ctrl+e events") - add("ctrl+q") - }.joinToString(" ") - row1Line to Line.from(Span.styled(hints, dimStyle)) - } - InputMode.FILTER -> { - val cursor = if (state.inputBuffer.isNotEmpty()) { - Line.from(Span.raw("▌ "), Span.raw(state.inputBuffer)) - } else { - Line.from(Span.raw("▌ "), Span.styled("/filter…", dimStyle)) - } - cursor to Line.from(Span.styled("filtering sessions esc clear enter apply", dimStyle)) - } - InputMode.STEER -> { - val cursor = if (state.inputBuffer.isNotEmpty()) { - Line.from(Span.raw("▌ "), Span.raw(state.inputBuffer)) - } else { - Line.from(Span.raw("▌ "), Span.styled("Steer the session…", dimStyle)) - } - cursor to Line.from(Span.styled("$sessionName · steering esc cancel enter send", dimStyle)) - } + InputMode.ROUTER -> createRowsForRouter(state, dimStyle, sessionName, hasApproval, hasSession) + InputMode.NAVIGATE -> createRowsForNavigate(dimStyle, sessionName, hasApproval, hasSession) + InputMode.FILTER -> createRowsForFilter(state, dimStyle) + InputMode.STEER -> createRowsForSteer(state, dimStyle, sessionName) + } val block = Block.builder() @@ -77,3 +38,73 @@ fun inputBarWidget(state: TuiState): Paragraph { return Paragraph.builder().text(Text.from(listOf(row1, row2))).block(block).build() } + +private fun createRowsForSteer( + state: TuiState, + dimStyle: Style?, + sessionName: String, +): Pair { + val cursor = if (state.inputBuffer.isNotEmpty()) { + Line.from(Span.raw("▌ "), Span.raw(state.inputBuffer)) + } else { + Line.from(Span.raw("▌ "), Span.styled("Steer the session…", dimStyle)) + } + return cursor to Line.from(Span.styled("$sessionName · steering esc cancel enter send", dimStyle)) +} + +private fun createRowsForFilter( + state: TuiState, + dimStyle: Style?, +): Pair { + val cursor = if (state.inputBuffer.isNotEmpty()) { + Line.from(Span.raw("▌ "), Span.raw(state.inputBuffer)) + } else { + Line.from(Span.raw("▌ "), Span.styled("/filter…", dimStyle)) + } + return cursor to Line.from(Span.styled("filtering sessions esc clear enter apply", dimStyle)) +} + +private fun createRowsForNavigate( + dimStyle: Style?, + sessionName: String, + hasApproval: Boolean, + hasSession: Boolean, +): Pair { + val row1Line = Line.from(Span.styled(" ↑↓ sessions enter select", dimStyle)) + val hints = buildList { + add("$sessionName · navigate") + add("tab router") + if (hasApproval) { + add("ctrl+a approve"); add("ctrl+r reject") + } + if (hasSession) add("ctrl+s steer") + add("ctrl+e events") + add("ctrl+q") + }.joinToString(" ") + return row1Line to Line.from(Span.styled(hints, dimStyle)) +} + +private fun createRowsForRouter( + state: TuiState, + dimStyle: Style?, + sessionName: String, + hasApproval: Boolean, + hasSession: Boolean, +): Pair { + val cursor = if (state.inputBuffer.isNotEmpty()) { + Line.from(Span.raw("▌ "), Span.raw(state.inputBuffer)) + } else { + Line.from(Span.raw("▌ "), Span.styled("Ask anything…", dimStyle)) + } + val hints = buildList { + add("$sessionName · router") + add("tab navigate") + if (hasApproval) { + add("ctrl+a approve"); add("ctrl+r reject") + } + if (hasSession) add("ctrl+s steer") + add("ctrl+e events") + add("ctrl+q") + }.joinToString(" ") + return cursor to Line.from(Span.styled(hints, dimStyle)) +} diff --git a/apps/tui/src/main/kotlin/com/correx/apps/tui/components/RouterPanel.kt b/apps/tui/src/main/kotlin/com/correx/apps/tui/components/RouterPanel.kt index e1ea8cf7..0e86fcb5 100644 --- a/apps/tui/src/main/kotlin/com/correx/apps/tui/components/RouterPanel.kt +++ b/apps/tui/src/main/kotlin/com/correx/apps/tui/components/RouterPanel.kt @@ -1,7 +1,6 @@ package com.correx.apps.tui.components import com.correx.apps.tui.state.TuiState -import dev.tamboui.style.Color import dev.tamboui.style.Style import dev.tamboui.text.Line import dev.tamboui.text.Span diff --git a/apps/tui/src/main/kotlin/com/correx/apps/tui/components/SessionList.kt b/apps/tui/src/main/kotlin/com/correx/apps/tui/components/SessionList.kt index 63de0db9..1edc2780 100644 --- a/apps/tui/src/main/kotlin/com/correx/apps/tui/components/SessionList.kt +++ b/apps/tui/src/main/kotlin/com/correx/apps/tui/components/SessionList.kt @@ -28,7 +28,6 @@ fun sessionListWidget(state: TuiState): Paragraph { val isSelected = s.id == state.sessions.selectedId val prefix = if (isSelected) Span.styled("▶ ", Style.create().blue()) else Span.raw(" ") val idSpan = Span.styled("[${s.id.take(ID_LEN)}]", dimStyle) - val statusStr = s.status.uppercase().padEnd(STATUS_WIDTH) val statusSpan = statusSpan(s) val rawName = s.name.ifEmpty { s.workflowId } val truncated = if (rawName.length > NAME_MAX) rawName.take(NAME_MAX - 1) + "…" else rawName @@ -61,6 +60,7 @@ private fun statusSpan(s: SessionSummary): Span { fun filteredSessions(sessions: SessionsState): List { val f = sessions.filter - return if (f.isEmpty()) sessions.sessions - else sessions.sessions.filter { it.workflowId.contains(f, ignoreCase = true) } + val sorted = sessions.sessions.sortedByDescending { it.lastEventAt } + return if (f.isEmpty()) sorted + else sorted.filter { it.workflowId.contains(f, ignoreCase = true) } } diff --git a/apps/tui/src/main/kotlin/com/correx/apps/tui/input/TambouiKeyMapper.kt b/apps/tui/src/main/kotlin/com/correx/apps/tui/input/TambouiKeyMapper.kt index 5d6c9675..e61f1a57 100644 --- a/apps/tui/src/main/kotlin/com/correx/apps/tui/input/TambouiKeyMapper.kt +++ b/apps/tui/src/main/kotlin/com/correx/apps/tui/input/TambouiKeyMapper.kt @@ -1,24 +1,23 @@ package com.correx.apps.tui.input import com.correx.apps.tui.KeyEvent -import com.correx.apps.tui.state.InputMode import dev.tamboui.tui.event.KeyCode import dev.tamboui.tui.event.KeyEvent as TambouiKeyEvent @Suppress("CyclomaticComplexMethod", "ReturnCount") -fun mapKey(event: TambouiKeyEvent, mode: InputMode): KeyEvent? { - if (event.isCtrlC()) return KeyEvent.Quit +fun mapKey(event: TambouiKeyEvent): KeyEvent? { + if (event.isCtrlC) return KeyEvent.Quit if (event.hasAlt()) return null if (event.hasCtrl()) { - return when (event.character()) { - 'q' -> KeyEvent.Quit - 'n' -> KeyEvent.NewSession - 'c' -> KeyEvent.Cancel - 'a' -> KeyEvent.Approve - 'r' -> KeyEvent.Reject - 's' -> KeyEvent.Steer - 'h' -> KeyEvent.ToggleApprovalOverlay - 'e' -> KeyEvent.ToggleEventOverlay + return when { + event.isChar('q') -> KeyEvent.Quit + event.isChar('n') -> KeyEvent.NewSession + event.isChar('c') -> KeyEvent.Cancel + event.isChar('a') -> KeyEvent.Approve + event.isChar('r') -> KeyEvent.Reject + event.isChar('s') -> KeyEvent.Steer + event.isChar('h') -> KeyEvent.ToggleApprovalOverlay + event.isChar('e') -> KeyEvent.ToggleEventOverlay else -> null } } diff --git a/apps/tui/src/main/kotlin/com/correx/apps/tui/reducer/RootReducer.kt b/apps/tui/src/main/kotlin/com/correx/apps/tui/reducer/RootReducer.kt index c7d65cb2..c257c74e 100644 --- a/apps/tui/src/main/kotlin/com/correx/apps/tui/reducer/RootReducer.kt +++ b/apps/tui/src/main/kotlin/com/correx/apps/tui/reducer/RootReducer.kt @@ -17,11 +17,13 @@ object RootReducer { val (approval, ae) = ApprovalReducer.reduce(afterInput.approval, prevInputMode, action) val (connection, ce) = ConnectionReducer.reduce(afterInput.connection, action) val (provider, pe) = ProviderReducer.reduce(afterInput.provider, action) + val approvalJustArrived = approval.active != null && afterInput.approval.active == null val withSubReducers = afterInput.copy( sessions = sessions, approval = approval, connection = connection, provider = provider, + approvalOverlayVisible = if (approvalJustArrived) true else afterInput.approvalOverlayVisible, ) val final = when (action) { is Action.ToggleApprovalOverlay -> withSubReducers.copy( diff --git a/apps/tui/src/main/kotlin/com/correx/apps/tui/reducer/SessionsReducer.kt b/apps/tui/src/main/kotlin/com/correx/apps/tui/reducer/SessionsReducer.kt index 93d97bc0..3b1b1bf1 100644 --- a/apps/tui/src/main/kotlin/com/correx/apps/tui/reducer/SessionsReducer.kt +++ b/apps/tui/src/main/kotlin/com/correx/apps/tui/reducer/SessionsReducer.kt @@ -25,8 +25,18 @@ object SessionsReducer { action: Action, clock: () -> Long = System::currentTimeMillis, ): Pair> = when (action) { - is Action.NavigateUp -> if (inputMode == InputMode.NAVIGATE) navigateUp(sessions) to emptyList() else sessions to emptyList() - is Action.NavigateDown -> if (inputMode == InputMode.NAVIGATE) navigateDown(sessions) to emptyList() else sessions to emptyList() + is Action.NavigateUp -> if (inputMode == InputMode.NAVIGATE) { + navigateUp(sessions) to emptyList() + } else { + sessions to emptyList() + } + + is Action.NavigateDown -> if (inputMode == InputMode.NAVIGATE) { + navigateDown(sessions) to emptyList() + } else { + sessions to emptyList() + } + is Action.SubmitInput -> if (inputMode == InputMode.FILTER) { sessions.copy(filter = inputText) to emptyList() } else { @@ -52,8 +62,12 @@ object SessionsReducer { else -> sessions to emptyList() } + private fun filteredSessions(sessions: SessionsState): List = + if (sessions.filter.isBlank()) sessions.sessions + else sessions.sessions.filter { it.workflowId.contains(sessions.filter, ignoreCase = true) } + private fun navigateUp(sessions: SessionsState): SessionsState { - val list = sessions.sessions + val list = filteredSessions(sessions) if (list.isEmpty()) return sessions val idx = list.indexOfFirst { it.id == sessions.selectedId } val newIdx = if (idx <= 0) list.lastIndex else idx - 1 @@ -61,7 +75,7 @@ object SessionsReducer { } private fun navigateDown(sessions: SessionsState): SessionsState { - val list = sessions.sessions + val list = filteredSessions(sessions) if (list.isEmpty()) return sessions val idx = list.indexOfFirst { it.id == sessions.selectedId } val newIdx = if (idx >= list.lastIndex) 0 else idx + 1 @@ -77,194 +91,244 @@ object SessionsReducer { msg: ServerMessage, clock: () -> Long, ): Pair> = when (msg) { - is ServerMessage.SessionStarted -> { - val summary = SessionSummary( - id = msg.sessionId.value, - status = "ACTIVE", - workflowId = msg.workflowId, - name = msg.workflowId, - lastEventAt = clock(), - currentStage = null, - lastOutput = null, - ) - val selected = sessions.selectedId ?: msg.sessionId.value - sessions.copy(sessions = sessions.sessions + summary, selectedId = selected) to - listOf(Effect.ConnectSession(msg.sessionId)) - } - - is ServerMessage.SessionPaused -> { - val statusLabel = if (msg.reason == PauseReason.APPROVAL_PENDING) { - "PAUSED awaiting approval" - } else { - "PAUSED" - } - sessions.copy( - sessions = sessions.sessions.map { s -> - if (s.id == msg.sessionId.value) s.copy(status = statusLabel, lastEventAt = clock()) else s - }, - ) to emptyList() - } - - is ServerMessage.SessionCompleted -> touchSession(sessions, msg.sessionId.value, "COMPLETED", clock) to listOf( - Effect.DisconnectSession, - ) - - is ServerMessage.SessionFailed -> touchSession( - sessions, - msg.sessionId.value, - "FAILED", - clock, - ) to listOf(Effect.DisconnectSession) - - is ServerMessage.StageStarted -> { - val now = clock() - sessions.copy( - sessions = sessions.sessions.map { s -> - if (s.id == msg.sessionId.value) { - val entry = TuiEventEntry(formatTime(now), "StageStarted", msg.stageId.value) - s.copy( - currentStage = msg.stageId.value, - currentStageId = msg.stageId.value, - tools = emptyList(), - recentEvents = (s.recentEvents + entry).takeLast(7), - lastEventAt = now, - ) - } else { - s - } - }, - ) to emptyList() - } - - is ServerMessage.StageCompleted -> { - val now = clock() - sessions.copy( - sessions = sessions.sessions.map { s -> - if (s.id == msg.sessionId.value) { - val entry = TuiEventEntry(formatTime(now), "StageCompleted", msg.stageId.value) - s.copy( - currentStage = null, - recentEvents = (s.recentEvents + entry).takeLast(7), - lastEventAt = now, - ) - } else { - s - } - }, - ) to emptyList() - } - - is ServerMessage.StageFailed -> { - val now = clock() - sessions.copy( - sessions = sessions.sessions.map { s -> - if (s.id == msg.sessionId.value) { - val entry = TuiEventEntry(formatTime(now), "StageFailed", msg.stageId.value) - s.copy( - currentStage = null, - recentEvents = (s.recentEvents + entry).takeLast(7), - lastEventAt = now, - ) - } else { - s - } - }, - ) to emptyList() - } - - is ServerMessage.ToolStarted -> { - val now = clock() - sessions.copy( - sessions = sessions.sessions.map { s -> - if (s.id == msg.sessionId.value) { - val record = TuiToolRecord(msg.toolName, msg.tier.level, ToolDisplayStatus.STARTED, null) - s.copy( - tools = (s.tools + record).takeLast(8), - lastEventAt = now, - ) - } else { - s - } - }, - ) to emptyList() - } - - is ServerMessage.InferenceCompleted -> applyInferenceCompleted(sessions, msg, clock) - is ServerMessage.ToolCompleted -> { - val now = clock() - sessions.copy( - sessions = sessions.sessions.map { s -> - if (s.id == msg.sessionId.value) { - val updatedTools = s.tools.map { t -> - if (t.name == msg.toolName && t.status == ToolDisplayStatus.STARTED) { - t.copy(status = ToolDisplayStatus.COMPLETED) - } else { - t - } - } - val entry = TuiEventEntry(formatTime(now), "ToolCompleted", msg.toolName) - s.copy( - tools = updatedTools, - lastOutput = "${msg.toolName}: ${msg.outputSummary}", - recentEvents = (s.recentEvents + entry).takeLast(7), - lastEventAt = now, - ) - } else { - s - } - }, - ) to emptyList() - } - - is ServerMessage.ToolFailed -> { - val now = clock() - sessions.copy( - sessions = sessions.sessions.map { s -> - if (s.id == msg.sessionId.value) { - val updatedTools = s.tools.map { t -> - if (t.name == msg.toolName && t.status == ToolDisplayStatus.STARTED) { - t.copy(status = ToolDisplayStatus.FAILED) - } else { - t - } - } - s.copy(tools = updatedTools, lastEventAt = now) - } else { - s - } - }, - ) to emptyList() - } - - is ServerMessage.ToolRejected -> { - val now = clock() - sessions.copy( - sessions = sessions.sessions.map { s -> - if (s.id == msg.sessionId.value) { - val updatedTools = s.tools.map { t -> - if (t.name == msg.toolName && t.status == ToolDisplayStatus.STARTED) { - t.copy(status = ToolDisplayStatus.REJECTED) - } else { - t - } - } - s.copy(tools = updatedTools, lastEventAt = now) - } else { - s - } - }, - ) to emptyList() - } - + is ServerMessage.SessionStarted -> processSessionStartedMessage(msg, clock, sessions) + is ServerMessage.SessionPaused -> processSessionPausedMessage(msg, sessions, clock) + is ServerMessage.SessionCompleted -> processSessionCompletedMessage(sessions, msg, clock) + is ServerMessage.SessionFailed -> processSessionFailedMessage(sessions, msg, clock) + is ServerMessage.StageStarted -> processStageStartedMessage(msg, sessions) + is ServerMessage.StageCompleted -> processStageCompletedMessage(msg, sessions) + is ServerMessage.StageFailed -> processStageFailedMessage(msg, sessions) + is ServerMessage.ToolStarted -> processToolStartedMessage(clock, sessions, msg) + is ServerMessage.InferenceCompleted -> processInferenceCompletedMessage(sessions, msg) + is ServerMessage.ToolCompleted -> processToolCompletedMessage(msg, sessions) + is ServerMessage.ToolFailed -> processToolFailedMessage(msg, sessions) + is ServerMessage.ToolRejected -> processToolRejectedMessage(clock, sessions, msg) else -> sessions to emptyList() } - private fun applyInferenceCompleted( - sessions: SessionsState, - msg: ServerMessage.InferenceCompleted, + private fun processToolRejectedMessage( clock: () -> Long, + sessions: SessionsState, + msg: ServerMessage.ToolRejected, ): Pair> { val now = clock() + return sessions.copy( + sessions = sessions.sessions.map { s -> + if (s.id == msg.sessionId.value) { + val updatedTools = s.tools.map { t -> + if (t.name == msg.toolName && t.status == ToolDisplayStatus.STARTED) { + t.copy(status = ToolDisplayStatus.REJECTED) + } else { + t + } + } + s.copy(tools = updatedTools, lastEventAt = now) + } else { + s + } + }, + ) to emptyList() + } + + private fun processToolFailedMessage( + msg: ServerMessage.ToolFailed, + sessions: SessionsState, + ): Pair> { + val now = msg.occurredAt + return sessions.copy( + sessions = sessions.sessions.map { s -> + if (s.id == msg.sessionId.value) { + val updatedTools = s.tools.map { t -> + if (t.name == msg.toolName && t.status == ToolDisplayStatus.STARTED) { + t.copy(status = ToolDisplayStatus.FAILED) + } else { + t + } + } + s.copy(tools = updatedTools, lastEventAt = now) + } else { + s + } + }, + ) to emptyList() + } + + private fun processToolCompletedMessage( + msg: ServerMessage.ToolCompleted, + sessions: SessionsState, + ): Pair> { + val now = msg.occurredAt + return sessions.copy( + sessions = sessions.sessions.map { s -> + if (s.id == msg.sessionId.value) { + val updatedTools = s.tools.map { t -> + if (t.name == msg.toolName && t.status == ToolDisplayStatus.STARTED) { + t.copy(status = ToolDisplayStatus.COMPLETED) + } else { + t + } + } + val entry = TuiEventEntry(formatTime(now), "ToolCompleted", msg.toolName) + s.copy( + tools = updatedTools, + lastOutput = "${msg.toolName}: ${msg.outputSummary}", + recentEvents = (s.recentEvents + entry).takeLast(7), + lastEventAt = now, + ) + } else { + s + } + }, + ) to emptyList() + } + + private fun processToolStartedMessage( + clock: () -> Long, + sessions: SessionsState, + msg: ServerMessage.ToolStarted, + ): Pair> { + val now = clock() + return sessions.copy( + sessions = sessions.sessions.map { s -> + if (s.id == msg.sessionId.value) { + val record = TuiToolRecord(msg.toolName, msg.tier.level, ToolDisplayStatus.STARTED, null) + s.copy( + tools = (s.tools + record).takeLast(8), + lastEventAt = now, + ) + } else { + s + } + }, + ) to emptyList() + } + + private fun processStageFailedMessage( + msg: ServerMessage.StageFailed, + sessions: SessionsState, + ): Pair> { + val now = msg.occurredAt + return sessions.copy( + sessions = sessions.sessions.map { s -> + if (s.id == msg.sessionId.value) { + val entry = TuiEventEntry(formatTime(now), "StageFailed", msg.stageId.value) + s.copy( + currentStage = null, + recentEvents = (s.recentEvents + entry).takeLast(7), + lastEventAt = now, + ) + } else { + s + } + }, + ) to emptyList() + } + + private fun processStageCompletedMessage( + msg: ServerMessage.StageCompleted, + sessions: SessionsState, + ): Pair> { + val now = msg.occurredAt + return sessions.copy( + sessions = sessions.sessions.map { s -> + if (s.id == msg.sessionId.value) { + val entry = TuiEventEntry(formatTime(now), "StageCompleted", msg.stageId.value) + s.copy( + currentStage = null, + recentEvents = (s.recentEvents + entry).takeLast(7), + lastEventAt = now, + ) + } else { + s + } + }, + ) to emptyList() + } + + private fun processStageStartedMessage( + msg: ServerMessage.StageStarted, + sessions: SessionsState, + ): Pair> { + val now = msg.occurredAt + return sessions.copy( + sessions = sessions.sessions.map { s -> + if (s.id == msg.sessionId.value) { + val entry = TuiEventEntry(formatTime(now), "StageStarted", msg.stageId.value) + s.copy( + currentStage = msg.stageId.value, + currentStageId = msg.stageId.value, + tools = emptyList(), + recentEvents = (s.recentEvents + entry).takeLast(7), + lastEventAt = now, + ) + } else { + s + } + }, + ) to emptyList() + } + + private fun processSessionFailedMessage( + sessions: SessionsState, + msg: ServerMessage.SessionFailed, + clock: () -> Long, + ): Pair> = touchSession( + sessions, + msg.sessionId.value, + "FAILED", + clock, + ) to listOf(Effect.DisconnectSession) + + private fun processSessionCompletedMessage( + sessions: SessionsState, + msg: ServerMessage.SessionCompleted, + clock: () -> Long, + ): Pair> = + touchSession(sessions, msg.sessionId.value, "COMPLETED", clock) to listOf( + Effect.DisconnectSession, + ) + + private fun processSessionPausedMessage( + msg: ServerMessage.SessionPaused, + sessions: SessionsState, + clock: () -> Long, + ): Pair> { + val statusLabel = if (msg.reason == PauseReason.APPROVAL_PENDING) { + "PAUSED awaiting approval" + } else { + "PAUSED" + } + return sessions.copy( + sessions = sessions.sessions.map { s -> + if (s.id == msg.sessionId.value) s.copy(status = statusLabel, lastEventAt = clock()) else s + }, + ) to emptyList() + } + + private fun processSessionStartedMessage( + msg: ServerMessage.SessionStarted, + clock: () -> Long, + sessions: SessionsState, + ): Pair> { + val summary = SessionSummary( + id = msg.sessionId.value, + status = "ACTIVE", + workflowId = msg.workflowId, + name = msg.workflowId, + lastEventAt = clock(), + currentStage = null, + lastOutput = null, + ) + val selected = sessions.selectedId ?: msg.sessionId.value + return sessions.copy(sessions = sessions.sessions + summary, selectedId = selected) to + listOf(Effect.ConnectSession(msg.sessionId)) + } + + private fun processInferenceCompletedMessage( + sessions: SessionsState, + msg: ServerMessage.InferenceCompleted, + ): Pair> { + val now = msg.occurredAt return sessions.copy( sessions = sessions.sessions.map { s -> if (s.id == msg.sessionId.value) { diff --git a/apps/tui/src/test/kotlin/com/correx/apps/tui/input/TambouiKeyMapperTest.kt b/apps/tui/src/test/kotlin/com/correx/apps/tui/input/TambouiKeyMapperTest.kt index e844cd6a..68d4f834 100644 --- a/apps/tui/src/test/kotlin/com/correx/apps/tui/input/TambouiKeyMapperTest.kt +++ b/apps/tui/src/test/kotlin/com/correx/apps/tui/input/TambouiKeyMapperTest.kt @@ -1,13 +1,12 @@ package com.correx.apps.tui.input import com.correx.apps.tui.KeyEvent -import com.correx.apps.tui.state.InputMode import dev.tamboui.tui.event.KeyCode -import dev.tamboui.tui.event.KeyEvent as TambouiKeyEvent import dev.tamboui.tui.event.KeyModifiers import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertNull import org.junit.jupiter.api.Test +import dev.tamboui.tui.event.KeyEvent as TambouiKeyEvent class TambouiKeyMapperTest { @@ -15,156 +14,156 @@ class TambouiKeyMapperTest { @Test fun `ctrl-c maps to Quit in InputMode ROUTER`() { - assertEquals(KeyEvent.Quit, mapKey(TambouiKeyEvent(KeyCode.CHAR, KeyModifiers.CTRL, 'c'.code), InputMode.ROUTER)) + assertEquals(KeyEvent.Quit, mapKey(TambouiKeyEvent(KeyCode.CHAR, KeyModifiers.CTRL, 'c'.code))) } @Test fun `ctrl-c maps to Quit in InputMode FILTER`() { - assertEquals(KeyEvent.Quit, mapKey(TambouiKeyEvent(KeyCode.CHAR, KeyModifiers.CTRL, 'c'.code), InputMode.FILTER)) + assertEquals(KeyEvent.Quit, mapKey(TambouiKeyEvent(KeyCode.CHAR, KeyModifiers.CTRL, 'c'.code))) } @Test fun `ctrl-c maps to Quit in InputMode NAVIGATE`() { - assertEquals(KeyEvent.Quit, mapKey(TambouiKeyEvent(KeyCode.CHAR, KeyModifiers.CTRL, 'c'.code), InputMode.NAVIGATE)) + assertEquals(KeyEvent.Quit, mapKey(TambouiKeyEvent(KeyCode.CHAR, KeyModifiers.CTRL, 'c'.code))) } @Test fun `ctrl-c maps to Quit in InputMode STEER`() { - assertEquals(KeyEvent.Quit, mapKey(TambouiKeyEvent(KeyCode.CHAR, KeyModifiers.CTRL, 'c'.code), InputMode.STEER)) + assertEquals(KeyEvent.Quit, mapKey(TambouiKeyEvent(KeyCode.CHAR, KeyModifiers.CTRL, 'c'.code))) } // --- Ctrl+key → keybind events --- @Test fun `ctrl-q maps to Quit`() { - assertEquals(KeyEvent.Quit, mapKey(TambouiKeyEvent(KeyCode.CHAR, KeyModifiers.CTRL, 'q'.code), InputMode.ROUTER)) + assertEquals(KeyEvent.Quit, mapKey(TambouiKeyEvent(KeyCode.CHAR, KeyModifiers.CTRL, 'q'.code))) } @Test fun `ctrl-a maps to Approve`() { - assertEquals(KeyEvent.Approve, mapKey(TambouiKeyEvent(KeyCode.CHAR, KeyModifiers.CTRL, 'a'.code), InputMode.ROUTER)) + assertEquals(KeyEvent.Approve, mapKey(TambouiKeyEvent(KeyCode.CHAR, KeyModifiers.CTRL, 'a'.code))) } @Test fun `ctrl-r maps to Reject`() { - assertEquals(KeyEvent.Reject, mapKey(TambouiKeyEvent(KeyCode.CHAR, KeyModifiers.CTRL, 'r'.code), InputMode.ROUTER)) + assertEquals(KeyEvent.Reject, mapKey(TambouiKeyEvent(KeyCode.CHAR, KeyModifiers.CTRL, 'r'.code))) } @Test fun `ctrl-n maps to NewSession`() { - assertEquals(KeyEvent.NewSession, mapKey(TambouiKeyEvent(KeyCode.CHAR, KeyModifiers.CTRL, 'n'.code), InputMode.ROUTER)) + assertEquals(KeyEvent.NewSession, mapKey(TambouiKeyEvent(KeyCode.CHAR, KeyModifiers.CTRL, 'n'.code))) } @Test fun `ctrl-s maps to Steer`() { - assertEquals(KeyEvent.Steer, mapKey(TambouiKeyEvent(KeyCode.CHAR, KeyModifiers.CTRL, 's'.code), InputMode.ROUTER)) + assertEquals(KeyEvent.Steer, mapKey(TambouiKeyEvent(KeyCode.CHAR, KeyModifiers.CTRL, 's'.code))) } @Test fun `ctrl-h maps to ToggleApprovalOverlay`() { - assertEquals(KeyEvent.ToggleApprovalOverlay, mapKey(TambouiKeyEvent(KeyCode.CHAR, KeyModifiers.CTRL, 'h'.code), InputMode.ROUTER)) + assertEquals(KeyEvent.ToggleApprovalOverlay, mapKey(TambouiKeyEvent(KeyCode.CHAR, KeyModifiers.CTRL, 'h'.code))) } @Test fun `ctrl-e maps to ToggleEventOverlay`() { - assertEquals(KeyEvent.ToggleEventOverlay, mapKey(TambouiKeyEvent(KeyCode.CHAR, KeyModifiers.CTRL, 'e'.code), InputMode.ROUTER)) + assertEquals(KeyEvent.ToggleEventOverlay, mapKey(TambouiKeyEvent(KeyCode.CHAR, KeyModifiers.CTRL, 'e'.code))) } @Test fun `ctrl+unbound key maps to null`() { - assertNull(mapKey(TambouiKeyEvent(KeyCode.CHAR, KeyModifiers.CTRL, 'z'.code), InputMode.ROUTER)) + assertNull(mapKey(TambouiKeyEvent(KeyCode.CHAR, KeyModifiers.CTRL, 'z'.code))) } // --- Alt always → null --- @Test fun `alt modifier maps to null`() { - assertNull(mapKey(TambouiKeyEvent(KeyCode.CHAR, KeyModifiers.ALT, 'q'.code), InputMode.ROUTER)) + assertNull(mapKey(TambouiKeyEvent(KeyCode.CHAR, KeyModifiers.ALT, 'q'.code))) } // --- Bare chars always → CharInput (never trigger keybinds) --- @Test fun `bare a maps to CharInput in ROUTER`() { - assertEquals(KeyEvent.CharInput('a'), mapKey(TambouiKeyEvent.ofChar('a'), InputMode.ROUTER)) + assertEquals(KeyEvent.CharInput('a'), mapKey(TambouiKeyEvent.ofChar('a'))) } @Test fun `bare q maps to CharInput in ROUTER`() { - assertEquals(KeyEvent.CharInput('q'), mapKey(TambouiKeyEvent.ofChar('q'), InputMode.ROUTER)) + assertEquals(KeyEvent.CharInput('q'), mapKey(TambouiKeyEvent.ofChar('q'))) } @Test fun `bare s maps to CharInput in ROUTER`() { - assertEquals(KeyEvent.CharInput('s'), mapKey(TambouiKeyEvent.ofChar('s'), InputMode.ROUTER)) + assertEquals(KeyEvent.CharInput('s'), mapKey(TambouiKeyEvent.ofChar('s'))) } @Test fun `bare x maps to CharInput in ROUTER`() { - assertEquals(KeyEvent.CharInput('x'), mapKey(TambouiKeyEvent.ofChar('x'), InputMode.ROUTER)) + assertEquals(KeyEvent.CharInput('x'), mapKey(TambouiKeyEvent.ofChar('x'))) } @Test fun `bare q maps to CharInput in FILTER`() { - assertEquals(KeyEvent.CharInput('q'), mapKey(TambouiKeyEvent.ofChar('q'), InputMode.FILTER)) + assertEquals(KeyEvent.CharInput('q'), mapKey(TambouiKeyEvent.ofChar('q'))) } @Test fun `bare q maps to CharInput in STEER`() { - assertEquals(KeyEvent.CharInput('q'), mapKey(TambouiKeyEvent.ofChar('q'), InputMode.STEER)) + assertEquals(KeyEvent.CharInput('q'), mapKey(TambouiKeyEvent.ofChar('q'))) } @Test fun `bare q maps to CharInput in NAVIGATE`() { - assertEquals(KeyEvent.CharInput('q'), mapKey(TambouiKeyEvent.ofChar('q'), InputMode.NAVIGATE)) + assertEquals(KeyEvent.CharInput('q'), mapKey(TambouiKeyEvent.ofChar('q'))) } // --- KeyCode mappings --- @Test fun `UP maps to NavUp`() { - assertEquals(KeyEvent.NavUp, mapKey(TambouiKeyEvent.ofKey(KeyCode.UP), InputMode.ROUTER)) + assertEquals(KeyEvent.NavUp, mapKey(TambouiKeyEvent.ofKey(KeyCode.UP))) } @Test fun `DOWN maps to NavDown`() { - assertEquals(KeyEvent.NavDown, mapKey(TambouiKeyEvent.ofKey(KeyCode.DOWN), InputMode.ROUTER)) + assertEquals(KeyEvent.NavDown, mapKey(TambouiKeyEvent.ofKey(KeyCode.DOWN))) } @Test fun `ENTER maps to Enter`() { - assertEquals(KeyEvent.Enter, mapKey(TambouiKeyEvent.ofKey(KeyCode.ENTER), InputMode.ROUTER)) + assertEquals(KeyEvent.Enter, mapKey(TambouiKeyEvent.ofKey(KeyCode.ENTER))) } @Test fun `ESCAPE maps to Escape`() { - assertEquals(KeyEvent.Escape, mapKey(TambouiKeyEvent.ofKey(KeyCode.ESCAPE), InputMode.ROUTER)) + assertEquals(KeyEvent.Escape, mapKey(TambouiKeyEvent.ofKey(KeyCode.ESCAPE))) } @Test fun `BACKSPACE maps to Backspace`() { - assertEquals(KeyEvent.Backspace, mapKey(TambouiKeyEvent.ofKey(KeyCode.BACKSPACE), InputMode.ROUTER)) + assertEquals(KeyEvent.Backspace, mapKey(TambouiKeyEvent.ofKey(KeyCode.BACKSPACE))) } @Test fun `TAB maps to Filter`() { - assertEquals(KeyEvent.Filter, mapKey(TambouiKeyEvent.ofKey(KeyCode.TAB), InputMode.ROUTER)) + assertEquals(KeyEvent.Filter, mapKey(TambouiKeyEvent.ofKey(KeyCode.TAB))) } @Test fun `TAB maps to Filter in FILTER mode`() { - assertEquals(KeyEvent.Filter, mapKey(TambouiKeyEvent.ofKey(KeyCode.TAB), InputMode.FILTER)) + assertEquals(KeyEvent.Filter, mapKey(TambouiKeyEvent.ofKey(KeyCode.TAB))) } // --- ISO control chars → null --- @Test fun `ESC codepoint as CHAR event maps to null`() { - assertNull(mapKey(TambouiKeyEvent(KeyCode.CHAR, KeyModifiers.NONE, 0x1B), InputMode.ROUTER)) + assertNull(mapKey(TambouiKeyEvent(KeyCode.CHAR, KeyModifiers.NONE, 0x1B))) } @Test fun `NUL codepoint as CHAR event maps to null`() { - assertNull(mapKey(TambouiKeyEvent(KeyCode.CHAR, KeyModifiers.NONE, 0x00), InputMode.ROUTER)) + assertNull(mapKey(TambouiKeyEvent(KeyCode.CHAR, KeyModifiers.NONE, 0x00))) } } diff --git a/apps/tui/src/test/kotlin/com/correx/apps/tui/reducer/RootReducerTest.kt b/apps/tui/src/test/kotlin/com/correx/apps/tui/reducer/RootReducerTest.kt index 6f7ca2e0..33d12119 100644 --- a/apps/tui/src/test/kotlin/com/correx/apps/tui/reducer/RootReducerTest.kt +++ b/apps/tui/src/test/kotlin/com/correx/apps/tui/reducer/RootReducerTest.kt @@ -51,7 +51,11 @@ class RootReducerTest { ) val (state2, _) = RootReducer.reduce(state1, Action.ServerEventReceived(startMsg2), fixedClock) // selected is s1 (first); navigating down should move to s2 - val (state3, _) = RootReducer.reduce(state2.copy(inputMode = InputMode.NAVIGATE), Action.NavigateDown, fixedClock) + val (state3, _) = RootReducer.reduce( + state2.copy(inputMode = InputMode.NAVIGATE), + Action.NavigateDown, + fixedClock, + ) assertEquals("s2", state3.sessions.selectedId) } diff --git a/apps/tui/src/test/kotlin/com/correx/apps/tui/reducer/SessionsReducerTest.kt b/apps/tui/src/test/kotlin/com/correx/apps/tui/reducer/SessionsReducerTest.kt index a80654da..0d6af391 100644 --- a/apps/tui/src/test/kotlin/com/correx/apps/tui/reducer/SessionsReducerTest.kt +++ b/apps/tui/src/test/kotlin/com/correx/apps/tui/reducer/SessionsReducerTest.kt @@ -132,7 +132,11 @@ class SessionsReducerTest { sessions = listOf(session("s1").copy(currentStage = "stage-1")), selectedId = "s1", ) - val msg = ServerMessage.StageCompleted(sessionId = SessionId("s1"), stageId = StageId("stage-1")) + val msg = ServerMessage.StageCompleted( + sessionId = SessionId("s1"), + stageId = StageId("stage-1"), + occurredAt = fixedClock(), + ) val (state, _) = reduce(sessions = s, action = Action.ServerEventReceived(msg)) assertEquals(null, state.sessions[0].currentStage) assertEquals(1000L, state.sessions[0].lastEventAt) @@ -148,6 +152,7 @@ class SessionsReducerTest { sessionId = SessionId("s1"), stageId = StageId("stage-1"), reason = "error", + occurredAt = fixedClock(), ) val (state, _) = reduce(sessions = s, action = Action.ServerEventReceived(msg)) assertEquals(null, state.sessions[0].currentStage) @@ -177,6 +182,7 @@ class SessionsReducerTest { val msg = ServerMessage.InferenceCompleted( sessionId = SessionId("s1"), stageId = StageId("stage-1"), outputSummary = "summary", responseText = "full response", + occurredAt = fixedClock(), ) val (state, _) = reduce(sessions = s, action = Action.ServerEventReceived(msg)) assertEquals("summary", state.sessions[0].lastOutput) @@ -190,6 +196,7 @@ class SessionsReducerTest { val msg = ServerMessage.InferenceCompleted( sessionId = SessionId("s1"), stageId = StageId("stage-1"), outputSummary = "", responseText = "", + occurredAt = fixedClock(), ) val (state, _) = reduce(sessions = s, action = Action.ServerEventReceived(msg)) assertEquals(null, state.sessions[0].lastResponseText) @@ -198,7 +205,11 @@ class SessionsReducerTest { @Test fun `ServerEventReceived StageStarted sets currentStage`() { val s = SessionsState(sessions = listOf(session("s1")), selectedId = "s1") - val msg = ServerMessage.StageStarted(sessionId = SessionId("s1"), stageId = StageId("stage-1")) + val msg = ServerMessage.StageStarted( + sessionId = SessionId("s1"), + stageId = StageId("stage-1"), + occurredAt = fixedClock(), + ) val (state, _) = reduce(sessions = s, action = Action.ServerEventReceived(msg)) assertEquals("stage-1", state.sessions[0].currentStage) assertEquals(1000L, state.sessions[0].lastEventAt) @@ -207,7 +218,12 @@ class SessionsReducerTest { @Test fun `ServerEventReceived ToolCompleted updates lastOutput`() { val s = SessionsState(sessions = listOf(session("s1")), selectedId = "s1") - val msg = ServerMessage.ToolCompleted(sessionId = SessionId("s1"), toolName = "file_write", outputSummary = "wrote 3 lines") + val msg = ServerMessage.ToolCompleted( + sessionId = SessionId("s1"), + toolName = "file_write", + outputSummary = "wrote 3 lines", + occurredAt = fixedClock(), + ) val (state, _) = reduce(sessions = s, action = Action.ServerEventReceived(msg)) assertEquals("file_write: wrote 3 lines", state.sessions[0].lastOutput) assertEquals(1000L, state.sessions[0].lastEventAt) diff --git a/core/events/src/main/kotlin/com/correx/core/events/events/EventEnvelope.kt b/core/events/src/main/kotlin/com/correx/core/events/events/EventEnvelope.kt index a59da6fb..f5c3dae8 100644 --- a/core/events/src/main/kotlin/com/correx/core/events/events/EventEnvelope.kt +++ b/core/events/src/main/kotlin/com/correx/core/events/events/EventEnvelope.kt @@ -13,4 +13,4 @@ data class StoredEvent( val metadata: EventMetadata, val sequence: Long, val payload: EventPayload -) \ No newline at end of file +) diff --git a/core/events/src/main/kotlin/com/correx/core/events/events/EventMetadata.kt b/core/events/src/main/kotlin/com/correx/core/events/events/EventMetadata.kt index 846f3b0c..035ffddc 100644 --- a/core/events/src/main/kotlin/com/correx/core/events/events/EventMetadata.kt +++ b/core/events/src/main/kotlin/com/correx/core/events/events/EventMetadata.kt @@ -15,4 +15,4 @@ data class EventMetadata( val schemaVersion: Int, val causationId: CausationId?, val correlationId: CorrelationId? -) \ No newline at end of file +) diff --git a/core/events/src/main/kotlin/com/correx/core/events/events/EventPayload.kt b/core/events/src/main/kotlin/com/correx/core/events/events/EventPayload.kt index d5679042..16774bf5 100644 --- a/core/events/src/main/kotlin/com/correx/core/events/events/EventPayload.kt +++ b/core/events/src/main/kotlin/com/correx/core/events/events/EventPayload.kt @@ -3,4 +3,4 @@ package com.correx.core.events.events import kotlinx.serialization.Serializable @Serializable -sealed interface EventPayload \ No newline at end of file +sealed interface EventPayload diff --git a/core/events/src/main/kotlin/com/correx/core/events/events/InferenceEvents.kt b/core/events/src/main/kotlin/com/correx/core/events/events/InferenceEvents.kt index 3638663e..262e9830 100644 --- a/core/events/src/main/kotlin/com/correx/core/events/events/InferenceEvents.kt +++ b/core/events/src/main/kotlin/com/correx/core/events/events/InferenceEvents.kt @@ -60,4 +60,4 @@ data class ModelUnloadedEvent( val providerId: ProviderId, val sessionId: SessionId, val cancellationReason: String? = null, // serialized label, not the sealed class -) : EventPayload \ No newline at end of file +) : EventPayload diff --git a/core/events/src/main/kotlin/com/correx/core/events/events/OrchestrationEvents.kt b/core/events/src/main/kotlin/com/correx/core/events/events/OrchestrationEvents.kt index f26d9366..46a2bbf2 100644 --- a/core/events/src/main/kotlin/com/correx/core/events/events/OrchestrationEvents.kt +++ b/core/events/src/main/kotlin/com/correx/core/events/events/OrchestrationEvents.kt @@ -8,6 +8,7 @@ import kotlinx.serialization.Serializable @Serializable data class WorkflowStartedEvent( val sessionId: SessionId, + val workflowId: String, val startStageId: StageId, val retryPolicy: RetryPolicy? = null, ) : EventPayload @@ -17,6 +18,7 @@ data class WorkflowCompletedEvent( val sessionId: SessionId, val terminalStageId: StageId, val totalStages: Int, + val workflowId: String, ) : EventPayload @Serializable @@ -47,4 +49,4 @@ data class RetryAttemptedEvent( val attemptNumber: Int, val maxAttempts: Int, val failureReason: String, -) : EventPayload \ No newline at end of file +) : EventPayload diff --git a/core/events/src/main/kotlin/com/correx/core/events/serialization/EventSerializer.kt b/core/events/src/main/kotlin/com/correx/core/events/serialization/EventSerializer.kt index 9178b0ba..be224bac 100644 --- a/core/events/src/main/kotlin/com/correx/core/events/serialization/EventSerializer.kt +++ b/core/events/src/main/kotlin/com/correx/core/events/serialization/EventSerializer.kt @@ -5,4 +5,4 @@ import com.correx.core.events.events.EventPayload interface EventSerializer { fun serialize(payload: EventPayload): String fun deserialize(raw: String): EventPayload -} \ No newline at end of file +} diff --git a/core/events/src/main/kotlin/com/correx/core/events/serialization/JsonEventSerializer.kt b/core/events/src/main/kotlin/com/correx/core/events/serialization/JsonEventSerializer.kt index a66e6058..d6116029 100644 --- a/core/events/src/main/kotlin/com/correx/core/events/serialization/JsonEventSerializer.kt +++ b/core/events/src/main/kotlin/com/correx/core/events/serialization/JsonEventSerializer.kt @@ -12,4 +12,4 @@ class JsonEventSerializer( override fun deserialize(raw: String): EventPayload = json.decodeFromString(EventPayload.serializer(), raw) -} \ No newline at end of file +} diff --git a/core/events/src/main/kotlin/com/correx/core/events/serialization/Serialization.kt b/core/events/src/main/kotlin/com/correx/core/events/serialization/Serialization.kt index cbee2a07..65549ce8 100644 --- a/core/events/src/main/kotlin/com/correx/core/events/serialization/Serialization.kt +++ b/core/events/src/main/kotlin/com/correx/core/events/serialization/Serialization.kt @@ -33,10 +33,10 @@ 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.SteeringNoteAddedEvent import com.correx.core.events.events.StageCompletedEvent import com.correx.core.events.events.StageFailedEvent import com.correx.core.events.events.StageStartedEvent +import com.correx.core.events.events.SteeringNoteAddedEvent import com.correx.core.events.events.ToolExecutionCompletedEvent import com.correx.core.events.events.ToolExecutionFailedEvent import com.correx.core.events.events.ToolExecutionRejectedEvent @@ -106,4 +106,4 @@ val eventModule = SerializersModule { val eventJson = Json { serializersModule = eventModule -} \ No newline at end of file +} diff --git a/core/events/src/main/kotlin/com/correx/core/events/stores/EventStore.kt b/core/events/src/main/kotlin/com/correx/core/events/stores/EventStore.kt index 0c690f18..c20bef02 100644 --- a/core/events/src/main/kotlin/com/correx/core/events/stores/EventStore.kt +++ b/core/events/src/main/kotlin/com/correx/core/events/stores/EventStore.kt @@ -47,4 +47,4 @@ interface EventStore { * Intended for batch jobs (e.g. CAS liveness scans). Implementations should stream lazily. */ fun allEvents(): Sequence -} \ No newline at end of file +} diff --git a/core/events/src/main/kotlin/com/correx/core/events/types/IdentityTypes.kt b/core/events/src/main/kotlin/com/correx/core/events/types/IdentityTypes.kt index 585fc0a8..5919814a 100644 --- a/core/events/src/main/kotlin/com/correx/core/events/types/IdentityTypes.kt +++ b/core/events/src/main/kotlin/com/correx/core/events/types/IdentityTypes.kt @@ -39,4 +39,4 @@ typealias ToolInvocationId = TypeId // Inference typealias InferenceRequestId = TypeId -typealias ProviderId = TypeId \ No newline at end of file +typealias ProviderId = TypeId diff --git a/core/events/src/main/kotlin/com/correx/core/inference/TokenUsage.kt b/core/events/src/main/kotlin/com/correx/core/inference/TokenUsage.kt index 89a23f54..4ab328fe 100644 --- a/core/events/src/main/kotlin/com/correx/core/inference/TokenUsage.kt +++ b/core/events/src/main/kotlin/com/correx/core/inference/TokenUsage.kt @@ -8,4 +8,4 @@ data class TokenUsage( val completionTokens: Int, ) { val totalTokens: Int get() = promptTokens + completionTokens -} \ No newline at end of file +} diff --git a/core/events/src/main/kotlin/com/correx/core/sessions/projections/DefaultStateBuilder.kt b/core/events/src/main/kotlin/com/correx/core/sessions/projections/DefaultStateBuilder.kt index 18e1a2ad..71374ee6 100644 --- a/core/events/src/main/kotlin/com/correx/core/sessions/projections/DefaultStateBuilder.kt +++ b/core/events/src/main/kotlin/com/correx/core/sessions/projections/DefaultStateBuilder.kt @@ -11,4 +11,4 @@ class DefaultStateBuilder( projection.apply(state, event) } } -} \ No newline at end of file +} diff --git a/core/events/src/main/kotlin/com/correx/core/sessions/projections/Projection.kt b/core/events/src/main/kotlin/com/correx/core/sessions/projections/Projection.kt index d7a13610..705ee0d7 100644 --- a/core/events/src/main/kotlin/com/correx/core/sessions/projections/Projection.kt +++ b/core/events/src/main/kotlin/com/correx/core/sessions/projections/Projection.kt @@ -7,4 +7,4 @@ interface Projection { fun initial(): S fun apply(state: S, event: StoredEvent): S -} \ No newline at end of file +} diff --git a/core/events/src/main/kotlin/com/correx/core/sessions/projections/StateBuilder.kt b/core/events/src/main/kotlin/com/correx/core/sessions/projections/StateBuilder.kt index 3e006a93..4fe9c227 100644 --- a/core/events/src/main/kotlin/com/correx/core/sessions/projections/StateBuilder.kt +++ b/core/events/src/main/kotlin/com/correx/core/sessions/projections/StateBuilder.kt @@ -4,4 +4,4 @@ import com.correx.core.events.events.StoredEvent interface StateBuilder { fun build(events: List): S -} \ No newline at end of file +} diff --git a/core/events/src/main/kotlin/com/correx/core/sessions/projections/replay/DefaultEventReplayer.kt b/core/events/src/main/kotlin/com/correx/core/sessions/projections/replay/DefaultEventReplayer.kt index 5b8cbd70..b352a896 100644 --- a/core/events/src/main/kotlin/com/correx/core/sessions/projections/replay/DefaultEventReplayer.kt +++ b/core/events/src/main/kotlin/com/correx/core/sessions/projections/replay/DefaultEventReplayer.kt @@ -16,4 +16,4 @@ class DefaultEventReplayer( return DefaultStateBuilder(projection) .build(events) } -} \ No newline at end of file +} diff --git a/core/events/src/main/kotlin/com/correx/core/sessions/projections/replay/EventReplayer.kt b/core/events/src/main/kotlin/com/correx/core/sessions/projections/replay/EventReplayer.kt index 579b407a..d1a5459c 100644 --- a/core/events/src/main/kotlin/com/correx/core/sessions/projections/replay/EventReplayer.kt +++ b/core/events/src/main/kotlin/com/correx/core/sessions/projections/replay/EventReplayer.kt @@ -4,4 +4,4 @@ import com.correx.core.events.types.SessionId interface EventReplayer { fun rebuild(sessionId: SessionId): S -} \ No newline at end of file +} diff --git a/core/events/src/main/kotlin/com/correx/core/utils/TypeId.kt b/core/events/src/main/kotlin/com/correx/core/utils/TypeId.kt index 64f1a6d9..d4eb9dd5 100644 --- a/core/events/src/main/kotlin/com/correx/core/utils/TypeId.kt +++ b/core/events/src/main/kotlin/com/correx/core/utils/TypeId.kt @@ -11,4 +11,4 @@ value class TypeId(val value: String) { } override fun toString(): String = value -} \ No newline at end of file +} diff --git a/core/inference/src/main/kotlin/com/correx/core/inference/DefaultInferenceReducer.kt b/core/inference/src/main/kotlin/com/correx/core/inference/DefaultInferenceReducer.kt index c236fec2..bbdbac5f 100644 --- a/core/inference/src/main/kotlin/com/correx/core/inference/DefaultInferenceReducer.kt +++ b/core/inference/src/main/kotlin/com/correx/core/inference/DefaultInferenceReducer.kt @@ -44,4 +44,4 @@ class DefaultInferenceReducer : InferenceReducer { return state.copy(records = updated) } -} \ No newline at end of file +} diff --git a/core/inference/src/main/kotlin/com/correx/core/inference/FinishReason.kt b/core/inference/src/main/kotlin/com/correx/core/inference/FinishReason.kt index 3d54dd69..360ce7e4 100644 --- a/core/inference/src/main/kotlin/com/correx/core/inference/FinishReason.kt +++ b/core/inference/src/main/kotlin/com/correx/core/inference/FinishReason.kt @@ -16,4 +16,4 @@ sealed class FinishReason { object Cancelled : FinishReason() @Serializable data class Error(val message: String) : FinishReason() -} \ No newline at end of file +} diff --git a/core/inference/src/main/kotlin/com/correx/core/inference/GenerationConfig.kt b/core/inference/src/main/kotlin/com/correx/core/inference/GenerationConfig.kt index 670b6f40..f862be6a 100644 --- a/core/inference/src/main/kotlin/com/correx/core/inference/GenerationConfig.kt +++ b/core/inference/src/main/kotlin/com/correx/core/inference/GenerationConfig.kt @@ -13,4 +13,4 @@ data class GenerationConfig( val maxTokens: Int, val stopSequences: List = emptyList(), val seed: Long? = null, // null = non-deterministic; set for replay -) \ No newline at end of file +) diff --git a/core/inference/src/main/kotlin/com/correx/core/inference/InferenceCancellationToken.kt b/core/inference/src/main/kotlin/com/correx/core/inference/InferenceCancellationToken.kt index 35b159b3..9b721c20 100644 --- a/core/inference/src/main/kotlin/com/correx/core/inference/InferenceCancellationToken.kt +++ b/core/inference/src/main/kotlin/com/correx/core/inference/InferenceCancellationToken.kt @@ -28,4 +28,4 @@ package com.correx.core.inference interface InferenceCancellationToken { val isCancelled: Boolean fun cancel(reason: CancellationReason) -} \ No newline at end of file +} diff --git a/core/inference/src/main/kotlin/com/correx/core/inference/InferenceProjector.kt b/core/inference/src/main/kotlin/com/correx/core/inference/InferenceProjector.kt index 81713e2d..b0391c05 100644 --- a/core/inference/src/main/kotlin/com/correx/core/inference/InferenceProjector.kt +++ b/core/inference/src/main/kotlin/com/correx/core/inference/InferenceProjector.kt @@ -12,4 +12,4 @@ class InferenceProjector( state: InferenceState, event: StoredEvent ): InferenceState = reducer.reduce(state, event) -} \ No newline at end of file +} diff --git a/core/inference/src/main/kotlin/com/correx/core/inference/InferenceProvider.kt b/core/inference/src/main/kotlin/com/correx/core/inference/InferenceProvider.kt index a99ab234..bf0beda2 100644 --- a/core/inference/src/main/kotlin/com/correx/core/inference/InferenceProvider.kt +++ b/core/inference/src/main/kotlin/com/correx/core/inference/InferenceProvider.kt @@ -17,4 +17,4 @@ interface ProviderRegistry { fun resolve(capability: ModelCapability): List // ordered by score desc fun listAll(): List suspend fun healthCheckAll(): Map -} \ No newline at end of file +} diff --git a/core/inference/src/main/kotlin/com/correx/core/inference/InferenceRecord.kt b/core/inference/src/main/kotlin/com/correx/core/inference/InferenceRecord.kt index c9268a08..f3a7f6cb 100644 --- a/core/inference/src/main/kotlin/com/correx/core/inference/InferenceRecord.kt +++ b/core/inference/src/main/kotlin/com/correx/core/inference/InferenceRecord.kt @@ -16,4 +16,4 @@ data class InferenceRecord( val tokensUsed: TokenUsage? = null, val latencyMs: Long? = null, val failureReason: String? = null, -) \ No newline at end of file +) diff --git a/core/inference/src/main/kotlin/com/correx/core/inference/InferenceReducer.kt b/core/inference/src/main/kotlin/com/correx/core/inference/InferenceReducer.kt index ddff3a79..8a187b15 100644 --- a/core/inference/src/main/kotlin/com/correx/core/inference/InferenceReducer.kt +++ b/core/inference/src/main/kotlin/com/correx/core/inference/InferenceReducer.kt @@ -4,4 +4,4 @@ import com.correx.core.events.events.StoredEvent interface InferenceReducer { fun reduce(state: InferenceState, event: StoredEvent): InferenceState -} \ No newline at end of file +} diff --git a/core/inference/src/main/kotlin/com/correx/core/inference/InferenceRepository.kt b/core/inference/src/main/kotlin/com/correx/core/inference/InferenceRepository.kt index 65068c18..a43cb3d4 100644 --- a/core/inference/src/main/kotlin/com/correx/core/inference/InferenceRepository.kt +++ b/core/inference/src/main/kotlin/com/correx/core/inference/InferenceRepository.kt @@ -9,4 +9,4 @@ class InferenceRepository( fun getInferenceState(sessionId: SessionId): InferenceState { return replayer.rebuild(sessionId) } -} \ No newline at end of file +} diff --git a/core/inference/src/main/kotlin/com/correx/core/inference/InferenceRequest.kt b/core/inference/src/main/kotlin/com/correx/core/inference/InferenceRequest.kt index 995282ad..3b86fecb 100644 --- a/core/inference/src/main/kotlin/com/correx/core/inference/InferenceRequest.kt +++ b/core/inference/src/main/kotlin/com/correx/core/inference/InferenceRequest.kt @@ -16,4 +16,4 @@ data class InferenceRequest( val timeout: InferenceTimeout? = null, val responseFormat: ResponseFormat = ResponseFormat.Text, val tools: List = emptyList(), -) \ No newline at end of file +) diff --git a/core/inference/src/main/kotlin/com/correx/core/inference/InferenceResponse.kt b/core/inference/src/main/kotlin/com/correx/core/inference/InferenceResponse.kt index 1a43fd80..db731ba6 100644 --- a/core/inference/src/main/kotlin/com/correx/core/inference/InferenceResponse.kt +++ b/core/inference/src/main/kotlin/com/correx/core/inference/InferenceResponse.kt @@ -11,4 +11,4 @@ data class InferenceResponse( val tokensUsed: TokenUsage, val latencyMs: Long, val toolCalls: List = emptyList(), -) \ No newline at end of file +) diff --git a/core/inference/src/main/kotlin/com/correx/core/inference/InferenceRouter.kt b/core/inference/src/main/kotlin/com/correx/core/inference/InferenceRouter.kt index f7e96ea8..323971c3 100644 --- a/core/inference/src/main/kotlin/com/correx/core/inference/InferenceRouter.kt +++ b/core/inference/src/main/kotlin/com/correx/core/inference/InferenceRouter.kt @@ -41,4 +41,4 @@ class ProviderUnavailableException( reason: String, ) : Exception( "Provider '${providerId.value}' is unavailable: $reason", -) \ No newline at end of file +) diff --git a/core/inference/src/main/kotlin/com/correx/core/inference/InferenceState.kt b/core/inference/src/main/kotlin/com/correx/core/inference/InferenceState.kt index 171c040a..18ba89df 100644 --- a/core/inference/src/main/kotlin/com/correx/core/inference/InferenceState.kt +++ b/core/inference/src/main/kotlin/com/correx/core/inference/InferenceState.kt @@ -5,4 +5,4 @@ import kotlinx.serialization.Serializable @Serializable data class InferenceState( val records: List = emptyList() -) \ No newline at end of file +) diff --git a/core/inference/src/main/kotlin/com/correx/core/inference/InferenceStatus.kt b/core/inference/src/main/kotlin/com/correx/core/inference/InferenceStatus.kt index d7b233cc..23958de6 100644 --- a/core/inference/src/main/kotlin/com/correx/core/inference/InferenceStatus.kt +++ b/core/inference/src/main/kotlin/com/correx/core/inference/InferenceStatus.kt @@ -2,4 +2,4 @@ package com.correx.core.inference enum class InferenceStatus { STARTED, COMPLETED, FAILED, TIMED_OUT -} \ No newline at end of file +} diff --git a/core/inference/src/main/kotlin/com/correx/core/inference/ProviderHealth.kt b/core/inference/src/main/kotlin/com/correx/core/inference/ProviderHealth.kt index e06dc569..d3a1c674 100644 --- a/core/inference/src/main/kotlin/com/correx/core/inference/ProviderHealth.kt +++ b/core/inference/src/main/kotlin/com/correx/core/inference/ProviderHealth.kt @@ -4,4 +4,4 @@ sealed class ProviderHealth { object Healthy : ProviderHealth() data class Degraded(val reason: String) : ProviderHealth() data class Unavailable(val reason: String) : ProviderHealth() -} \ No newline at end of file +} diff --git a/core/inference/src/main/kotlin/com/correx/core/inference/Tokenizer.kt b/core/inference/src/main/kotlin/com/correx/core/inference/Tokenizer.kt index ed3f8290..ff2151a1 100644 --- a/core/inference/src/main/kotlin/com/correx/core/inference/Tokenizer.kt +++ b/core/inference/src/main/kotlin/com/correx/core/inference/Tokenizer.kt @@ -10,4 +10,4 @@ value class Token(val id: Int) interface Tokenizer { suspend fun tokenize(text: String): List suspend fun countTokens(text: String): Int -} \ No newline at end of file +} diff --git a/core/inference/src/main/kotlin/com/correx/core/inference/ToolCallRequest.kt b/core/inference/src/main/kotlin/com/correx/core/inference/ToolCallRequest.kt index a5dfe2c0..35ad63fe 100644 --- a/core/inference/src/main/kotlin/com/correx/core/inference/ToolCallRequest.kt +++ b/core/inference/src/main/kotlin/com/correx/core/inference/ToolCallRequest.kt @@ -13,4 +13,4 @@ data class ToolCallRequest( data class ToolCallFunction( val name: String, val arguments: String, -) \ No newline at end of file +) diff --git a/core/kernel/src/main/kotlin/com/correx/core/kernel/execution/ReplayStrategy.kt b/core/kernel/src/main/kotlin/com/correx/core/kernel/execution/ReplayStrategy.kt index 3ac77441..8bbcd8a4 100644 --- a/core/kernel/src/main/kotlin/com/correx/core/kernel/execution/ReplayStrategy.kt +++ b/core/kernel/src/main/kotlin/com/correx/core/kernel/execution/ReplayStrategy.kt @@ -4,4 +4,4 @@ sealed interface ReplayStrategy { data object Full : ReplayStrategy data object SkipInference : ReplayStrategy // use recorded InferenceCompletedEvent artifacts data object SkipValidation : ReplayStrategy // trust recorded outcomes -} \ No newline at end of file +} diff --git a/core/kernel/src/main/kotlin/com/correx/core/kernel/execution/StageOutcome.kt b/core/kernel/src/main/kotlin/com/correx/core/kernel/execution/StageOutcome.kt index 6205bd6e..08bc1b21 100644 --- a/core/kernel/src/main/kotlin/com/correx/core/kernel/execution/StageOutcome.kt +++ b/core/kernel/src/main/kotlin/com/correx/core/kernel/execution/StageOutcome.kt @@ -15,4 +15,4 @@ sealed interface StageOutcome { data class InferenceFailure(val reason: String, val retryable: Boolean) : StageOutcome data class ApprovalRequired(val request: DomainApprovalRequest) : StageOutcome data object Cancelled : StageOutcome -} \ No newline at end of file +} diff --git a/core/kernel/src/main/kotlin/com/correx/core/kernel/execution/WorkflowResult.kt b/core/kernel/src/main/kotlin/com/correx/core/kernel/execution/WorkflowResult.kt index ce001534..d97a04b3 100644 --- a/core/kernel/src/main/kotlin/com/correx/core/kernel/execution/WorkflowResult.kt +++ b/core/kernel/src/main/kotlin/com/correx/core/kernel/execution/WorkflowResult.kt @@ -7,4 +7,4 @@ sealed interface WorkflowResult { data class Completed(val sessionId: SessionId, val terminalStageId: StageId) : WorkflowResult data class Failed(val sessionId: SessionId, val reason: String, val retryExhausted: Boolean) : WorkflowResult data class Cancelled(val sessionId: SessionId) : WorkflowResult -} \ No newline at end of file +} diff --git a/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/DefaultOrchestrationReducer.kt b/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/DefaultOrchestrationReducer.kt index 11e32f5e..dbd9339b 100644 --- a/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/DefaultOrchestrationReducer.kt +++ b/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/DefaultOrchestrationReducer.kt @@ -46,4 +46,4 @@ class DefaultOrchestrationReducer : OrchestrationReducer { else -> state } -} \ No newline at end of file +} diff --git a/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/DefaultSessionOrchestrator.kt b/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/DefaultSessionOrchestrator.kt index b0cb982b..11314037 100644 --- a/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/DefaultSessionOrchestrator.kt +++ b/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/DefaultSessionOrchestrator.kt @@ -1,14 +1,13 @@ package com.correx.core.kernel.orchestration import com.correx.core.approvals.model.ApprovalDecision -import com.correx.core.artifactstore.ArtifactStore import com.correx.core.artifacts.ArtifactState +import com.correx.core.artifactstore.ArtifactStore import com.correx.core.events.events.ArtifactValidatedEvent -import com.correx.core.events.types.ArtifactId -import com.correx.core.events.types.ArtifactLifecyclePhase -import org.slf4j.LoggerFactory import com.correx.core.events.orchestration.OrchestrationState import com.correx.core.events.types.ApprovalRequestId +import com.correx.core.events.types.ArtifactId +import com.correx.core.events.types.ArtifactLifecyclePhase import com.correx.core.events.types.SessionId import com.correx.core.events.types.StageId import com.correx.core.kernel.execution.WorkflowResult @@ -17,6 +16,7 @@ import com.correx.core.sessions.Session import com.correx.core.transitions.execution.StageExecutionResult import com.correx.core.transitions.graph.WorkflowGraph import com.correx.core.transitions.resolution.TransitionDecision +import org.slf4j.LoggerFactory import java.util.concurrent.* import java.util.concurrent.atomic.* @@ -94,7 +94,7 @@ class DefaultSessionOrchestrator( } is TransitionDecision.Stay -> if (isTerminal(enriched.graph, enriched.currentStageId)) { - completeWorkflow(enriched.sessionId, enriched.currentStageId, enriched.stageCount) + completeWorkflow(enriched.sessionId, enriched.currentStageId, enriched.stageCount, enriched.graph.id) } else { failWorkflow( sessionId = enriched.sessionId, @@ -140,7 +140,7 @@ class DefaultSessionOrchestrator( if (isTerminal(ctx.graph, nextStageId)) { // Terminal stage is a sentinel — not executed, so don't count it. return StepResult.Terminal( - completeWorkflow(ctx.sessionId, nextStageId, ctx.stageCount), + completeWorkflow(ctx.sessionId, nextStageId, ctx.stageCount, ctx.graph.id), ) } @@ -150,6 +150,7 @@ class DefaultSessionOrchestrator( currentStageId = advanceStage(ctx.sessionId, ctx.currentStageId, decision), ), ) + is StepResult.Terminal -> result } } @@ -171,7 +172,7 @@ class DefaultSessionOrchestrator( is StageExecutionResult.Failure -> { log.debug( "[Orchestrator] stage failed session=${ctx.sessionId.value} " + - "stage=${stageId.value} reason=${result.reason} retryable=${result.retryable}", + "stage=${stageId.value} reason=${result.reason} retryable=${result.retryable}", ) val refreshedState = orchestrationRepository.getState(ctx.sessionId) val shouldRetry = retryCoordinator.shouldRetry( diff --git a/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/OrchestrationConfig.kt b/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/OrchestrationConfig.kt index 2750e26e..0fe83203 100644 --- a/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/OrchestrationConfig.kt +++ b/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/OrchestrationConfig.kt @@ -10,4 +10,4 @@ data class OrchestrationConfig( val stageTimeoutMs: Long = 60_000L, val sandboxRoot: Path = Path.of(System.getProperty("java.io.tmpdir"), "correx-sandbox"), val defaultSystemPromptPath: String? = null, -) \ No newline at end of file +) diff --git a/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/OrchestrationProjector.kt b/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/OrchestrationProjector.kt index f4ae1fc8..fae7200e 100644 --- a/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/OrchestrationProjector.kt +++ b/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/OrchestrationProjector.kt @@ -13,4 +13,4 @@ class OrchestrationProjector( state: OrchestrationState, event: StoredEvent, ): OrchestrationState = orchestrationReducer.reduce(state, event) -} \ No newline at end of file +} diff --git a/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/OrchestrationReducer.kt b/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/OrchestrationReducer.kt index a732434a..9ac6d611 100644 --- a/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/OrchestrationReducer.kt +++ b/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/OrchestrationReducer.kt @@ -5,4 +5,4 @@ import com.correx.core.events.orchestration.OrchestrationState interface OrchestrationReducer { fun reduce(state: OrchestrationState, event: StoredEvent): OrchestrationState -} \ No newline at end of file +} diff --git a/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/OrchestrationRepository.kt b/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/OrchestrationRepository.kt index ca168ab3..fa8b1c42 100644 --- a/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/OrchestrationRepository.kt +++ b/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/OrchestrationRepository.kt @@ -8,4 +8,4 @@ class OrchestrationRepository( private val replayer: EventReplayer, ) { fun getState(sessionId: SessionId): OrchestrationState = replayer.rebuild(sessionId) -} \ No newline at end of file +} diff --git a/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/ReplayOrchestrator.kt b/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/ReplayOrchestrator.kt index f52fa36a..df115adc 100644 --- a/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/ReplayOrchestrator.kt +++ b/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/ReplayOrchestrator.kt @@ -2,7 +2,6 @@ package com.correx.core.kernel.orchestration import com.correx.core.approvals.domain.NoOpApprovalEngine import com.correx.core.artifactstore.ArtifactStore -import org.slf4j.LoggerFactory import com.correx.core.context.model.ContextPack import com.correx.core.events.events.ArtifactValidatedEvent import com.correx.core.events.events.ArtifactValidatingEvent @@ -12,17 +11,18 @@ import com.correx.core.events.types.StageId import com.correx.core.inference.GenerationConfig import com.correx.core.inference.InferenceRequest import com.correx.core.inference.ResponseFormat -import com.correx.core.sessions.Session import com.correx.core.kernel.execution.ReplayStrategy import com.correx.core.kernel.execution.WorkflowResult import com.correx.core.kernel.replay.ReplayInferenceProvider import com.correx.core.kernel.retry.exception.ReplayArtifactMissingException import com.correx.core.risk.NoOpRiskAssessor +import com.correx.core.sessions.Session import com.correx.core.transitions.execution.StageExecutionResult import com.correx.core.transitions.graph.StageConfig import com.correx.core.transitions.graph.WorkflowGraph import com.correx.core.transitions.resolution.TransitionDecision import com.correx.core.validation.model.ValidationContext +import org.slf4j.LoggerFactory import java.util.* import java.util.concurrent.* import java.util.concurrent.atomic.* @@ -87,7 +87,7 @@ class ReplayOrchestrator( val nextStageId = decision.to if (isTerminal(ctx.graph, nextStageId)) { - return completeWorkflow(ctx.sessionId, nextStageId, ctx.stageCount) + return completeWorkflow(ctx.sessionId, nextStageId, ctx.stageCount, ctx.graph.id) } when (val result = enterStage(ctx, nextStageId, session)) { @@ -101,7 +101,7 @@ class ReplayOrchestrator( } is TransitionDecision.Stay -> if (isTerminal(ctx.graph, ctx.currentStageId)) { - completeWorkflow(ctx.sessionId, ctx.currentStageId, ctx.stageCount) + completeWorkflow(ctx.sessionId, ctx.currentStageId, ctx.stageCount, ctx.graph.id) } else { step(ctx) } diff --git a/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/SessionOrchestrator.kt b/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/SessionOrchestrator.kt index 34c26495..5f9e75b9 100644 --- a/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/SessionOrchestrator.kt +++ b/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/SessionOrchestrator.kt @@ -2,22 +2,24 @@ package com.correx.core.kernel.orchestration import com.correx.core.approvals.ApprovalOutcome import com.correx.core.approvals.ApprovalStatus +import com.correx.core.approvals.Tier import com.correx.core.approvals.model.ApprovalDecision import com.correx.core.approvals.model.DomainApprovalRequest +import com.correx.core.artifacts.ArtifactState +import com.correx.core.artifacts.kind.JsonSchema +import com.correx.core.artifactstore.ArtifactStore import com.correx.core.context.builder.ContextPackBuilder import com.correx.core.context.model.ContextEntry import com.correx.core.context.model.ContextLayer import com.correx.core.context.model.ContextPack import com.correx.core.context.model.TokenBudget -import com.correx.core.events.types.ContextEntryId +import com.correx.core.events.events.ApprovalDecisionResolvedEvent +import com.correx.core.events.events.ApprovalRequestedEvent import com.correx.core.events.events.ArtifactCreatedEvent import com.correx.core.events.events.ArtifactValidatedEvent import com.correx.core.events.events.ArtifactValidatingEvent -import com.correx.core.events.events.ApprovalDecisionResolvedEvent -import com.correx.core.events.events.ApprovalRequestedEvent import com.correx.core.events.events.EventMetadata import com.correx.core.events.events.EventPayload -import com.correx.core.artifactstore.ArtifactStore import com.correx.core.events.events.InferenceCompletedEvent import com.correx.core.events.events.InferenceFailedEvent import com.correx.core.events.events.InferenceStartedEvent @@ -26,27 +28,25 @@ import com.correx.core.events.events.NewEvent import com.correx.core.events.events.OrchestrationPausedEvent import com.correx.core.events.events.OrchestrationResumedEvent import com.correx.core.events.events.RiskAssessedEvent +import com.correx.core.events.events.ToolInvocationRequestedEvent +import com.correx.core.events.events.ToolRequest 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.artifacts.ArtifactState -import com.correx.core.artifacts.kind.JsonSchema import com.correx.core.events.stores.EventStore import com.correx.core.events.types.ApprovalDecisionId import com.correx.core.events.types.ApprovalRequestId import com.correx.core.events.types.ArtifactId +import com.correx.core.events.types.ContextEntryId import com.correx.core.events.types.ContextPackId import com.correx.core.events.types.EventId import com.correx.core.events.types.InferenceRequestId import com.correx.core.events.types.RiskSummaryId import com.correx.core.events.types.SessionId import com.correx.core.events.types.StageId -import com.correx.core.events.types.ValidationReportId -import com.correx.core.approvals.Tier -import com.correx.core.events.events.ToolInvocationRequestedEvent -import com.correx.core.events.events.ToolRequest import com.correx.core.events.types.ToolInvocationId +import com.correx.core.events.types.ValidationReportId import com.correx.core.inference.FinishReason import com.correx.core.inference.InferenceRepository import com.correx.core.inference.InferenceRequest @@ -56,14 +56,14 @@ import com.correx.core.inference.ResponseFormat import com.correx.core.inference.ToolCallRequest import com.correx.core.inference.ToolDefinition import com.correx.core.inference.ToolFunction -import com.correx.core.tools.contract.ToolExecutor -import com.correx.core.tools.contract.ToolResult -import com.correx.core.tools.registry.ToolRegistry import com.correx.core.kernel.execution.WorkflowResult import com.correx.core.risk.RiskAssessor import com.correx.core.risk.RiskContext import com.correx.core.risk.toApprovalTier import com.correx.core.sessions.Session +import com.correx.core.tools.contract.ToolExecutor +import com.correx.core.tools.contract.ToolResult +import com.correx.core.tools.registry.ToolRegistry import com.correx.core.transitions.evaluation.EvaluationContext import com.correx.core.transitions.evaluation.PromptResolver import com.correx.core.transitions.execution.StageExecutionResult @@ -587,6 +587,7 @@ abstract class SessionOrchestrator( sessionId, WorkflowStartedEvent( sessionId = sessionId, + workflowId = graph.id, startStageId = graph.start, retryPolicy = config.retryPolicy, ), @@ -597,12 +598,13 @@ abstract class SessionOrchestrator( sessionId: SessionId, terminalStageId: StageId, stageCount: Int, + workflowId: String, ): WorkflowResult.Completed { log.debug( "[Orchestrator] COMPLETED session={} terminalStage={} stages={}", sessionId.value, terminalStageId.value, stageCount, ) - emit(sessionId, WorkflowCompletedEvent(sessionId, terminalStageId, stageCount)) + emit(sessionId, WorkflowCompletedEvent(sessionId, terminalStageId, stageCount, workflowId)) cancellations.remove(sessionId) return WorkflowResult.Completed(sessionId, terminalStageId) } diff --git a/core/router/src/main/kotlin/com/correx/core/router/RouterContextBuilder.kt b/core/router/src/main/kotlin/com/correx/core/router/RouterContextBuilder.kt index 7a4ef8fd..4c7bcd43 100644 --- a/core/router/src/main/kotlin/com/correx/core/router/RouterContextBuilder.kt +++ b/core/router/src/main/kotlin/com/correx/core/router/RouterContextBuilder.kt @@ -13,7 +13,7 @@ import com.correx.core.router.model.RouterConfig import com.correx.core.router.model.RouterL2Entry import com.correx.core.router.model.RouterState import com.correx.core.router.model.RouterTurn -import java.util.UUID +import java.util.* interface RouterContextBuilder { fun build(state: RouterState, budget: TokenBudget): ContextPack diff --git a/core/router/src/main/kotlin/com/correx/core/router/RouterFacade.kt b/core/router/src/main/kotlin/com/correx/core/router/RouterFacade.kt index 54867807..434e0b39 100644 --- a/core/router/src/main/kotlin/com/correx/core/router/RouterFacade.kt +++ b/core/router/src/main/kotlin/com/correx/core/router/RouterFacade.kt @@ -5,11 +5,11 @@ import com.correx.core.events.events.NewEvent import com.correx.core.events.events.SteeringNoteAddedEvent import com.correx.core.events.stores.EventStore import com.correx.core.events.types.EventId +import com.correx.core.events.types.InferenceRequestId import com.correx.core.events.types.SessionId import com.correx.core.events.types.StageId import com.correx.core.inference.GenerationConfig import com.correx.core.inference.InferenceRequest -import com.correx.core.events.types.InferenceRequestId import com.correx.core.inference.InferenceRouter import com.correx.core.inference.ModelCapability import com.correx.core.inference.ResponseFormat @@ -18,8 +18,8 @@ import com.correx.core.router.model.RouterResponse import com.correx.core.router.model.RouterTurn import com.correx.core.router.model.TurnRole import kotlinx.datetime.Clock -import java.util.UUID -import java.util.concurrent.ConcurrentHashMap +import java.util.* +import java.util.concurrent.* interface RouterFacade { suspend fun onUserInput( diff --git a/core/router/src/main/kotlin/com/correx/core/router/RouterReducer.kt b/core/router/src/main/kotlin/com/correx/core/router/RouterReducer.kt index f9dc705c..751860d9 100644 --- a/core/router/src/main/kotlin/com/correx/core/router/RouterReducer.kt +++ b/core/router/src/main/kotlin/com/correx/core/router/RouterReducer.kt @@ -2,9 +2,9 @@ package com.correx.core.router import com.correx.core.events.events.OrchestrationPausedEvent import com.correx.core.events.events.OrchestrationResumedEvent -import com.correx.core.events.events.StoredEvent 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.WorkflowCompletedEvent import com.correx.core.events.events.WorkflowFailedEvent import com.correx.core.events.events.WorkflowStartedEvent diff --git a/core/sessions/src/main/kotlin/com/correx/core/sessions/ApprovalMode.kt b/core/sessions/src/main/kotlin/com/correx/core/sessions/ApprovalMode.kt index c22e2757..93d31974 100644 --- a/core/sessions/src/main/kotlin/com/correx/core/sessions/ApprovalMode.kt +++ b/core/sessions/src/main/kotlin/com/correx/core/sessions/ApprovalMode.kt @@ -8,4 +8,4 @@ enum class ApprovalMode { PROMPT, AUTO, YOLO -} \ No newline at end of file +} diff --git a/core/sessions/src/main/kotlin/com/correx/core/sessions/DefaultSessionReducer.kt b/core/sessions/src/main/kotlin/com/correx/core/sessions/DefaultSessionReducer.kt index 56173597..e726c62a 100644 --- a/core/sessions/src/main/kotlin/com/correx/core/sessions/DefaultSessionReducer.kt +++ b/core/sessions/src/main/kotlin/com/correx/core/sessions/DefaultSessionReducer.kt @@ -56,4 +56,4 @@ class DefaultSessionReducer : SessionReducer { updatedAt = event.metadata.timestamp ) } -} \ No newline at end of file +} diff --git a/core/sessions/src/main/kotlin/com/correx/core/sessions/DefaultSessionRepository.kt b/core/sessions/src/main/kotlin/com/correx/core/sessions/DefaultSessionRepository.kt index ff0ec3e1..07e87893 100644 --- a/core/sessions/src/main/kotlin/com/correx/core/sessions/DefaultSessionRepository.kt +++ b/core/sessions/src/main/kotlin/com/correx/core/sessions/DefaultSessionRepository.kt @@ -13,4 +13,4 @@ class DefaultSessionRepository( sessionId, replayer.rebuild(sessionId), ) -} \ No newline at end of file +} diff --git a/core/sessions/src/main/kotlin/com/correx/core/sessions/SessionCounterProjection.kt b/core/sessions/src/main/kotlin/com/correx/core/sessions/SessionCounterProjection.kt index 30fabc84..edabff7c 100644 --- a/core/sessions/src/main/kotlin/com/correx/core/sessions/SessionCounterProjection.kt +++ b/core/sessions/src/main/kotlin/com/correx/core/sessions/SessionCounterProjection.kt @@ -16,4 +16,4 @@ class SessionCounterProjection( ): SessionCounterState { return state.copy(count = state.count + 1) } -} \ No newline at end of file +} diff --git a/core/sessions/src/main/kotlin/com/correx/core/sessions/SessionCounterState.kt b/core/sessions/src/main/kotlin/com/correx/core/sessions/SessionCounterState.kt index 6e51655f..b9ff0211 100644 --- a/core/sessions/src/main/kotlin/com/correx/core/sessions/SessionCounterState.kt +++ b/core/sessions/src/main/kotlin/com/correx/core/sessions/SessionCounterState.kt @@ -3,4 +3,4 @@ package com.correx.core.sessions data class SessionCounterState( val sessionId: String, val count: Int -) \ No newline at end of file +) diff --git a/core/sessions/src/main/kotlin/com/correx/core/sessions/SessionProjector.kt b/core/sessions/src/main/kotlin/com/correx/core/sessions/SessionProjector.kt index 1bd6b63c..d5d024bb 100644 --- a/core/sessions/src/main/kotlin/com/correx/core/sessions/SessionProjector.kt +++ b/core/sessions/src/main/kotlin/com/correx/core/sessions/SessionProjector.kt @@ -16,4 +16,4 @@ class SessionProjector( state: SessionState, event: StoredEvent ): SessionState = reducer.reduce(state, event) -} \ No newline at end of file +} diff --git a/core/sessions/src/main/kotlin/com/correx/core/sessions/SessionReducer.kt b/core/sessions/src/main/kotlin/com/correx/core/sessions/SessionReducer.kt index 047c8f8f..830fef53 100644 --- a/core/sessions/src/main/kotlin/com/correx/core/sessions/SessionReducer.kt +++ b/core/sessions/src/main/kotlin/com/correx/core/sessions/SessionReducer.kt @@ -7,4 +7,4 @@ interface SessionReducer { state: SessionState, event: StoredEvent ): SessionState -} \ No newline at end of file +} diff --git a/core/sessions/src/main/kotlin/com/correx/core/sessions/SessionStatus.kt b/core/sessions/src/main/kotlin/com/correx/core/sessions/SessionStatus.kt index 4a1217be..a673ff09 100644 --- a/core/sessions/src/main/kotlin/com/correx/core/sessions/SessionStatus.kt +++ b/core/sessions/src/main/kotlin/com/correx/core/sessions/SessionStatus.kt @@ -7,4 +7,4 @@ enum class SessionStatus { COMPLETED, FAILED, ; -} \ No newline at end of file +} diff --git a/core/sessions/src/main/kotlin/com/correx/core/sessions/TransitionResult.kt b/core/sessions/src/main/kotlin/com/correx/core/sessions/TransitionResult.kt index 7b0f3d61..c6844d06 100644 --- a/core/sessions/src/main/kotlin/com/correx/core/sessions/TransitionResult.kt +++ b/core/sessions/src/main/kotlin/com/correx/core/sessions/TransitionResult.kt @@ -3,4 +3,4 @@ package com.correx.core.sessions sealed interface TransitionResult { data class Applied(val newState: SessionStatus) : TransitionResult data object Rejected : TransitionResult -} \ No newline at end of file +} diff --git a/core/sessions/src/test/kotlin/com/correx/core/sessions/projections/SessionCounterProjectionTest.kt b/core/sessions/src/test/kotlin/com/correx/core/sessions/projections/SessionCounterProjectionTest.kt index 960f4b54..c664437c 100644 --- a/core/sessions/src/test/kotlin/com/correx/core/sessions/projections/SessionCounterProjectionTest.kt +++ b/core/sessions/src/test/kotlin/com/correx/core/sessions/projections/SessionCounterProjectionTest.kt @@ -2,4 +2,4 @@ package com.correx.core.sessions.projections import com.correx.testing.contracts.fixtures.projections.CountingProjectionContractTest -class SessionCounterProjectionTest : CountingProjectionContractTest() \ No newline at end of file +class SessionCounterProjectionTest : CountingProjectionContractTest() diff --git a/core/transitions/src/main/kotlin/com/correx/core/transitions/analysis/CycleExtractor.kt b/core/transitions/src/main/kotlin/com/correx/core/transitions/analysis/CycleExtractor.kt index c66a9975..a811f209 100644 --- a/core/transitions/src/main/kotlin/com/correx/core/transitions/analysis/CycleExtractor.kt +++ b/core/transitions/src/main/kotlin/com/correx/core/transitions/analysis/CycleExtractor.kt @@ -31,4 +31,4 @@ internal class CycleExtractor( return canonicalize(cycles) } -} \ No newline at end of file +} diff --git a/core/transitions/src/main/kotlin/com/correx/core/transitions/analysis/DetectedCycle.kt b/core/transitions/src/main/kotlin/com/correx/core/transitions/analysis/DetectedCycle.kt index c67571f1..b9564714 100644 --- a/core/transitions/src/main/kotlin/com/correx/core/transitions/analysis/DetectedCycle.kt +++ b/core/transitions/src/main/kotlin/com/correx/core/transitions/analysis/DetectedCycle.kt @@ -6,4 +6,4 @@ import com.correx.core.transitions.graph.TransitionEdge data class DetectedCycle( val nodes: List, val edges: List -) \ No newline at end of file +) diff --git a/core/transitions/src/main/kotlin/com/correx/core/transitions/analysis/DeterministicAdjacencyBuilder.kt b/core/transitions/src/main/kotlin/com/correx/core/transitions/analysis/DeterministicAdjacencyBuilder.kt index 39b267c1..76965d17 100644 --- a/core/transitions/src/main/kotlin/com/correx/core/transitions/analysis/DeterministicAdjacencyBuilder.kt +++ b/core/transitions/src/main/kotlin/com/correx/core/transitions/analysis/DeterministicAdjacencyBuilder.kt @@ -18,4 +18,4 @@ internal class DeterministicAdjacencyBuilder { ) } } -} \ No newline at end of file +} diff --git a/core/transitions/src/main/kotlin/com/correx/core/transitions/analysis/canonicalization/CycleCanonicalizer.kt b/core/transitions/src/main/kotlin/com/correx/core/transitions/analysis/canonicalization/CycleCanonicalizer.kt index 809905d3..4c16a389 100644 --- a/core/transitions/src/main/kotlin/com/correx/core/transitions/analysis/canonicalization/CycleCanonicalizer.kt +++ b/core/transitions/src/main/kotlin/com/correx/core/transitions/analysis/canonicalization/CycleCanonicalizer.kt @@ -33,4 +33,4 @@ internal object CycleCanonicalizer { } .sortedBy { it.nodes.first().value } } -} \ No newline at end of file +} diff --git a/core/transitions/src/main/kotlin/com/correx/core/transitions/evaluation/EvaluationContext.kt b/core/transitions/src/main/kotlin/com/correx/core/transitions/evaluation/EvaluationContext.kt index 137e7c4b..28f42e8c 100644 --- a/core/transitions/src/main/kotlin/com/correx/core/transitions/evaluation/EvaluationContext.kt +++ b/core/transitions/src/main/kotlin/com/correx/core/transitions/evaluation/EvaluationContext.kt @@ -10,4 +10,4 @@ data class EvaluationContext( val currentStage: StageId, val artifacts: Map = emptyMap(), val variables: Map = emptyMap(), -) \ No newline at end of file +) diff --git a/core/transitions/src/main/kotlin/com/correx/core/transitions/evaluation/TransitionConditionEvaluator.kt b/core/transitions/src/main/kotlin/com/correx/core/transitions/evaluation/TransitionConditionEvaluator.kt index 99496c72..aa66c9b5 100644 --- a/core/transitions/src/main/kotlin/com/correx/core/transitions/evaluation/TransitionConditionEvaluator.kt +++ b/core/transitions/src/main/kotlin/com/correx/core/transitions/evaluation/TransitionConditionEvaluator.kt @@ -7,4 +7,4 @@ fun interface TransitionConditionEvaluator { condition: TransitionCondition, context: EvaluationContext ): Boolean -} \ No newline at end of file +} diff --git a/core/transitions/src/main/kotlin/com/correx/core/transitions/execution/StageExecutionResult.kt b/core/transitions/src/main/kotlin/com/correx/core/transitions/execution/StageExecutionResult.kt index 29543615..3ff5199a 100644 --- a/core/transitions/src/main/kotlin/com/correx/core/transitions/execution/StageExecutionResult.kt +++ b/core/transitions/src/main/kotlin/com/correx/core/transitions/execution/StageExecutionResult.kt @@ -9,4 +9,4 @@ sealed interface StageExecutionResult { val reason: String, val retryable: Boolean, ) : StageExecutionResult -} \ No newline at end of file +} diff --git a/core/transitions/src/main/kotlin/com/correx/core/transitions/execution/StageExecutor.kt b/core/transitions/src/main/kotlin/com/correx/core/transitions/execution/StageExecutor.kt index a56b37cf..79ddcef8 100644 --- a/core/transitions/src/main/kotlin/com/correx/core/transitions/execution/StageExecutor.kt +++ b/core/transitions/src/main/kotlin/com/correx/core/transitions/execution/StageExecutor.kt @@ -4,4 +4,4 @@ interface StageExecutor { fun execute( request: StageExecutionRequest ): StageExecutionResult -} \ No newline at end of file +} diff --git a/core/transitions/src/main/kotlin/com/correx/core/transitions/graph/GraphOrdering.kt b/core/transitions/src/main/kotlin/com/correx/core/transitions/graph/GraphOrdering.kt index 4148166e..3f7a5639 100644 --- a/core/transitions/src/main/kotlin/com/correx/core/transitions/graph/GraphOrdering.kt +++ b/core/transitions/src/main/kotlin/com/correx/core/transitions/graph/GraphOrdering.kt @@ -12,4 +12,4 @@ internal fun WorkflowGraph.sortedTransitions(): List = { it.id.value }, { it.to.value } ) - ) \ No newline at end of file + ) diff --git a/core/transitions/src/main/kotlin/com/correx/core/transitions/graph/TransitionCondition.kt b/core/transitions/src/main/kotlin/com/correx/core/transitions/graph/TransitionCondition.kt index db14ba55..9523ec23 100644 --- a/core/transitions/src/main/kotlin/com/correx/core/transitions/graph/TransitionCondition.kt +++ b/core/transitions/src/main/kotlin/com/correx/core/transitions/graph/TransitionCondition.kt @@ -4,4 +4,4 @@ import com.correx.core.transitions.evaluation.EvaluationContext fun interface TransitionCondition { fun evaluate(context: EvaluationContext): Boolean -} \ No newline at end of file +} diff --git a/core/transitions/src/main/kotlin/com/correx/core/transitions/graph/TransitionEdge.kt b/core/transitions/src/main/kotlin/com/correx/core/transitions/graph/TransitionEdge.kt index 93e7a8df..0f4effd3 100644 --- a/core/transitions/src/main/kotlin/com/correx/core/transitions/graph/TransitionEdge.kt +++ b/core/transitions/src/main/kotlin/com/correx/core/transitions/graph/TransitionEdge.kt @@ -8,4 +8,4 @@ data class TransitionEdge( val from: StageId, val to: StageId, val condition: TransitionCondition -) \ No newline at end of file +) diff --git a/core/transitions/src/main/kotlin/com/correx/core/transitions/graph/WorkflowGraph.kt b/core/transitions/src/main/kotlin/com/correx/core/transitions/graph/WorkflowGraph.kt index 0838eb24..10c1a17d 100644 --- a/core/transitions/src/main/kotlin/com/correx/core/transitions/graph/WorkflowGraph.kt +++ b/core/transitions/src/main/kotlin/com/correx/core/transitions/graph/WorkflowGraph.kt @@ -10,6 +10,7 @@ data class WorkflowGraph( ) { val stageIds: Set get() = stages.keys init { + require(id.isNotBlank()) { "workflow id must not be blank" } require(start in stages) { "start stage must exist in stages" } } } diff --git a/core/transitions/src/main/kotlin/com/correx/core/transitions/mapper/DefaultStageExecutionEventMapper.kt b/core/transitions/src/main/kotlin/com/correx/core/transitions/mapper/DefaultStageExecutionEventMapper.kt index 4bab35fb..01722694 100644 --- a/core/transitions/src/main/kotlin/com/correx/core/transitions/mapper/DefaultStageExecutionEventMapper.kt +++ b/core/transitions/src/main/kotlin/com/correx/core/transitions/mapper/DefaultStageExecutionEventMapper.kt @@ -44,4 +44,4 @@ class DefaultStageExecutionEventMapper : StageExecutionEventMapper { ) } } -} \ No newline at end of file +} diff --git a/core/transitions/src/main/kotlin/com/correx/core/transitions/mapper/StageExecutionEventMapper.kt b/core/transitions/src/main/kotlin/com/correx/core/transitions/mapper/StageExecutionEventMapper.kt index 8d748f2f..f5756370 100644 --- a/core/transitions/src/main/kotlin/com/correx/core/transitions/mapper/StageExecutionEventMapper.kt +++ b/core/transitions/src/main/kotlin/com/correx/core/transitions/mapper/StageExecutionEventMapper.kt @@ -9,4 +9,4 @@ interface StageExecutionEventMapper { request: StageExecutionRequest, result: StageExecutionResult ): List -} \ No newline at end of file +} diff --git a/core/transitions/src/main/kotlin/com/correx/core/transitions/policy/CyclePolicy.kt b/core/transitions/src/main/kotlin/com/correx/core/transitions/policy/CyclePolicy.kt index b33493e8..d9af09a1 100644 --- a/core/transitions/src/main/kotlin/com/correx/core/transitions/policy/CyclePolicy.kt +++ b/core/transitions/src/main/kotlin/com/correx/core/transitions/policy/CyclePolicy.kt @@ -12,4 +12,4 @@ sealed interface CyclePolicy { data class Approval( val timeoutMs: Long ) : CyclePolicy -} \ No newline at end of file +} diff --git a/core/transitions/src/main/kotlin/com/correx/core/transitions/policy/CyclePolicyBinding.kt b/core/transitions/src/main/kotlin/com/correx/core/transitions/policy/CyclePolicyBinding.kt index 9dd1d5ea..94ef89dd 100644 --- a/core/transitions/src/main/kotlin/com/correx/core/transitions/policy/CyclePolicyBinding.kt +++ b/core/transitions/src/main/kotlin/com/correx/core/transitions/policy/CyclePolicyBinding.kt @@ -3,4 +3,4 @@ package com.correx.core.transitions.policy data class CyclePolicyBinding( val cycle: CycleSignature, val policy: CyclePolicy -) \ No newline at end of file +) diff --git a/core/transitions/src/main/kotlin/com/correx/core/transitions/policy/CyclePolicyResolver.kt b/core/transitions/src/main/kotlin/com/correx/core/transitions/policy/CyclePolicyResolver.kt index 9d51e70a..bc500bf0 100644 --- a/core/transitions/src/main/kotlin/com/correx/core/transitions/policy/CyclePolicyResolver.kt +++ b/core/transitions/src/main/kotlin/com/correx/core/transitions/policy/CyclePolicyResolver.kt @@ -9,4 +9,4 @@ class CyclePolicyResolver( .firstOrNull { it.cycle == signature } ?.policy } -} \ No newline at end of file +} diff --git a/core/transitions/src/main/kotlin/com/correx/core/transitions/policy/PolicyValidation.kt b/core/transitions/src/main/kotlin/com/correx/core/transitions/policy/PolicyValidation.kt index 9540494a..4a32a407 100644 --- a/core/transitions/src/main/kotlin/com/correx/core/transitions/policy/PolicyValidation.kt +++ b/core/transitions/src/main/kotlin/com/correx/core/transitions/policy/PolicyValidation.kt @@ -20,4 +20,4 @@ class PolicyValidation { return errors } -} \ No newline at end of file +} diff --git a/core/transitions/src/main/kotlin/com/correx/core/transitions/resolution/DefaultTransitionResolver.kt b/core/transitions/src/main/kotlin/com/correx/core/transitions/resolution/DefaultTransitionResolver.kt index b172cbf5..891c9442 100644 --- a/core/transitions/src/main/kotlin/com/correx/core/transitions/resolution/DefaultTransitionResolver.kt +++ b/core/transitions/src/main/kotlin/com/correx/core/transitions/resolution/DefaultTransitionResolver.kt @@ -32,4 +32,4 @@ class DefaultTransitionResolver( } return TransitionDecision.Stay } -} \ No newline at end of file +} diff --git a/core/transitions/src/main/kotlin/com/correx/core/transitions/resolution/TransitionDecision.kt b/core/transitions/src/main/kotlin/com/correx/core/transitions/resolution/TransitionDecision.kt index 73e5227b..668039bb 100644 --- a/core/transitions/src/main/kotlin/com/correx/core/transitions/resolution/TransitionDecision.kt +++ b/core/transitions/src/main/kotlin/com/correx/core/transitions/resolution/TransitionDecision.kt @@ -17,4 +17,4 @@ sealed interface TransitionDecision { ) : TransitionDecision data object NoMatch : TransitionDecision -} \ No newline at end of file +} diff --git a/core/transitions/src/main/kotlin/com/correx/core/transitions/resolution/TransitionOrdering.kt b/core/transitions/src/main/kotlin/com/correx/core/transitions/resolution/TransitionOrdering.kt index 1c38fecf..43059557 100644 --- a/core/transitions/src/main/kotlin/com/correx/core/transitions/resolution/TransitionOrdering.kt +++ b/core/transitions/src/main/kotlin/com/correx/core/transitions/resolution/TransitionOrdering.kt @@ -7,4 +7,4 @@ object TransitionOrdering { compareBy { it.from.value } .thenBy { it.id.value } .thenBy { it.to.value } -} \ No newline at end of file +} diff --git a/core/transitions/src/main/kotlin/com/correx/core/transitions/resolution/TransitionResolver.kt b/core/transitions/src/main/kotlin/com/correx/core/transitions/resolution/TransitionResolver.kt index 2e7deeb7..7c2ea9a3 100644 --- a/core/transitions/src/main/kotlin/com/correx/core/transitions/resolution/TransitionResolver.kt +++ b/core/transitions/src/main/kotlin/com/correx/core/transitions/resolution/TransitionResolver.kt @@ -8,4 +8,4 @@ interface TransitionResolver { graph: WorkflowGraph, context: EvaluationContext ): TransitionDecision -} \ No newline at end of file +} diff --git a/core/transitions/src/main/kotlin/com/correx/core/transitions/state/VisitState.kt b/core/transitions/src/main/kotlin/com/correx/core/transitions/state/VisitState.kt index 9f3de49c..65b1608b 100644 --- a/core/transitions/src/main/kotlin/com/correx/core/transitions/state/VisitState.kt +++ b/core/transitions/src/main/kotlin/com/correx/core/transitions/state/VisitState.kt @@ -3,4 +3,4 @@ package com.correx.core.transitions.state internal enum class VisitState { VISITING, VISITED -} \ No newline at end of file +} diff --git a/core/validation/src/main/kotlin/com/correx/core/validation/approval/ApprovalRequest.kt b/core/validation/src/main/kotlin/com/correx/core/validation/approval/ApprovalRequest.kt index 30e0a49e..9d601950 100644 --- a/core/validation/src/main/kotlin/com/correx/core/validation/approval/ApprovalRequest.kt +++ b/core/validation/src/main/kotlin/com/correx/core/validation/approval/ApprovalRequest.kt @@ -7,4 +7,4 @@ data class ApprovalRequest( val sessionId: SessionId?, val riskSummary: ValidationRiskStats, val validationReport: ValidationReport -) \ No newline at end of file +) diff --git a/core/validation/src/main/kotlin/com/correx/core/validation/approval/ApprovalTrigger.kt b/core/validation/src/main/kotlin/com/correx/core/validation/approval/ApprovalTrigger.kt index 510feeb3..881754ed 100644 --- a/core/validation/src/main/kotlin/com/correx/core/validation/approval/ApprovalTrigger.kt +++ b/core/validation/src/main/kotlin/com/correx/core/validation/approval/ApprovalTrigger.kt @@ -43,4 +43,4 @@ class ApprovalTrigger { return null } -} \ No newline at end of file +} diff --git a/core/validation/src/main/kotlin/com/correx/core/validation/approval/RiskSummary.kt b/core/validation/src/main/kotlin/com/correx/core/validation/approval/RiskSummary.kt index 0724cd5f..68165a4f 100644 --- a/core/validation/src/main/kotlin/com/correx/core/validation/approval/RiskSummary.kt +++ b/core/validation/src/main/kotlin/com/correx/core/validation/approval/RiskSummary.kt @@ -4,4 +4,4 @@ data class RiskSummary( val errorCount: Int, val warningCount: Int, val hasCyclePoliciesMissing: Boolean -) \ No newline at end of file +) diff --git a/core/validation/src/main/kotlin/com/correx/core/validation/artifact/ArtifactPayloadValidator.kt b/core/validation/src/main/kotlin/com/correx/core/validation/artifact/ArtifactPayloadValidator.kt index db47d8ef..7790e54f 100644 --- a/core/validation/src/main/kotlin/com/correx/core/validation/artifact/ArtifactPayloadValidator.kt +++ b/core/validation/src/main/kotlin/com/correx/core/validation/artifact/ArtifactPayloadValidator.kt @@ -1,9 +1,9 @@ package com.correx.core.validation.artifact -import com.correx.core.artifactstore.ArtifactStore import com.correx.core.artifacts.kind.FileWrittenArtifact import com.correx.core.artifacts.kind.ProcessResultArtifact import com.correx.core.artifacts.kind.TypedArtifactSlot +import com.correx.core.artifactstore.ArtifactStore import com.correx.core.events.types.ArtifactId import com.correx.core.validation.model.ValidationContext import com.correx.core.validation.model.ValidationIssue diff --git a/core/validation/src/main/kotlin/com/correx/core/validation/graph/GraphValidator.kt b/core/validation/src/main/kotlin/com/correx/core/validation/graph/GraphValidator.kt index 7442167e..ddf855c9 100644 --- a/core/validation/src/main/kotlin/com/correx/core/validation/graph/GraphValidator.kt +++ b/core/validation/src/main/kotlin/com/correx/core/validation/graph/GraphValidator.kt @@ -51,4 +51,4 @@ class GraphValidator : Validator { issues = issues ) } -} \ No newline at end of file +} diff --git a/core/validation/src/main/kotlin/com/correx/core/validation/model/ValidationLocation.kt b/core/validation/src/main/kotlin/com/correx/core/validation/model/ValidationLocation.kt index 23e98be1..e803d37f 100644 --- a/core/validation/src/main/kotlin/com/correx/core/validation/model/ValidationLocation.kt +++ b/core/validation/src/main/kotlin/com/correx/core/validation/model/ValidationLocation.kt @@ -13,4 +13,4 @@ sealed interface ValidationLocation { data class Session( val sessionId: SessionId ) : ValidationLocation -} \ No newline at end of file +} diff --git a/core/validation/src/main/kotlin/com/correx/core/validation/model/ValidationReport.kt b/core/validation/src/main/kotlin/com/correx/core/validation/model/ValidationReport.kt index 0aba2dc1..532acdd3 100644 --- a/core/validation/src/main/kotlin/com/correx/core/validation/model/ValidationReport.kt +++ b/core/validation/src/main/kotlin/com/correx/core/validation/model/ValidationReport.kt @@ -7,4 +7,4 @@ data class ValidationReport( sections.any { section -> section.issues.any { it.severity == ValidationSeverity.ERROR } } -} \ No newline at end of file +} diff --git a/core/validation/src/main/kotlin/com/correx/core/validation/model/ValidationSeverity.kt b/core/validation/src/main/kotlin/com/correx/core/validation/model/ValidationSeverity.kt index efd3e6d3..c36376bf 100644 --- a/core/validation/src/main/kotlin/com/correx/core/validation/model/ValidationSeverity.kt +++ b/core/validation/src/main/kotlin/com/correx/core/validation/model/ValidationSeverity.kt @@ -4,4 +4,4 @@ enum class ValidationSeverity { INFO, WARNING, ERROR -} \ No newline at end of file +} diff --git a/core/validation/src/main/kotlin/com/correx/core/validation/pipeline/ValidationPipeline.kt b/core/validation/src/main/kotlin/com/correx/core/validation/pipeline/ValidationPipeline.kt index 5dfc3fbf..45b2e147 100644 --- a/core/validation/src/main/kotlin/com/correx/core/validation/pipeline/ValidationPipeline.kt +++ b/core/validation/src/main/kotlin/com/correx/core/validation/pipeline/ValidationPipeline.kt @@ -29,4 +29,4 @@ class ValidationPipeline( ValidationOutcome.Passed(report) } } -} \ No newline at end of file +} diff --git a/core/validation/src/main/kotlin/com/correx/core/validation/pipeline/Validator.kt b/core/validation/src/main/kotlin/com/correx/core/validation/pipeline/Validator.kt index 0711ccbd..b9eb4be0 100644 --- a/core/validation/src/main/kotlin/com/correx/core/validation/pipeline/Validator.kt +++ b/core/validation/src/main/kotlin/com/correx/core/validation/pipeline/Validator.kt @@ -5,4 +5,4 @@ import com.correx.core.validation.model.ValidationSection fun interface Validator { suspend fun validate(context: ValidationContext): ValidationSection -} \ No newline at end of file +} diff --git a/core/validation/src/main/kotlin/com/correx/core/validation/semantic/SemanticRule.kt b/core/validation/src/main/kotlin/com/correx/core/validation/semantic/SemanticRule.kt index cf53cea0..c1d4b6ee 100644 --- a/core/validation/src/main/kotlin/com/correx/core/validation/semantic/SemanticRule.kt +++ b/core/validation/src/main/kotlin/com/correx/core/validation/semantic/SemanticRule.kt @@ -5,4 +5,4 @@ import com.correx.core.validation.model.ValidationIssue interface SemanticRule { fun validate(context: ValidationContext): List -} \ No newline at end of file +} diff --git a/core/validation/src/main/kotlin/com/correx/core/validation/semantic/SemanticValidator.kt b/core/validation/src/main/kotlin/com/correx/core/validation/semantic/SemanticValidator.kt index c50b3912..b1734c69 100644 --- a/core/validation/src/main/kotlin/com/correx/core/validation/semantic/SemanticValidator.kt +++ b/core/validation/src/main/kotlin/com/correx/core/validation/semantic/SemanticValidator.kt @@ -17,4 +17,4 @@ class SemanticValidator( issues = issues ) } -} \ No newline at end of file +} diff --git a/core/validation/src/main/kotlin/com/correx/core/validation/semantic/rules/CyclePolicyBindingRule.kt b/core/validation/src/main/kotlin/com/correx/core/validation/semantic/rules/CyclePolicyBindingRule.kt index 6f93bdc6..371937be 100644 --- a/core/validation/src/main/kotlin/com/correx/core/validation/semantic/rules/CyclePolicyBindingRule.kt +++ b/core/validation/src/main/kotlin/com/correx/core/validation/semantic/rules/CyclePolicyBindingRule.kt @@ -33,4 +33,4 @@ class CyclePolicyBindingRule( ) } } -} \ No newline at end of file +} diff --git a/core/validation/src/main/kotlin/com/correx/core/validation/session/SessionValidator.kt b/core/validation/src/main/kotlin/com/correx/core/validation/session/SessionValidator.kt index 590fe02c..8d6c3e6f 100644 --- a/core/validation/src/main/kotlin/com/correx/core/validation/session/SessionValidator.kt +++ b/core/validation/src/main/kotlin/com/correx/core/validation/session/SessionValidator.kt @@ -57,4 +57,4 @@ class SessionValidator : Validator { issues = issues ) } -} \ No newline at end of file +} diff --git a/core/validation/src/main/kotlin/com/correx/core/validation/transition/TransitionValidator.kt b/core/validation/src/main/kotlin/com/correx/core/validation/transition/TransitionValidator.kt index bf4bc412..b149a506 100644 --- a/core/validation/src/main/kotlin/com/correx/core/validation/transition/TransitionValidator.kt +++ b/core/validation/src/main/kotlin/com/correx/core/validation/transition/TransitionValidator.kt @@ -69,4 +69,4 @@ class TransitionValidator( issues = issues ) } -} \ No newline at end of file +} diff --git a/docs/specs/2026-05-17-tamboui-migration.md b/docs/specs/2026-05-17-tamboui-migration.md index cce308d4..d5f152d1 100644 --- a/docs/specs/2026-05-17-tamboui-migration.md +++ b/docs/specs/2026-05-17-tamboui-migration.md @@ -381,7 +381,7 @@ end-to-end example. ### Skeleton ```kotlin -fun main() = runBlocking { +fun main(): Unit = runBlocking { var state = TuiState() val ws = TuiWsClient(host = "localhost", port = 8080) val effectScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) diff --git a/infrastructure/artifacts-cas/src/main/kotlin/com/correx/infrastructure/artifactscas/CasArtifactStore.kt b/infrastructure/artifacts-cas/src/main/kotlin/com/correx/infrastructure/artifactscas/CasArtifactStore.kt index 81ec9451..5ac0e4e7 100644 --- a/infrastructure/artifacts-cas/src/main/kotlin/com/correx/infrastructure/artifactscas/CasArtifactStore.kt +++ b/infrastructure/artifacts-cas/src/main/kotlin/com/correx/infrastructure/artifactscas/CasArtifactStore.kt @@ -8,8 +8,8 @@ import com.correx.infrastructure.artifactscas.compact.CompactionReport import com.correx.infrastructure.artifactscas.compact.Compactor import com.correx.infrastructure.artifactscas.compact.LivenessScanner import com.correx.infrastructure.artifactscas.config.CasConfig -import com.correx.infrastructure.artifactscas.evict.Evictor import com.correx.infrastructure.artifactscas.evict.EvictionReport +import com.correx.infrastructure.artifactscas.evict.Evictor import com.correx.infrastructure.artifactscas.index.ArtifactIndex import com.correx.infrastructure.artifactscas.recovery.TailScanReport import com.correx.infrastructure.artifactscas.recovery.TailScanner diff --git a/infrastructure/artifacts-cas/src/main/kotlin/com/correx/infrastructure/artifactscas/segment/SegmentLayout.kt b/infrastructure/artifacts-cas/src/main/kotlin/com/correx/infrastructure/artifactscas/segment/SegmentLayout.kt index 9c51a426..6f2b1b21 100644 --- a/infrastructure/artifacts-cas/src/main/kotlin/com/correx/infrastructure/artifactscas/segment/SegmentLayout.kt +++ b/infrastructure/artifacts-cas/src/main/kotlin/com/correx/infrastructure/artifactscas/segment/SegmentLayout.kt @@ -3,7 +3,7 @@ package com.correx.infrastructure.artifactscas.segment import org.bouncycastle.crypto.digests.Blake3Digest import java.nio.ByteBuffer import java.nio.ByteOrder -import java.util.zip.CRC32C +import java.util.zip.* object SegmentLayout { const val HASH_SIZE: Int = 32 diff --git a/infrastructure/artifacts-cas/src/test/kotlin/com/correx/infrastructure/artifactscas/compact/CompactorTest.kt b/infrastructure/artifacts-cas/src/test/kotlin/com/correx/infrastructure/artifactscas/compact/CompactorTest.kt index 2f6edd33..b557c6d0 100644 --- a/infrastructure/artifacts-cas/src/test/kotlin/com/correx/infrastructure/artifactscas/compact/CompactorTest.kt +++ b/infrastructure/artifacts-cas/src/test/kotlin/com/correx/infrastructure/artifactscas/compact/CompactorTest.kt @@ -24,7 +24,7 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.io.TempDir import java.nio.file.Files import java.nio.file.Path -import java.util.UUID +import java.util.* import kotlin.io.path.listDirectoryEntries class CompactorTest { diff --git a/infrastructure/artifacts-cas/src/test/kotlin/com/correx/infrastructure/artifactscas/evict/EvictorTest.kt b/infrastructure/artifacts-cas/src/test/kotlin/com/correx/infrastructure/artifactscas/evict/EvictorTest.kt index 55ff5857..131bf260 100644 --- a/infrastructure/artifacts-cas/src/test/kotlin/com/correx/infrastructure/artifactscas/evict/EvictorTest.kt +++ b/infrastructure/artifacts-cas/src/test/kotlin/com/correx/infrastructure/artifactscas/evict/EvictorTest.kt @@ -21,7 +21,7 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.io.TempDir import java.nio.file.Files import java.nio.file.Path -import java.util.UUID +import java.util.* import kotlin.io.path.listDirectoryEntries class EvictorTest { diff --git a/infrastructure/persistence/src/main/kotlin/com/correx/infrastructure/persistence/InMemoryEventStore.kt b/infrastructure/persistence/src/main/kotlin/com/correx/infrastructure/persistence/InMemoryEventStore.kt index 7357af04..9a19f204 100644 --- a/infrastructure/persistence/src/main/kotlin/com/correx/infrastructure/persistence/InMemoryEventStore.kt +++ b/infrastructure/persistence/src/main/kotlin/com/correx/infrastructure/persistence/InMemoryEventStore.kt @@ -77,4 +77,4 @@ class InMemoryEventStore : EventStore { return stored } -} \ No newline at end of file +} diff --git a/infrastructure/persistence/src/main/kotlin/com/correx/infrastructure/persistence/SqliteEventStore.kt b/infrastructure/persistence/src/main/kotlin/com/correx/infrastructure/persistence/SqliteEventStore.kt index f83eec56..65ce7c22 100644 --- a/infrastructure/persistence/src/main/kotlin/com/correx/infrastructure/persistence/SqliteEventStore.kt +++ b/infrastructure/persistence/src/main/kotlin/com/correx/infrastructure/persistence/SqliteEventStore.kt @@ -19,7 +19,7 @@ import kotlinx.coroutines.withContext import kotlinx.datetime.Instant import java.sql.Connection import java.sql.ResultSet -import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.* class SqliteEventStore( private val connection: Connection, @@ -254,4 +254,4 @@ private fun ResultSet.toStoredEvent(jsonSerializer: JsonEventSerializer): Stored ), sequence = getLong("sequence"), payload = jsonSerializer.deserialize(getString("payload")) - ) \ No newline at end of file + ) diff --git a/infrastructure/persistence/src/main/kotlin/com/correx/infrastructure/persistence/artifact/LiveArtifactRepository.kt b/infrastructure/persistence/src/main/kotlin/com/correx/infrastructure/persistence/artifact/LiveArtifactRepository.kt index 6e44fdfa..7b123271 100644 --- a/infrastructure/persistence/src/main/kotlin/com/correx/infrastructure/persistence/artifact/LiveArtifactRepository.kt +++ b/infrastructure/persistence/src/main/kotlin/com/correx/infrastructure/persistence/artifact/LiveArtifactRepository.kt @@ -21,7 +21,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch -import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.* class LiveArtifactRepository( private val eventStore: EventStore, diff --git a/infrastructure/persistence/src/main/kotlin/com/correx/infrastructure/persistence/util/JDBCHelper.kt b/infrastructure/persistence/src/main/kotlin/com/correx/infrastructure/persistence/util/JDBCHelper.kt index f2a0fd0d..192c6ef8 100644 --- a/infrastructure/persistence/src/main/kotlin/com/correx/infrastructure/persistence/util/JDBCHelper.kt +++ b/infrastructure/persistence/src/main/kotlin/com/correx/infrastructure/persistence/util/JDBCHelper.kt @@ -20,4 +20,4 @@ object JDBCHelper { autoCommit = oldAutoCommit } } -} \ No newline at end of file +} diff --git a/infrastructure/persistence/src/test/kotlin/com/correx/infrastructure/persistence/InMemoryEventStoreTest.kt b/infrastructure/persistence/src/test/kotlin/com/correx/infrastructure/persistence/InMemoryEventStoreTest.kt index b9d13142..becfc309 100644 --- a/infrastructure/persistence/src/test/kotlin/com/correx/infrastructure/persistence/InMemoryEventStoreTest.kt +++ b/infrastructure/persistence/src/test/kotlin/com/correx/infrastructure/persistence/InMemoryEventStoreTest.kt @@ -5,4 +5,4 @@ import com.correx.testing.contracts.fixtures.events.store.EventStoreContractTest class InMemoryEventStoreTest : EventStoreContractTest() { override fun store(): EventStore = InMemoryEventStore() -} \ No newline at end of file +} diff --git a/infrastructure/persistence/src/test/kotlin/com/correx/infrastructure/persistence/InMemoryReplayTest.kt b/infrastructure/persistence/src/test/kotlin/com/correx/infrastructure/persistence/InMemoryReplayTest.kt index b63a46a6..649dcf94 100644 --- a/infrastructure/persistence/src/test/kotlin/com/correx/infrastructure/persistence/InMemoryReplayTest.kt +++ b/infrastructure/persistence/src/test/kotlin/com/correx/infrastructure/persistence/InMemoryReplayTest.kt @@ -20,4 +20,4 @@ class InMemoryReplayTest : SessionReplayContractTest() { SessionCounterProjection("s1") ) } -} \ No newline at end of file +} diff --git a/infrastructure/persistence/src/test/kotlin/com/correx/infrastructure/persistence/SqliteEventStoreTest.kt b/infrastructure/persistence/src/test/kotlin/com/correx/infrastructure/persistence/SqliteEventStoreTest.kt index 8106c2e6..eec25fa3 100644 --- a/infrastructure/persistence/src/test/kotlin/com/correx/infrastructure/persistence/SqliteEventStoreTest.kt +++ b/infrastructure/persistence/src/test/kotlin/com/correx/infrastructure/persistence/SqliteEventStoreTest.kt @@ -10,4 +10,4 @@ class SqliteEventStoreTest : EventStoreContractTest() { val conn = DriverManager.getConnection("jdbc:sqlite::memory:") return SqliteEventStore(conn, artifactStore = NoopArtifactStore()) } -} \ No newline at end of file +} diff --git a/infrastructure/persistence/src/test/kotlin/com/correx/infrastructure/persistence/SqliteReplayTest.kt b/infrastructure/persistence/src/test/kotlin/com/correx/infrastructure/persistence/SqliteReplayTest.kt index 5ac7d2fc..4850fb86 100644 --- a/infrastructure/persistence/src/test/kotlin/com/correx/infrastructure/persistence/SqliteReplayTest.kt +++ b/infrastructure/persistence/src/test/kotlin/com/correx/infrastructure/persistence/SqliteReplayTest.kt @@ -26,4 +26,4 @@ class SqliteReplayTest : SessionReplayContractTest() { SessionCounterProjection("s1") ) } -} \ No newline at end of file +} diff --git a/infrastructure/src/main/kotlin/com/correx/infrastructure/InfrastructureModule.kt b/infrastructure/src/main/kotlin/com/correx/infrastructure/InfrastructureModule.kt index f93ce4c6..75aa9f12 100644 --- a/infrastructure/src/main/kotlin/com/correx/infrastructure/InfrastructureModule.kt +++ b/infrastructure/src/main/kotlin/com/correx/infrastructure/InfrastructureModule.kt @@ -1,5 +1,7 @@ package com.correx.infrastructure +import com.correx.core.artifacts.DefaultArtifactReducer +import com.correx.core.artifacts.repository.ArtifactRepository import com.correx.core.artifactstore.ArtifactStore import com.correx.core.events.EventDispatcher import com.correx.core.events.stores.EventStore @@ -8,8 +10,19 @@ import com.correx.core.inference.InferenceProvider import com.correx.core.inference.InferenceRouter import com.correx.core.inference.ProviderRegistry import com.correx.core.inference.RoutingStrategy +import com.correx.core.router.DefaultRouterContextBuilder +import com.correx.core.router.DefaultRouterFacade +import com.correx.core.router.DefaultRouterReducer +import com.correx.core.router.DefaultRouterRepository +import com.correx.core.router.RouterFacade +import com.correx.core.router.RouterProjector +import com.correx.core.router.model.RouterConfig +import com.correx.core.sessions.projections.replay.DefaultEventReplayer import com.correx.core.tools.contract.ToolExecutor import com.correx.core.tools.registry.ToolRegistry +import com.correx.infrastructure.artifactscas.CasArtifactStore +import com.correx.infrastructure.artifactscas.config.CasConfig +import com.correx.infrastructure.artifactscas.index.SqliteArtifactIndex import com.correx.infrastructure.inference.DefaultProviderRegistry import com.correx.infrastructure.inference.FirstAvailableRoutingStrategy import com.correx.infrastructure.inference.commons.ModelDescriptor @@ -17,11 +30,8 @@ import com.correx.infrastructure.inference.commons.ModelManager import com.correx.infrastructure.inference.commons.ResidencyMode import com.correx.infrastructure.inference.llama.cpp.DefaultModelManager import com.correx.infrastructure.inference.llama.cpp.LlamaCppInferenceProvider -import com.correx.infrastructure.artifactscas.CasArtifactStore -import kotlinx.coroutines.runBlocking -import com.correx.infrastructure.artifactscas.config.CasConfig -import com.correx.infrastructure.artifactscas.index.SqliteArtifactIndex import com.correx.infrastructure.persistence.SqliteEventStore +import com.correx.infrastructure.persistence.artifact.LiveArtifactRepository import com.correx.infrastructure.tools.DefaultToolRegistry import com.correx.infrastructure.tools.DispatchingToolExecutor import com.correx.infrastructure.tools.SandboxedToolExecutor @@ -31,18 +41,8 @@ import com.correx.infrastructure.workflow.FileSystemPromptLoader import com.correx.infrastructure.workflow.PromptLoader import com.correx.infrastructure.workflow.TomlWorkflowLoader import com.correx.infrastructure.workflow.WorkflowLoader -import com.correx.infrastructure.persistence.artifact.LiveArtifactRepository -import com.correx.core.artifacts.repository.ArtifactRepository -import com.correx.core.artifacts.DefaultArtifactReducer -import com.correx.core.router.DefaultRouterContextBuilder -import com.correx.core.router.DefaultRouterFacade -import com.correx.core.router.DefaultRouterRepository -import com.correx.core.router.DefaultRouterReducer -import com.correx.core.router.RouterFacade -import com.correx.core.router.RouterProjector -import com.correx.core.router.model.RouterConfig -import com.correx.core.sessions.projections.replay.DefaultEventReplayer import io.ktor.client.HttpClient +import kotlinx.coroutines.runBlocking import java.nio.file.Files import java.nio.file.Path import java.nio.file.Paths diff --git a/infrastructure/tools/filesystem/src/main/kotlin/com/correx/infrastructure/tools/filesystem/FileReadTool.kt b/infrastructure/tools/filesystem/src/main/kotlin/com/correx/infrastructure/tools/filesystem/FileReadTool.kt index 56042609..fe8688e7 100644 --- a/infrastructure/tools/filesystem/src/main/kotlin/com/correx/infrastructure/tools/filesystem/FileReadTool.kt +++ b/infrastructure/tools/filesystem/src/main/kotlin/com/correx/infrastructure/tools/filesystem/FileReadTool.kt @@ -32,7 +32,7 @@ class FileReadTool( (request.parameters["path"] as? String)?.let { pathString -> runCatching { val path = Paths.get(pathString).normalize().toAbsolutePath() - if (allowedPaths.isNotEmpty() && allowedPaths.none { path.startsWith(it.normalize().toAbsolutePath()) }) { + if (allowedPaths.none { path.startsWith(it.normalize().toAbsolutePath()) }) { return@runCatching ValidationResult.Invalid("Path '$pathString' is not in the allowed list") } ValidationResult.Valid diff --git a/infrastructure/tools/filesystem/src/main/kotlin/com/correx/infrastructure/tools/filesystem/FileWriteTool.kt b/infrastructure/tools/filesystem/src/main/kotlin/com/correx/infrastructure/tools/filesystem/FileWriteTool.kt index 7428907a..4fe99429 100644 --- a/infrastructure/tools/filesystem/src/main/kotlin/com/correx/infrastructure/tools/filesystem/FileWriteTool.kt +++ b/infrastructure/tools/filesystem/src/main/kotlin/com/correx/infrastructure/tools/filesystem/FileWriteTool.kt @@ -65,7 +65,11 @@ class FileWriteTool( } put( "required", - buildJsonArray { add(JsonPrimitive("path")); add(JsonPrimitive("content")); add(JsonPrimitive("operation")) }, + buildJsonArray { + add(JsonPrimitive("path")) + add(JsonPrimitive("content")) + add(JsonPrimitive("operation")) + }, ) } override val tier: Tier = Tier.T2 diff --git a/infrastructure/tools/filesystem/src/test/kotlin/com/correx/infrastructure/tools/filesystem/FileEditToolTest.kt b/infrastructure/tools/filesystem/src/test/kotlin/com/correx/infrastructure/tools/filesystem/FileEditToolTest.kt index 7733a8f0..f69ead52 100644 --- a/infrastructure/tools/filesystem/src/test/kotlin/com/correx/infrastructure/tools/filesystem/FileEditToolTest.kt +++ b/infrastructure/tools/filesystem/src/test/kotlin/com/correx/infrastructure/tools/filesystem/FileEditToolTest.kt @@ -33,7 +33,7 @@ class FileEditToolTest { } @Test - fun `execute patch success`() = runBlocking { + fun `execute patch success`(): Unit = runBlocking { val tempDir = Files.createTempDirectory("file_edit_patch") val filePath = tempDir.resolve("test.txt") Files.writeString(filePath, "line1\nline2\nline3\n") @@ -63,7 +63,7 @@ class FileEditToolTest { } @Test - fun `validateRequest returns Invalid for disallowed path`() = runBlocking { + fun `validateRequest returns Invalid for disallowed path`(): Unit = runBlocking { val tempDir = Files.createTempDirectory("file_edit_test") val otherDir = Files.createTempDirectory("other_dir") val tool = FileEditTool(allowedPaths = setOf(tempDir)) @@ -80,7 +80,7 @@ class FileEditToolTest { } @Test - fun `validateRequest returns Invalid for non-existent file`() = runBlocking { + fun `validateRequest returns Invalid for non-existent file`(): Unit = runBlocking { val tempDir = Files.createTempDirectory("file_edit_test") val tool = FileEditTool(allowedPaths = setOf(tempDir)) val request = createRequest( @@ -96,7 +96,7 @@ class FileEditToolTest { } @Test - fun `validateRequest returns Invalid for missing parameters`() = runBlocking { + fun `validateRequest returns Invalid for missing parameters`(): Unit = runBlocking { val tempDir = Files.createTempDirectory("file_edit_test") val filePath = tempDir.resolve("test.txt") Files.writeString(filePath, "content") @@ -120,7 +120,7 @@ class FileEditToolTest { } @Test - fun `execute append success`() = runBlocking { + fun `execute append success`(): Unit = runBlocking { val tempDir = Files.createTempDirectory("file_edit_append") val filePath = tempDir.resolve("test.txt") Files.writeString(filePath, "hello") @@ -139,7 +139,7 @@ class FileEditToolTest { } @Test - fun `execute replace success`() = runBlocking { + fun `execute replace success`(): Unit = runBlocking { val tempDir = Files.createTempDirectory("file_edit_replace") val filePath = tempDir.resolve("test.txt") Files.writeString(filePath, "the quick brown fox") @@ -159,7 +159,7 @@ class FileEditToolTest { } @Test - fun `execute replace failure zero matches`() = runBlocking { + fun `execute replace failure zero matches`(): Unit = runBlocking { val tempDir = Files.createTempDirectory("file_edit_replace_fail") val filePath = tempDir.resolve("test.txt") Files.writeString(filePath, "the quick brown fox") @@ -180,7 +180,7 @@ class FileEditToolTest { } @Test - fun `execute replace failure multiple matches`() = runBlocking { + fun `execute replace failure multiple matches`(): Unit = runBlocking { val tempDir = Files.createTempDirectory("file_edit_replace_fail_multi") val filePath = tempDir.resolve("test.txt") Files.writeString(filePath, "fox fox fox") @@ -202,7 +202,7 @@ class FileEditToolTest { @Test - fun `execute patch failure`() = runBlocking { + fun `execute patch failure`(): Unit = runBlocking { val tempDir = Files.createTempDirectory("file_edit_patch_fail") val filePath = tempDir.resolve("test.txt") Files.writeString(filePath, "original") diff --git a/infrastructure/tools/filesystem/src/test/kotlin/com/correx/infrastructure/tools/filesystem/FileReadToolTest.kt b/infrastructure/tools/filesystem/src/test/kotlin/com/correx/infrastructure/tools/filesystem/FileReadToolTest.kt index 34fa39ee..1653bcbb 100644 --- a/infrastructure/tools/filesystem/src/test/kotlin/com/correx/infrastructure/tools/filesystem/FileReadToolTest.kt +++ b/infrastructure/tools/filesystem/src/test/kotlin/com/correx/infrastructure/tools/filesystem/FileReadToolTest.kt @@ -31,15 +31,15 @@ class FileReadToolTest { } @Test - fun `validateRequest returns Valid for allowed path`() = runBlocking { - val tool = FileReadTool(allowedPaths = emptySet()) + fun `validateRequest returns Valid for allowed path`(): Unit = runBlocking { val tempFile = Files.createTempFile("test_allowed", ".txt") + val tool = FileReadTool(allowedPaths = setOf(tempFile.parent)) val request = createRequest(tempFile.toString()) assertEquals(ValidationResult.Valid, tool.validateRequest(request)) } @Test - fun `validateRequest returns Invalid for disallowed path`() = runBlocking { + fun `validateRequest returns Invalid for disallowed path`(): Unit = runBlocking { val tempFile = Files.createTempFile("test_disallowed", ".txt") val tool = FileReadTool(allowedPaths = setOf(tempFile.parent.resolve("subdir"))) val request = createRequest(tempFile.toString()) @@ -49,7 +49,7 @@ class FileReadToolTest { } @Test - fun `validateRequest returns Invalid for missing path parameter`() = runBlocking { + fun `validateRequest returns Invalid for missing path parameter`(): Unit = runBlocking { val tool = FileReadTool() val request = ToolRequest( invocationId = invocationId, @@ -67,12 +67,12 @@ class FileReadToolTest { } @Test - fun `execute returns Success for valid file`() = runBlocking { + fun `execute returns Success for valid file`(): Unit = runBlocking { val content = "Hello, world!" val tempFile = Files.createTempFile("test_content", ".txt") Files.writeString(tempFile, content) - val tool = FileReadTool(allowedPaths = emptySet()) + val tool = FileReadTool(allowedPaths = setOf(tempFile.parent)) val request = createRequest(tempFile.toString()) val result = tool.execute(request) @@ -83,7 +83,7 @@ class FileReadToolTest { } @Test - fun `execute returns Failure for non-existent file`() = runBlocking { + fun `execute returns Failure for non-existent file`(): Unit = runBlocking { val tempDir = Files.createTempDirectory("test_dir") val nonExistentFile = tempDir.resolve("missing.txt") @@ -98,7 +98,7 @@ class FileReadToolTest { } @Test - fun `execute returns Failure for disallowed path`() = runBlocking { + fun `execute returns Failure for disallowed path`(): Unit = runBlocking { val tempFile = Files.createTempFile("test_disallowed_exec", ".txt") val tool = FileReadTool(allowedPaths = setOf(tempFile.parent.resolve("other"))) val request = createRequest(tempFile.toString()) diff --git a/infrastructure/tools/filesystem/src/test/kotlin/com/correx/infrastructure/tools/filesystem/FileWriteToolTest.kt b/infrastructure/tools/filesystem/src/test/kotlin/com/correx/infrastructure/tools/filesystem/FileWriteToolTest.kt index f3ac8b91..8444b8e2 100644 --- a/infrastructure/tools/filesystem/src/test/kotlin/com/correx/infrastructure/tools/filesystem/FileWriteToolTest.kt +++ b/infrastructure/tools/filesystem/src/test/kotlin/com/correx/infrastructure/tools/filesystem/FileWriteToolTest.kt @@ -34,7 +34,7 @@ class FileWriteToolTest { } @Test - fun `validateRequest returns Valid for allowed path and valid operation`() = runBlocking { + fun `validateRequest returns Valid for allowed path and valid operation`(): Unit = runBlocking { val tempDir = Files.createTempDirectory("file_write_test") val tool = FileWriteTool(allowedPaths = setOf(tempDir)) val request = createRequest( @@ -48,7 +48,7 @@ class FileWriteToolTest { } @Test - fun `validateRequest returns Invalid for disallowed path`() = runBlocking { + fun `validateRequest returns Invalid for disallowed path`(): Unit = runBlocking { val tempDir = Files.createTempDirectory("file_write_test") val otherDir = Files.createTempDirectory("other_dir") val tool = FileWriteTool(allowedPaths = setOf(tempDir)) @@ -65,7 +65,7 @@ class FileWriteToolTest { } @Test - fun `validateRequest returns Invalid for missing operation`() = runBlocking { + fun `validateRequest returns Invalid for missing operation`(): Unit = runBlocking { val tempDir = Files.createTempDirectory("file_write_test") val tool = FileWriteTool(allowedPaths = setOf(tempDir)) val request = createRequest( @@ -83,7 +83,7 @@ class FileWriteToolTest { } @Test - fun `validateRequest returns Invalid for missing path`() = runBlocking { + fun `validateRequest returns Invalid for missing path`(): Unit = runBlocking { val tempDir = Files.createTempDirectory("file_write_test") val tool = FileWriteTool(allowedPaths = setOf(tempDir)) val request = createRequest( @@ -98,7 +98,7 @@ class FileWriteToolTest { } @Test - fun `validateRequest returns Invalid for missing content on write`() = runBlocking { + fun `validateRequest returns Invalid for missing content on write`(): Unit = runBlocking { val tempDir = Files.createTempDirectory("file_write_test") val tool = FileWriteTool(allowedPaths = setOf(tempDir)) val request = createRequest( @@ -113,7 +113,7 @@ class FileWriteToolTest { } @Test - fun `validateRequest returns Invalid for unknown operation`() = runBlocking { + fun `validateRequest returns Invalid for unknown operation`(): Unit = runBlocking { val tempDir = Files.createTempDirectory("file_write_test") val tool = FileWriteTool(allowedPaths = setOf(tempDir)) val request = createRequest( @@ -128,7 +128,7 @@ class FileWriteToolTest { } @Test - fun `execute returns Success for write operation`() = runBlocking { + fun `execute returns Success for write operation`(): Unit = runBlocking { val tempDir = Files.createTempDirectory("file_write_test") val tool = FileWriteTool(allowedPaths = setOf(tempDir)) val filePath = tempDir.resolve("test.txt") @@ -151,7 +151,7 @@ class FileWriteToolTest { } @Test - fun `execute returns Success for delete operation`() = runBlocking { + fun `execute returns Success for delete operation`(): Unit = runBlocking { val tempDir = Files.createTempDirectory("file_write_test") val tool = FileWriteTool(allowedPaths = setOf(tempDir)) val filePath = tempDir.resolve("to_delete.txt") @@ -164,12 +164,7 @@ class FileWriteToolTest { ), ) - println("TempDir: $tempDir") - println("FilePath: $filePath") - println("PathString from request: ${request.parameters["path"]}") val result = tool.execute(request) - println("Result: $result") - println("Exists after: ${Files.exists(filePath)}") assertTrue(result is ToolResult.Success) val success = result as ToolResult.Success assertEquals("File deleted successfully: $filePath", success.output) @@ -178,7 +173,7 @@ class FileWriteToolTest { } @Test - fun `execute returns Failure for deleting non-existent file`() = runBlocking { + fun `execute returns Failure for deleting non-existent file`(): Unit = runBlocking { val tempDir = Files.createTempDirectory("file_write_test") val tool = FileWriteTool(allowedPaths = setOf(tempDir)) val filePath = tempDir.resolve("non_existent.txt") @@ -199,7 +194,7 @@ class FileWriteToolTest { } @Test - fun `execute returns Failure for disallowed path`() = runBlocking { + fun `execute returns Failure for disallowed path`(): Unit = runBlocking { val tempDir = Files.createTempDirectory("file_write_test") val otherDir = Files.createTempDirectory("other_dir") val tool = FileWriteTool(allowedPaths = setOf(tempDir)) @@ -221,7 +216,7 @@ class FileWriteToolTest { } @Test - fun `validateRequest returns Valid for existing file in allowed directory`() = runBlocking { + fun `validateRequest returns Valid for existing file in allowed directory`(): Unit = runBlocking { val tempDir = Files.createTempDirectory("file_write_test_existing") val filePath = tempDir.resolve("existing.txt") Files.writeString(filePath, "content") diff --git a/infrastructure/tools/src/main/kotlin/com/correx/infrastructure/tools/SandboxedToolExecutor.kt b/infrastructure/tools/src/main/kotlin/com/correx/infrastructure/tools/SandboxedToolExecutor.kt index 58e24451..50f47f0f 100644 --- a/infrastructure/tools/src/main/kotlin/com/correx/infrastructure/tools/SandboxedToolExecutor.kt +++ b/infrastructure/tools/src/main/kotlin/com/correx/infrastructure/tools/SandboxedToolExecutor.kt @@ -21,7 +21,6 @@ import java.io.IOException import java.nio.file.Files import java.nio.file.Path import java.nio.file.StandardCopyOption -import java.util.* @SuppressWarnings("TooManyFunctions", "LongParameterList") class SandboxedToolExecutor( diff --git a/infrastructure/tools/src/main/kotlin/com/correx/infrastructure/tools/ToolConfig.kt b/infrastructure/tools/src/main/kotlin/com/correx/infrastructure/tools/ToolConfig.kt index 9e79d14f..10076ae2 100644 --- a/infrastructure/tools/src/main/kotlin/com/correx/infrastructure/tools/ToolConfig.kt +++ b/infrastructure/tools/src/main/kotlin/com/correx/infrastructure/tools/ToolConfig.kt @@ -1,7 +1,7 @@ package com.correx.infrastructure.tools -import com.correx.core.artifactstore.ArtifactStore import com.correx.core.artifacts.MaterializingArtifactWriter +import com.correx.core.artifactstore.ArtifactStore import com.correx.core.tools.contract.Tool import com.correx.infrastructure.tools.filesystem.FileEditTool import com.correx.infrastructure.tools.filesystem.FileReadTool @@ -25,41 +25,55 @@ data class FileWriteConfig( val sandboxRoot: Path? = null, val workingDir: Path? = null, ) + data class FileEditConfig( val enabled: Boolean = false, val allowedPaths: Set = emptySet(), val workingDir: Path? = null, ) -data class ShellConfig(val enabled: Boolean = false, val allowedExecutables: Set = emptySet(), val workingDir: Path? = null) + +data class ShellConfig( + val enabled: Boolean = false, + val allowedExecutables: Set = emptySet(), + val workingDir: Path? = null, +) /** * Extension function to convert [ToolConfig] into a list of [Tool] implementations. */ fun ToolConfig.buildTools(): List = buildList { if (shell.enabled) { - add(ShellTool( - allowedExecutables = shell.allowedExecutables, - workingDir = shell.workingDir, - )) + add( + ShellTool( + allowedExecutables = shell.allowedExecutables, + workingDir = shell.workingDir, + ), + ) } if (fileRead.enabled) { - add(FileReadTool( - allowedPaths = fileRead.allowedPaths - )) + add( + FileReadTool( + allowedPaths = fileRead.allowedPaths, + ), + ) } if (fileWrite.enabled) { - add(FileWriteTool( - allowedPaths = fileWrite.allowedPaths, - artifactStore = fileWrite.artifactStore, - materializingWriter = fileWrite.materializingWriter, - sandboxRoot = fileWrite.sandboxRoot, - workingDir = fileWrite.workingDir, - )) + add( + FileWriteTool( + allowedPaths = fileWrite.allowedPaths, + artifactStore = fileWrite.artifactStore, + materializingWriter = fileWrite.materializingWriter, + sandboxRoot = fileWrite.sandboxRoot, + workingDir = fileWrite.workingDir, + ), + ) } if (fileEdit.enabled) { - add(FileEditTool( - allowedPaths = fileEdit.allowedPaths, - workingDir = fileEdit.workingDir, - )) + add( + FileEditTool( + allowedPaths = fileEdit.allowedPaths, + workingDir = fileEdit.workingDir, + ), + ) } } diff --git a/infrastructure/tools/src/test/kotlin/com/correx/infrastructure/tools/shell/ShellToolTest.kt b/infrastructure/tools/src/test/kotlin/com/correx/infrastructure/tools/shell/ShellToolTest.kt index 1c8f456b..e4d8c5df 100644 --- a/infrastructure/tools/src/test/kotlin/com/correx/infrastructure/tools/shell/ShellToolTest.kt +++ b/infrastructure/tools/src/test/kotlin/com/correx/infrastructure/tools/shell/ShellToolTest.kt @@ -30,14 +30,14 @@ class ShellToolTest { } @Test - fun `validateRequest returns Valid for allowed executable`() = runBlocking { + fun `validateRequest returns Valid for allowed executable`(): Unit = runBlocking { val tool = ShellTool(allowedExecutables = setOf("echo")) val request = createRequest(listOf("echo", "hello")) assertEquals(ValidationResult.Valid, tool.validateRequest(request)) } @Test - fun `validateRequest returns Invalid for disallowed executable`() = runBlocking { + fun `validateRequest returns Invalid for disallowed executable`(): Unit = runBlocking { val tool = ShellTool(allowedExecutables = setOf("echo")) val request = createRequest(listOf("ls")) val result = tool.validateRequest(request) @@ -46,7 +46,7 @@ class ShellToolTest { } @Test - fun `validateRequest returns Invalid for empty argv`() = runBlocking { + fun `validateRequest returns Invalid for empty argv`(): Unit = runBlocking { val tool = ShellTool(allowedExecutables = setOf("echo")) val request = createRequest(emptyList()) val result = tool.validateRequest(request) @@ -58,7 +58,7 @@ class ShellToolTest { } @Test - fun `validateRequest returns Invalid for null argv`() = runBlocking { + fun `validateRequest returns Invalid for null argv`(): Unit = runBlocking { val tool = ShellTool(allowedExecutables = setOf("echo")) val request = ToolRequest( invocationId = invocationId, @@ -76,7 +76,7 @@ class ShellToolTest { } @Test - fun `execute returns Success for exit code 0`() = runBlocking { + fun `execute returns Success for exit code 0`(): Unit = runBlocking { val tool = ShellTool(allowedExecutables = setOf("echo")) val request = createRequest(listOf("echo", "hello")) val result = tool.execute(request) @@ -89,7 +89,7 @@ class ShellToolTest { } @Test - fun `execute returns Failure with non-zero exit code for failed process`() = runBlocking { + fun `execute returns Failure with non-zero exit code for failed process`(): Unit = runBlocking { val tool = ShellTool(allowedExecutables = setOf("false")) val request = createRequest(listOf("false")) val result = tool.execute(request) @@ -101,7 +101,7 @@ class ShellToolTest { } @Test - fun `execute returns Failure on timeout`() = runBlocking { + fun `execute returns Failure on timeout`(): Unit = runBlocking { // Using sleep command to simulate timeout val tool = ShellTool(allowedExecutables = setOf("sleep"), timeoutMs = 100L) val request = createRequest(listOf("sleep", "1")) diff --git a/infrastructure/workflow/src/main/kotlin/com/correx/infrastructure/workflow/TomlWorkflowLoader.kt b/infrastructure/workflow/src/main/kotlin/com/correx/infrastructure/workflow/TomlWorkflowLoader.kt index 6ec6ef9b..82f42ac7 100644 --- a/infrastructure/workflow/src/main/kotlin/com/correx/infrastructure/workflow/TomlWorkflowLoader.kt +++ b/infrastructure/workflow/src/main/kotlin/com/correx/infrastructure/workflow/TomlWorkflowLoader.kt @@ -32,12 +32,12 @@ private data class ProducesEntry( private data class StageSection( val id: String = "", val prompt: String? = null, - @JsonProperty("system_prompt") val systemPrompt: String? = null, + @param:JsonProperty("system_prompt") val systemPrompt: String? = null, val produces: List = emptyList(), val needs: List = emptyList(), - @JsonProperty("allowed_tools") val allowedTools: List = emptyList(), - @JsonProperty("token_budget") val tokenBudget: Int = 4096, - @JsonProperty("max_retries") val maxRetries: Int = 3, + @param:JsonProperty("allowed_tools") val allowedTools: List = emptyList(), + @param:JsonProperty("token_budget") val tokenBudget: Int = 4096, + @param:JsonProperty("max_retries") val maxRetries: Int = 3, ) // condition fields flattened into the transition row @@ -45,10 +45,10 @@ private data class TransitionSection( val id: String = "", val from: String = "", val to: String = "", - @JsonProperty("condition_type") val conditionType: String = "", - @JsonProperty("condition_artifact_id") val conditionArtifactId: String? = null, - @JsonProperty("condition_key") val conditionKey: String? = null, - @JsonProperty("condition_value") val conditionValue: String? = null, + @param:JsonProperty("condition_type") val conditionType: String = "", + @param:JsonProperty("condition_artifact_id") val conditionArtifactId: String? = null, + @param:JsonProperty("condition_key") val conditionKey: String? = null, + @param:JsonProperty("condition_value") val conditionValue: String? = null, ) private const val TERMINAL = "done" diff --git a/infrastructure/workflow/src/test/kotlin/com/correx/infrastructure/workflow/FileSystemPromptLoaderTest.kt b/infrastructure/workflow/src/test/kotlin/com/correx/infrastructure/workflow/FileSystemPromptLoaderTest.kt index 35dd1f08..6ff750ad 100644 --- a/infrastructure/workflow/src/test/kotlin/com/correx/infrastructure/workflow/FileSystemPromptLoaderTest.kt +++ b/infrastructure/workflow/src/test/kotlin/com/correx/infrastructure/workflow/FileSystemPromptLoaderTest.kt @@ -4,8 +4,8 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows import java.nio.file.Files import kotlin.io.path.writeText -import kotlin.test.assertEquals import kotlin.test.assertContains +import kotlin.test.assertEquals class FileSystemPromptLoaderTest { diff --git a/testing/approvals/src/test/kotlin/ApprovalTriggerTest.kt b/testing/approvals/src/test/kotlin/ApprovalTriggerTest.kt index 579d556a..8709e5a5 100644 --- a/testing/approvals/src/test/kotlin/ApprovalTriggerTest.kt +++ b/testing/approvals/src/test/kotlin/ApprovalTriggerTest.kt @@ -47,4 +47,4 @@ class ApprovalTriggerTest { assertNull(result) } -} \ No newline at end of file +} diff --git a/testing/approvals/src/test/kotlin/GrantSemanticsTest.kt b/testing/approvals/src/test/kotlin/GrantSemanticsTest.kt index f1664982..c1be52f6 100644 --- a/testing/approvals/src/test/kotlin/GrantSemanticsTest.kt +++ b/testing/approvals/src/test/kotlin/GrantSemanticsTest.kt @@ -89,4 +89,4 @@ class GrantSemanticsTest { val decision = engine.evaluate(req, ctx, grants, now) assertEquals(ApprovalOutcome.AUTO_APPROVED, decision.outcome) } -} \ No newline at end of file +} diff --git a/testing/approvals/src/test/kotlin/TierImmutabilityTest.kt b/testing/approvals/src/test/kotlin/TierImmutabilityTest.kt index f34e3e09..b70881e7 100644 --- a/testing/approvals/src/test/kotlin/TierImmutabilityTest.kt +++ b/testing/approvals/src/test/kotlin/TierImmutabilityTest.kt @@ -52,4 +52,4 @@ class TierImmutabilityTest { val decision = engine.evaluate(req, ctx, listOf(grant), now) Assertions.assertEquals(tier, decision.tier) } -} \ No newline at end of file +} diff --git a/testing/contracts/src/main/kotlin/com/correx/testing/contracts/utils/ConcurrencyRunner.kt b/testing/contracts/src/main/kotlin/com/correx/testing/contracts/utils/ConcurrencyRunner.kt index 3b6d0205..c9a9d7c0 100644 --- a/testing/contracts/src/main/kotlin/com/correx/testing/contracts/utils/ConcurrencyRunner.kt +++ b/testing/contracts/src/main/kotlin/com/correx/testing/contracts/utils/ConcurrencyRunner.kt @@ -32,4 +32,4 @@ object ConcurrencyRunner { done.await() executor.shutdown() } -} \ No newline at end of file +} diff --git a/testing/contracts/src/main/kotlin/com/correx/testing/contracts/utils/DeterministicBarrier.kt b/testing/contracts/src/main/kotlin/com/correx/testing/contracts/utils/DeterministicBarrier.kt index d73f6ba9..0b03be5a 100644 --- a/testing/contracts/src/main/kotlin/com/correx/testing/contracts/utils/DeterministicBarrier.kt +++ b/testing/contracts/src/main/kotlin/com/correx/testing/contracts/utils/DeterministicBarrier.kt @@ -9,4 +9,4 @@ class DeterministicBarrier(parties: Int) { fun await() { barrier.await() } -} \ No newline at end of file +} diff --git a/testing/contracts/src/test/kotlin/com/correx/testing/contracts/model/StageIdTest.kt b/testing/contracts/src/test/kotlin/com/correx/testing/contracts/model/StageIdTest.kt index 81344063..f5ea2c7f 100644 --- a/testing/contracts/src/test/kotlin/com/correx/testing/contracts/model/StageIdTest.kt +++ b/testing/contracts/src/test/kotlin/com/correx/testing/contracts/model/StageIdTest.kt @@ -29,4 +29,4 @@ class StageIdTest { assertEquals(a, b) } -} \ No newline at end of file +} diff --git a/testing/contracts/src/test/kotlin/com/correx/testing/contracts/model/TransitionIdTest.kt b/testing/contracts/src/test/kotlin/com/correx/testing/contracts/model/TransitionIdTest.kt index c9b5de7b..58024636 100644 --- a/testing/contracts/src/test/kotlin/com/correx/testing/contracts/model/TransitionIdTest.kt +++ b/testing/contracts/src/test/kotlin/com/correx/testing/contracts/model/TransitionIdTest.kt @@ -29,4 +29,4 @@ class TransitionIdTest { assertEquals(a, b) } -} \ No newline at end of file +} diff --git a/testing/contracts/src/test/kotlin/com/correx/testing/contracts/model/ValidationReportContractTest.kt b/testing/contracts/src/test/kotlin/com/correx/testing/contracts/model/ValidationReportContractTest.kt index e200ddcc..5c309682 100644 --- a/testing/contracts/src/test/kotlin/com/correx/testing/contracts/model/ValidationReportContractTest.kt +++ b/testing/contracts/src/test/kotlin/com/correx/testing/contracts/model/ValidationReportContractTest.kt @@ -26,4 +26,4 @@ class ValidationReportContractTest { assertEquals(r1, r2) } -} \ No newline at end of file +} diff --git a/testing/contracts/src/test/kotlin/com/correx/testing/contracts/model/WorkflowGraphTest.kt b/testing/contracts/src/test/kotlin/com/correx/testing/contracts/model/WorkflowGraphTest.kt index fcd0bc28..e474a912 100644 --- a/testing/contracts/src/test/kotlin/com/correx/testing/contracts/model/WorkflowGraphTest.kt +++ b/testing/contracts/src/test/kotlin/com/correx/testing/contracts/model/WorkflowGraphTest.kt @@ -17,6 +17,7 @@ class WorkflowGraphTest { @Test fun `graph stores immutable structure`() { val graph = WorkflowGraph( + id = "workflow-test", stages = mapOf(s1 to StageConfig(), s2 to StageConfig()), transitions = setOf( TransitionEdge( @@ -39,6 +40,7 @@ class WorkflowGraphTest { val node = s1 val graph = WorkflowGraph( + id = "workflow-test", stages = mapOf(node to StageConfig()), transitions = emptySet(), start = s1 @@ -51,12 +53,14 @@ class WorkflowGraphTest { @Test fun `graph equality is structural`() { val g1 = WorkflowGraph( + id = "workflow-test", stages = mapOf(s1 to StageConfig()), transitions = emptySet(), start = s1 ) val g2 = WorkflowGraph( + id = "workflow-test", stages = mapOf(s1 to StageConfig()), transitions = emptySet(), start = s1 diff --git a/testing/contracts/src/test/kotlin/com/correx/testing/contracts/model/orchestration/EventsTest.kt b/testing/contracts/src/test/kotlin/com/correx/testing/contracts/model/orchestration/EventsTest.kt index a9adc16a..0edb62a2 100644 --- a/testing/contracts/src/test/kotlin/com/correx/testing/contracts/model/orchestration/EventsTest.kt +++ b/testing/contracts/src/test/kotlin/com/correx/testing/contracts/model/orchestration/EventsTest.kt @@ -14,11 +14,15 @@ import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test class EventsTest { + + private val workflowId = "workflow-test" + @Test fun `WorkflowStartedEvent survives round-trip`() { val event: EventPayload = WorkflowStartedEvent( sessionId = SessionId("s1"), startStageId = StageId("stage-a"), + workflowId = workflowId, ) val json = eventJson.encodeToString(EventPayload.serializer(), event) @@ -33,6 +37,7 @@ class EventsTest { sessionId = SessionId("s1"), terminalStageId = StageId("stage-a"), totalStages = 1, + workflowId = workflowId, ) val json = eventJson.encodeToString(EventPayload.serializer(), event) @@ -129,4 +134,4 @@ class EventsTest { assertEquals(event, decoded) } -} \ No newline at end of file +} diff --git a/testing/deterministic/src/test/kotlin/GraphValidatorTest.kt b/testing/deterministic/src/test/kotlin/GraphValidatorTest.kt index a9289b39..5fc66ed8 100644 --- a/testing/deterministic/src/test/kotlin/GraphValidatorTest.kt +++ b/testing/deterministic/src/test/kotlin/GraphValidatorTest.kt @@ -103,6 +103,7 @@ class GraphValidatorTest { ) val graph = WorkflowGraph( + id = "workflow-test", stages = mapOf(a to StageConfig(), b to StageConfig()), transitions = setOf(edge1, edge2), start = a diff --git a/testing/deterministic/src/test/kotlin/RouterContextBuilderTest.kt b/testing/deterministic/src/test/kotlin/RouterContextBuilderTest.kt index 0e929843..0f505ff7 100644 --- a/testing/deterministic/src/test/kotlin/RouterContextBuilderTest.kt +++ b/testing/deterministic/src/test/kotlin/RouterContextBuilderTest.kt @@ -1,6 +1,7 @@ -import com.correx.core.context.model.CompressionMetadata import com.correx.core.context.model.ContextLayer import com.correx.core.context.model.TokenBudget +import com.correx.core.events.types.SessionId +import com.correx.core.events.types.StageId import com.correx.core.router.DefaultRouterContextBuilder import com.correx.core.router.model.RouterConfig import com.correx.core.router.model.RouterL2Entry @@ -9,10 +10,6 @@ import com.correx.core.router.model.RouterTurn import com.correx.core.router.model.StageOutcomeKind import com.correx.core.router.model.TurnRole import com.correx.core.router.model.WorkflowStatus -import com.correx.core.events.types.ContextEntryId -import com.correx.core.events.types.ContextPackId -import com.correx.core.events.types.SessionId -import com.correx.core.events.types.StageId import kotlinx.datetime.Clock import kotlinx.datetime.Instant import org.junit.jupiter.api.Assertions.assertEquals @@ -537,8 +534,18 @@ class RouterContextBuilderTest { RouterTurn(TurnRole.ROUTER, "second reply", Instant.parse("2026-01-04T00:00:00Z")), ), l2Memory = listOf( - RouterL2Entry(StageId("s1"), "stage one done", StageOutcomeKind.SUCCESS, Instant.parse("2026-01-01T10:00:00Z")), - RouterL2Entry(StageId("s2"), "stage two failed", StageOutcomeKind.FAILURE, Instant.parse("2026-01-02T10:00:00Z")), + RouterL2Entry( + StageId("s1"), + "stage one done", + StageOutcomeKind.SUCCESS, + Instant.parse("2026-01-01T10:00:00Z"), + ), + RouterL2Entry( + StageId("s2"), + "stage two failed", + StageOutcomeKind.FAILURE, + Instant.parse("2026-01-02T10:00:00Z"), + ), ), ) val pack = builder.build(state, TokenBudget(limit = 10000)) diff --git a/testing/deterministic/src/test/kotlin/RouterFacadeTest.kt b/testing/deterministic/src/test/kotlin/RouterFacadeTest.kt index bc00503e..1ba69c69 100644 --- a/testing/deterministic/src/test/kotlin/RouterFacadeTest.kt +++ b/testing/deterministic/src/test/kotlin/RouterFacadeTest.kt @@ -1,12 +1,13 @@ +import com.correx.core.context.model.ContextPack import com.correx.core.context.model.TokenBudget import com.correx.core.events.events.NewEvent import com.correx.core.events.events.SteeringNoteAddedEvent import com.correx.core.events.events.StoredEvent import com.correx.core.events.stores.EventStore +import com.correx.core.events.types.ContextPackId import com.correx.core.events.types.InferenceRequestId import com.correx.core.events.types.SessionId import com.correx.core.events.types.StageId -import com.correx.core.inference.GenerationConfig import com.correx.core.inference.InferenceProvider import com.correx.core.inference.InferenceRequest import com.correx.core.inference.InferenceResponse @@ -14,7 +15,6 @@ import com.correx.core.inference.InferenceRouter import com.correx.core.inference.ModelCapability import com.correx.core.inference.TokenUsage import com.correx.core.router.ChatMode -import com.correx.core.router.DefaultRouterContextBuilder import com.correx.core.router.DefaultRouterFacade import com.correx.core.router.RouterContextBuilder import com.correx.core.router.RouterFacade @@ -24,18 +24,13 @@ import com.correx.core.router.model.RouterResponse import com.correx.core.router.model.RouterState import com.correx.core.router.model.TurnRole import com.correx.core.router.model.WorkflowStatus -import com.correx.core.context.model.ContextPack -import com.correx.core.events.types.ContextPackId -import com.correx.core.events.types.ContextEntryId -import com.correx.core.context.model.ContextEntry -import com.correx.core.context.model.ContextLayer +import kotlinx.coroutines.runBlocking import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.Assertions.assertNotNull import org.junit.jupiter.api.Assertions.assertNull import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test -import kotlinx.coroutines.runBlocking class RouterFacadeTest { @@ -44,7 +39,7 @@ class RouterFacadeTest { // -------------------------------------------------------------------------- @Test - fun `CHAT mode returns inference response content`() = runBlocking { + fun `CHAT mode returns inference response content`(): Unit = runBlocking { val mockStore = mockEventStore() val facade = facadeWithMocks(eventStore = mockStore, chatMode = ChatMode.CHAT) val response = facade.onUserInput(sessionId = SessionId("test-session"), input = "Hello, world!") @@ -52,7 +47,7 @@ class RouterFacadeTest { } @Test - fun `CHAT mode sets steeringEmitted to false`() = runBlocking { + fun `CHAT mode sets steeringEmitted to false`(): Unit = runBlocking { val mockStore = mockEventStore() val facade = facadeWithMocks(eventStore = mockStore, chatMode = ChatMode.CHAT) val response = facade.onUserInput(sessionId = SessionId("test-session"), input = "Hello!") @@ -60,7 +55,7 @@ class RouterFacadeTest { } @Test - fun `CHAT mode does not append to EventStore`() = runBlocking { + fun `CHAT mode does not append to EventStore`(): Unit = runBlocking { val mockStore = mockEventStore() val facade = facadeWithMocks(eventStore = mockStore, chatMode = ChatMode.CHAT) facade.onUserInput(sessionId = SessionId("test-session"), input = "Hello!") @@ -72,7 +67,7 @@ class RouterFacadeTest { // -------------------------------------------------------------------------- @Test - fun `STEERING mode sets steeringEmitted to true`() = runBlocking { + fun `STEERING mode sets steeringEmitted to true`(): Unit = runBlocking { val mockStore = mockEventStore() val facade = facadeWithMocks(eventStore = mockStore, chatMode = ChatMode.STEERING) val response = facade.onUserInput(sessionId = SessionId("test-session"), input = "Hello!") @@ -80,7 +75,7 @@ class RouterFacadeTest { } @Test - fun `STEERING mode appends SteeringNoteAddedEvent with correct session id and user input`() = runBlocking { + fun `STEERING mode appends SteeringNoteAddedEvent with correct session id and user input`(): Unit = runBlocking { val mockStore = mockEventStore() val facade = facadeWithMocks(eventStore = mockStore, chatMode = ChatMode.STEERING) facade.onUserInput(sessionId = SessionId("session-xyz"), input = "steer this way") @@ -93,13 +88,17 @@ class RouterFacadeTest { } @Test - fun `STEERING mode appends SteeringNoteAddedEvent with stageId from state`() = runBlocking { + fun `STEERING mode appends SteeringNoteAddedEvent with stageId from state`(): Unit = runBlocking { val mockStore = mockEventStore() val stageId = StageId("stage-A") val facade = DefaultRouterFacade( routerRepository = object : RouterRepository { override suspend fun getRouterState(sessionId: SessionId): RouterState = - RouterState(sessionId = sessionId, workflowStatus = WorkflowStatus.RUNNING, currentStageId = stageId) + RouterState( + sessionId = sessionId, + workflowStatus = WorkflowStatus.RUNNING, + currentStageId = stageId, + ) }, routerContextBuilder = object : RouterContextBuilder { override fun build(state: RouterState, budget: TokenBudget): ContextPack = emptyContextPack() @@ -114,7 +113,7 @@ class RouterFacadeTest { } @Test - fun `STEERING mode appends SteeringNoteAddedEvent with null stageId when state has none`() = runBlocking { + fun `STEERING mode appends SteeringNoteAddedEvent with null stageId when state has none`(): Unit = runBlocking { val mockStore = mockEventStore() val facade = DefaultRouterFacade( routerRepository = object : RouterRepository { @@ -138,12 +137,16 @@ class RouterFacadeTest { // -------------------------------------------------------------------------- @Test - fun `conversation history grows per call — user and router turns appended`() = runBlocking { + fun `conversation history grows per call - user and router turns appended`(): Unit = runBlocking { val capturedStates = mutableListOf() val facade = DefaultRouterFacade( routerRepository = object : RouterRepository { override suspend fun getRouterState(sessionId: SessionId): RouterState = - RouterState(sessionId = sessionId, workflowStatus = WorkflowStatus.RUNNING, currentStageId = StageId("s1")) + RouterState( + sessionId = sessionId, + workflowStatus = WorkflowStatus.RUNNING, + currentStageId = StageId("s1"), + ) }, routerContextBuilder = object : RouterContextBuilder { override fun build(state: RouterState, budget: TokenBudget): ContextPack { @@ -171,7 +174,7 @@ class RouterFacadeTest { } @Test - fun `conversation history is session-scoped — different sessions do not share history`() = runBlocking { + fun `conversation history is session-scoped - different sessions do not share history`(): Unit = runBlocking { val capturedStates = mutableListOf() val facade = DefaultRouterFacade( routerRepository = object : RouterRepository { @@ -204,7 +207,7 @@ class RouterFacadeTest { // -------------------------------------------------------------------------- @Test - fun `state is passed through to context builder`() = runBlocking { + fun `state is passed through to context builder`(): Unit = runBlocking { val capturedState = mutableListOf() val mockContextBuilder = object : RouterContextBuilder { override fun build(state: RouterState, budget: TokenBudget): ContextPack { @@ -215,7 +218,11 @@ class RouterFacadeTest { val facade = DefaultRouterFacade( routerRepository = object : RouterRepository { override suspend fun getRouterState(sessionId: SessionId): RouterState = - RouterState(sessionId = sessionId, workflowStatus = WorkflowStatus.RUNNING, currentStageId = StageId("s1")) + RouterState( + sessionId = sessionId, + workflowStatus = WorkflowStatus.RUNNING, + currentStageId = StageId("s1"), + ) }, routerContextBuilder = mockContextBuilder, inferenceRouter = mockInferenceRouter("inference response"), @@ -229,7 +236,7 @@ class RouterFacadeTest { } @Test - fun `budget is passed through to context builder`() = runBlocking { + fun `budget is passed through to context builder`(): Unit = runBlocking { val capturedBudget = mutableListOf() val mockContextBuilder = object : RouterContextBuilder { override fun build(state: RouterState, budget: TokenBudget): ContextPack { @@ -252,10 +259,13 @@ class RouterFacadeTest { } @Test - fun `stage ID uses state currentStageId`() = runBlocking { + fun `stage ID uses state currentStageId`(): Unit = runBlocking { val capturedStageId = mutableListOf() val mockInferenceRouter = object : InferenceRouter { - override suspend fun route(stageId: StageId, requiredCapabilities: Set): InferenceProvider { + override suspend fun route( + stageId: StageId, + requiredCapabilities: Set, + ): InferenceProvider { capturedStageId.add(stageId) return mockProvider("response") } @@ -281,10 +291,13 @@ class RouterFacadeTest { } @Test - fun `stage ID falls back to StageId("none") when state has no currentStageId`() = runBlocking { + fun `stage ID falls back to StageId none when state has no currentStageId`(): Unit = runBlocking { val capturedStageId = mutableListOf() val mockInferenceRouter = object : InferenceRouter { - override suspend fun route(stageId: StageId, requiredCapabilities: Set): InferenceProvider { + override suspend fun route( + stageId: StageId, + requiredCapabilities: Set, + ): InferenceProvider { capturedStageId.add(stageId) return mockProvider("response") } @@ -309,10 +322,13 @@ class RouterFacadeTest { } @Test - fun `new InferenceRequestId per call`() = runBlocking { + fun `new InferenceRequestId per call`(): Unit = runBlocking { val capturedRequestIds = mutableListOf() val mockInferenceRouter = object : InferenceRouter { - override suspend fun route(stageId: StageId, requiredCapabilities: Set): InferenceProvider { + override suspend fun route( + stageId: StageId, + requiredCapabilities: Set, + ): InferenceProvider { return mockProviderWithCapture("response", capturedRequestIds) } } @@ -334,10 +350,13 @@ class RouterFacadeTest { } @Test - fun `GenerationConfig defaults temperature 0 7 topP 0 9 maxTokens 512`() = runBlocking { + fun `GenerationConfig defaults temperature 0 7 topP 0 9 maxTokens 512`(): Unit = runBlocking { val capturedRequests = mutableListOf() val mockInferenceRouter = object : InferenceRouter { - override suspend fun route(stageId: StageId, requiredCapabilities: Set): InferenceProvider { + override suspend fun route( + stageId: StageId, + requiredCapabilities: Set, + ): InferenceProvider { return mockProviderWithRequestCapture("response", capturedRequests) } } @@ -360,7 +379,7 @@ class RouterFacadeTest { } @Test - fun `context pack is passed to inference provider`() = runBlocking { + fun `context pack is passed to inference provider`(): Unit = runBlocking { val capturedContextPacks = mutableListOf() val facade = DefaultRouterFacade( routerRepository = object : RouterRepository { @@ -381,7 +400,10 @@ class RouterFacadeTest { } }, inferenceRouter = object : InferenceRouter { - override suspend fun route(stageId: StageId, requiredCapabilities: Set): InferenceProvider { + override suspend fun route( + stageId: StageId, + requiredCapabilities: Set, + ): InferenceProvider { return object : InferenceProvider { override val id = com.correx.core.events.types.ProviderId("mock") override val name = "Mock" @@ -397,8 +419,10 @@ class RouterFacadeTest { latencyMs = 0, ) } + override suspend fun healthCheck(): com.correx.core.inference.ProviderHealth = com.correx.core.inference.ProviderHealth.Healthy + override fun capabilities(): Set = emptySet() } } @@ -410,7 +434,7 @@ class RouterFacadeTest { } @Test - fun `responseFormat defaults to Text`() = runBlocking { + fun `responseFormat defaults to Text`(): Unit = runBlocking { val capturedRequests = mutableListOf() val facade = DefaultRouterFacade( routerRepository = object : RouterRepository { @@ -420,7 +444,10 @@ class RouterFacadeTest { override fun build(state: RouterState, budget: TokenBudget): ContextPack = emptyContextPack() }, inferenceRouter = object : InferenceRouter { - override suspend fun route(stageId: StageId, requiredCapabilities: Set): InferenceProvider { + override suspend fun route( + stageId: StageId, + requiredCapabilities: Set, + ): InferenceProvider { return mockProviderWithRequestCapture("response", capturedRequests) } }, @@ -433,7 +460,7 @@ class RouterFacadeTest { } @Test - fun `onUserInput returns RouterResponse with content and mode flag`() = runBlocking { + fun `onUserInput returns RouterResponse with content and mode flag`(): Unit = runBlocking { val mockStore = mockEventStore() val facade = facadeWithMocks(eventStore = mockStore, chatMode = ChatMode.STEERING) val response = facade.onUserInput(sessionId = SessionId("test-session"), input = "Hello!") @@ -455,7 +482,11 @@ class RouterFacadeTest { ): RouterFacade = DefaultRouterFacade( routerRepository = object : RouterRepository { override suspend fun getRouterState(sessionId: SessionId): RouterState = - RouterState(sessionId = sessionId, workflowStatus = WorkflowStatus.RUNNING, currentStageId = StageId("s1")) + RouterState( + sessionId = sessionId, + workflowStatus = WorkflowStatus.RUNNING, + currentStageId = StageId("s1"), + ) }, routerContextBuilder = object : RouterContextBuilder { override fun build(state: RouterState, budget: TokenBudget): ContextPack = emptyContextPack() @@ -472,7 +503,10 @@ class RouterFacadeTest { private fun mockInferenceRouter(responseText: String): InferenceRouter = object : InferenceRouter { - override suspend fun route(stageId: StageId, requiredCapabilities: Set): InferenceProvider = + override suspend fun route( + stageId: StageId, + requiredCapabilities: Set, + ): InferenceProvider = mockProvider(responseText) } @@ -488,8 +522,10 @@ class RouterFacadeTest { tokensUsed = TokenUsage(promptTokens = 10, completionTokens = 5), latencyMs = 0, ) + override suspend fun healthCheck(): com.correx.core.inference.ProviderHealth = com.correx.core.inference.ProviderHealth.Healthy + override fun capabilities(): Set = setOf(com.correx.core.inference.CapabilityScore(ModelCapability.General, 1.0)) } @@ -512,8 +548,10 @@ class RouterFacadeTest { latencyMs = 0, ) } + override suspend fun healthCheck(): com.correx.core.inference.ProviderHealth = com.correx.core.inference.ProviderHealth.Healthy + override fun capabilities(): Set = emptySet() } @@ -535,8 +573,10 @@ class RouterFacadeTest { latencyMs = 0, ) } + override suspend fun healthCheck(): com.correx.core.inference.ProviderHealth = com.correx.core.inference.ProviderHealth.Healthy + override fun capabilities(): Set = emptySet() } @@ -568,16 +608,19 @@ class RouterFacadeTest { override suspend fun appendAll(events: List): List = events.map { append(it) } - override fun read(sessionId: com.correx.core.events.types.SessionId): List = + override fun read(sessionId: SessionId): List = storedEvents.values.filter { it.metadata.sessionId == sessionId }.toList() - override fun readFrom(sessionId: com.correx.core.events.types.SessionId, fromSequence: Long): List = + override fun readFrom( + sessionId: SessionId, + fromSequence: Long, + ): List = read(sessionId).filter { it.sequence >= fromSequence } - override fun lastSequence(sessionId: com.correx.core.events.types.SessionId): Long? = + override fun lastSequence(sessionId: SessionId): Long? = read(sessionId).maxOfOrNull { it.sequence } - override fun subscribe(sessionId: com.correx.core.events.types.SessionId): kotlinx.coroutines.flow.Flow = + override fun subscribe(sessionId: SessionId): kotlinx.coroutines.flow.Flow = throw UnsupportedOperationException("subscribe not implemented for mock") override fun allEvents(): Sequence = diff --git a/testing/deterministic/src/test/kotlin/RouterProjectorTest.kt b/testing/deterministic/src/test/kotlin/RouterProjectorTest.kt index 21f3aa70..4ba8ae1d 100644 --- a/testing/deterministic/src/test/kotlin/RouterProjectorTest.kt +++ b/testing/deterministic/src/test/kotlin/RouterProjectorTest.kt @@ -7,13 +7,13 @@ import com.correx.core.events.events.ToolInvokedEvent import com.correx.core.events.events.WorkflowCompletedEvent import com.correx.core.events.events.WorkflowFailedEvent import com.correx.core.events.events.WorkflowStartedEvent +import com.correx.core.events.types.SessionId +import com.correx.core.events.types.StageId import com.correx.core.router.DefaultRouterReducer import com.correx.core.router.RouterProjector import com.correx.core.router.model.RouterState import com.correx.core.router.model.StageOutcomeKind import com.correx.core.router.model.WorkflowStatus -import com.correx.core.events.types.SessionId -import com.correx.core.events.types.StageId import com.correx.testing.fixtures.EventFixtures.stored import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertNull @@ -26,6 +26,7 @@ class RouterProjectorTest { private val projector = RouterProjector(reducer) private val sessionId = SessionId("s1") private val stageId = StageId("stage-1") + private val workflowId = "workflow-test" @Test fun `initial() returns RouterState with IDLE status and empty collections`() { @@ -49,7 +50,7 @@ class RouterProjectorTest { val state = projector.initial() val updated = projector.apply( state, - stored(payload = WorkflowStartedEvent(sessionId, stageId)), + stored(payload = WorkflowStartedEvent(sessionId, workflowId, stageId)), ) assertEquals(sessionId, updated.sessionId) assertEquals(WorkflowStatus.RUNNING, updated.workflowStatus) @@ -60,11 +61,11 @@ class RouterProjectorTest { fun `apply(WorkflowCompletedEvent) delegates to reducer and sets COMPLETED`() { val started = projector.apply( projector.initial(), - stored(payload = WorkflowStartedEvent(sessionId, stageId)), + stored(payload = WorkflowStartedEvent(sessionId, workflowId, stageId)), ) val updated = projector.apply( started, - stored(payload = WorkflowCompletedEvent(sessionId, stageId, 3)), + stored(payload = WorkflowCompletedEvent(sessionId, stageId, 3, workflowId)), ) assertEquals(WorkflowStatus.COMPLETED, updated.workflowStatus) assertNull(updated.currentStageId) @@ -75,7 +76,7 @@ class RouterProjectorTest { fun `apply(WorkflowFailedEvent) delegates to reducer and sets FAILED`() { val started = projector.apply( projector.initial(), - stored(payload = WorkflowStartedEvent(sessionId, stageId)), + stored(payload = WorkflowStartedEvent(sessionId, workflowId, stageId)), ) val updated = projector.apply( started, @@ -89,7 +90,7 @@ class RouterProjectorTest { fun `apply(OrchestrationPausedEvent) delegates to reducer and sets PAUSED`() { val started = projector.apply( projector.initial(), - stored(payload = WorkflowStartedEvent(sessionId, stageId)), + stored(payload = WorkflowStartedEvent(sessionId, workflowId, stageId)), ) val updated = projector.apply( started, @@ -103,7 +104,7 @@ class RouterProjectorTest { fun `apply(OrchestrationResumedEvent) delegates to reducer and sets RUNNING`() { val started = projector.apply( projector.initial(), - stored(payload = WorkflowStartedEvent(sessionId, stageId)), + stored(payload = WorkflowStartedEvent(sessionId, workflowId, stageId)), ) val paused = projector.apply( started, @@ -133,7 +134,7 @@ class RouterProjectorTest { fun `apply(StageFailedEvent) delegates to reducer and appends FAILURE entry`() { val state = projector.apply( projector.initial(), - stored(payload = WorkflowStartedEvent(sessionId, stageId)), + stored(payload = WorkflowStartedEvent(sessionId, workflowId, stageId)), ) val updated = projector.apply( state, @@ -169,13 +170,13 @@ class RouterProjectorTest { fun `apply produces same result as reducer reduce for every handled event`() { val state = projector.initial() val events = listOf( - stored(payload = WorkflowStartedEvent(sessionId, StageId("a"))), + stored(payload = WorkflowStartedEvent(sessionId, workflowId, StageId("a"))), stored(payload = StageCompletedEvent(sessionId, StageId("a"), StageId("t1"))), stored(payload = OrchestrationPausedEvent(sessionId, StageId("a"), "hold")), stored(payload = OrchestrationResumedEvent(sessionId, StageId("a"))), stored(payload = StageCompletedEvent(sessionId, StageId("b"), StageId("t2"))), stored(payload = StageFailedEvent(sessionId, StageId("b"), StageId("t3"), "error")), - stored(payload = WorkflowCompletedEvent(sessionId, StageId("c"), 3)), + stored(payload = WorkflowCompletedEvent(sessionId, StageId("c"), 3, workflowId)), ) var projected = state @@ -193,7 +194,7 @@ class RouterProjectorTest { state = projector.apply( state, - stored(payload = WorkflowStartedEvent(sessionId, StageId("stage-A"))), + stored(payload = WorkflowStartedEvent(sessionId, workflowId, StageId("stage-A"))), ) assertEquals(WorkflowStatus.RUNNING, state.workflowStatus) @@ -205,7 +206,7 @@ class RouterProjectorTest { state = projector.apply( state, - stored(payload = WorkflowCompletedEvent(sessionId, StageId("stage-A"), 1)), + stored(payload = WorkflowCompletedEvent(sessionId, StageId("stage-A"), 1, workflowId)), ) assertEquals(WorkflowStatus.COMPLETED, state.workflowStatus) assertNull(state.currentStageId) diff --git a/testing/deterministic/src/test/kotlin/RouterReducerTest.kt b/testing/deterministic/src/test/kotlin/RouterReducerTest.kt index 657d0d71..cdca0736 100644 --- a/testing/deterministic/src/test/kotlin/RouterReducerTest.kt +++ b/testing/deterministic/src/test/kotlin/RouterReducerTest.kt @@ -1,17 +1,17 @@ import com.correx.core.events.events.OrchestrationPausedEvent import com.correx.core.events.events.OrchestrationResumedEvent -import com.correx.core.events.events.SteeringNoteAddedEvent import com.correx.core.events.events.StageCompletedEvent import com.correx.core.events.events.StageFailedEvent +import com.correx.core.events.events.SteeringNoteAddedEvent import com.correx.core.events.events.ToolInvokedEvent import com.correx.core.events.events.WorkflowCompletedEvent import com.correx.core.events.events.WorkflowFailedEvent import com.correx.core.events.events.WorkflowStartedEvent +import com.correx.core.events.types.SessionId +import com.correx.core.events.types.StageId import com.correx.core.router.DefaultRouterReducer import com.correx.core.router.model.StageOutcomeKind import com.correx.core.router.model.WorkflowStatus -import com.correx.core.events.types.SessionId -import com.correx.core.events.types.StageId import com.correx.testing.fixtures.EventFixtures.stored import kotlinx.datetime.Instant import org.junit.jupiter.api.Assertions.assertEquals @@ -25,6 +25,7 @@ class RouterReducerTest { private val reducer = DefaultRouterReducer() private val sessionId = SessionId("s1") private val stageId = StageId("stage-1") + private val workflowId = "workflow-test" @Test fun `initial state has IDLE status and empty collections`() { @@ -41,7 +42,7 @@ class RouterReducerTest { val state = reducer.initial val updated = reducer.reduce( state, - stored(payload = WorkflowStartedEvent(sessionId, stageId)), + stored(payload = WorkflowStartedEvent(sessionId, workflowId, stageId)), ) assertEquals(sessionId, updated.sessionId) assertEquals(WorkflowStatus.RUNNING, updated.workflowStatus) @@ -53,11 +54,11 @@ class RouterReducerTest { val initial = reducer.initial val started = reducer.reduce( initial, - stored(payload = WorkflowStartedEvent(sessionId, stageId)), + stored(payload = WorkflowStartedEvent(sessionId, workflowId, stageId)), ) val completed = reducer.reduce( started, - stored(payload = WorkflowCompletedEvent(sessionId, stageId, 3)), + stored(payload = WorkflowCompletedEvent(sessionId, stageId, 3, workflowId)), ) assertEquals(WorkflowStatus.COMPLETED, completed.workflowStatus) assertNull(completed.currentStageId) @@ -69,7 +70,7 @@ class RouterReducerTest { val initial = reducer.initial val started = reducer.reduce( initial, - stored(payload = WorkflowStartedEvent(sessionId, stageId)), + stored(payload = WorkflowStartedEvent(sessionId, workflowId, stageId)), ) val failed = reducer.reduce( started, @@ -84,7 +85,7 @@ class RouterReducerTest { val initial = reducer.initial val started = reducer.reduce( initial, - stored(payload = WorkflowStartedEvent(sessionId, stageId)), + stored(payload = WorkflowStartedEvent(sessionId, workflowId, stageId)), ) val paused = reducer.reduce( started, @@ -99,7 +100,7 @@ class RouterReducerTest { val initial = reducer.initial val started = reducer.reduce( initial, - stored(payload = WorkflowStartedEvent(sessionId, stageId)), + stored(payload = WorkflowStartedEvent(sessionId, workflowId, stageId)), ) val paused = reducer.reduce( started, @@ -130,7 +131,7 @@ class RouterReducerTest { val initial = reducer.initial val started = reducer.reduce( initial, - stored(payload = WorkflowStartedEvent(sessionId, stageId)), + stored(payload = WorkflowStartedEvent(sessionId, workflowId, stageId)), ) val updated = reducer.reduce( started, @@ -146,7 +147,7 @@ class RouterReducerTest { val initial = reducer.initial val started = reducer.reduce( initial, - stored(payload = WorkflowStartedEvent(sessionId, stageId)), + stored(payload = WorkflowStartedEvent(sessionId, workflowId, stageId)), ) val storedEvent = stored(payload = StageFailedEvent(sessionId, stageId, StageId("t1"), "timeout")) val updated = reducer.reduce(started, storedEvent) @@ -189,7 +190,7 @@ class RouterReducerTest { val initial = reducer.initial val started = reducer.reduce( initial, - stored(payload = WorkflowStartedEvent(sessionId, stageId)), + stored(payload = WorkflowStartedEvent(sessionId, workflowId, stageId)), ) val withNote = reducer.reduce( started, @@ -223,7 +224,7 @@ class RouterReducerTest { val s0 = reducer.initial assertEquals(WorkflowStatus.IDLE, s0.workflowStatus) - val s1 = reducer.reduce(s0, stored(payload = WorkflowStartedEvent(sessionId, StageId("stage-A")))) + val s1 = reducer.reduce(s0, stored(payload = WorkflowStartedEvent(sessionId, workflowId, StageId("stage-A")))) assertEquals(WorkflowStatus.RUNNING, s1.workflowStatus) assertEquals(StageId("stage-A"), s1.currentStageId) @@ -231,11 +232,15 @@ class RouterReducerTest { assertEquals(1, s2.l2Memory.size) assertEquals(StageOutcomeKind.SUCCESS, s2.l2Memory[0].outcome) - val s3 = reducer.reduce(s2, stored(payload = WorkflowCompletedEvent(sessionId, StageId("stage-A"), 1))) + val s3 = + reducer.reduce(s2, stored(payload = WorkflowCompletedEvent(sessionId, StageId("stage-A"), 1, workflowId))) assertEquals(WorkflowStatus.COMPLETED, s3.workflowStatus) assertNull(s3.currentStageId) - val s4 = reducer.reduce(s3, stored(payload = OrchestrationPausedEvent(sessionId, StageId("stage-A"), "manual pause"))) + val s4 = reducer.reduce( + s3, + stored(payload = OrchestrationPausedEvent(sessionId, StageId("stage-A"), "manual pause")), + ) assertEquals(WorkflowStatus.PAUSED, s4.workflowStatus) val s5 = reducer.reduce(s4, stored(payload = OrchestrationResumedEvent(sessionId, StageId("stage-A")))) diff --git a/testing/deterministic/src/test/kotlin/SessionReplayDeterminismTest.kt b/testing/deterministic/src/test/kotlin/SessionReplayDeterminismTest.kt index 64eba35d..7c5b9cc7 100644 --- a/testing/deterministic/src/test/kotlin/SessionReplayDeterminismTest.kt +++ b/testing/deterministic/src/test/kotlin/SessionReplayDeterminismTest.kt @@ -55,4 +55,4 @@ class SessionReplayDeterminismTest { assertEquals(state1, state2) } -} \ No newline at end of file +} diff --git a/testing/fixtures/src/main/kotlin/com/correx/testing/fixtures/EventFixtures.kt b/testing/fixtures/src/main/kotlin/com/correx/testing/fixtures/EventFixtures.kt index dde182a4..8502d888 100644 --- a/testing/fixtures/src/main/kotlin/com/correx/testing/fixtures/EventFixtures.kt +++ b/testing/fixtures/src/main/kotlin/com/correx/testing/fixtures/EventFixtures.kt @@ -50,4 +50,4 @@ object EventFixtures { ) } -} \ No newline at end of file +} diff --git a/testing/fixtures/src/main/kotlin/com/correx/testing/fixtures/InferenceFixtures.kt b/testing/fixtures/src/main/kotlin/com/correx/testing/fixtures/InferenceFixtures.kt index 1ac43c7b..62e5b127 100644 --- a/testing/fixtures/src/main/kotlin/com/correx/testing/fixtures/InferenceFixtures.kt +++ b/testing/fixtures/src/main/kotlin/com/correx/testing/fixtures/InferenceFixtures.kt @@ -11,7 +11,6 @@ import com.correx.core.events.types.InferenceRequestId import com.correx.core.events.types.ProviderId import com.correx.core.events.types.SessionId import com.correx.core.events.types.StageId -import com.correx.core.utils.TypeId import com.correx.core.inference.FinishReason import com.correx.core.inference.GenerationConfig import com.correx.core.inference.InferenceProvider @@ -20,6 +19,7 @@ import com.correx.core.inference.InferenceResponse import com.correx.core.inference.InferenceRouter import com.correx.core.inference.ModelCapability import com.correx.core.inference.TokenUsage +import com.correx.core.utils.TypeId import com.correx.testing.fixtures.inference.MockInferenceProvider import kotlinx.datetime.Clock import java.util.* diff --git a/testing/fixtures/src/main/kotlin/com/correx/testing/fixtures/WorkflowFixtures.kt b/testing/fixtures/src/main/kotlin/com/correx/testing/fixtures/WorkflowFixtures.kt index df31ec7a..9a70f7a7 100644 --- a/testing/fixtures/src/main/kotlin/com/correx/testing/fixtures/WorkflowFixtures.kt +++ b/testing/fixtures/src/main/kotlin/com/correx/testing/fixtures/WorkflowFixtures.kt @@ -20,6 +20,7 @@ object WorkflowFixtures { ) return WorkflowGraph( + id = "workflow-test", stages = mapOf(a to StageConfig(), b to StageConfig()), transitions = setOf(t1), start = a diff --git a/testing/fixtures/src/main/kotlin/com/correx/testing/fixtures/inference/MockInferenceProvider.kt b/testing/fixtures/src/main/kotlin/com/correx/testing/fixtures/inference/MockInferenceProvider.kt index fee68cd9..74bea364 100644 --- a/testing/fixtures/src/main/kotlin/com/correx/testing/fixtures/inference/MockInferenceProvider.kt +++ b/testing/fixtures/src/main/kotlin/com/correx/testing/fixtures/inference/MockInferenceProvider.kt @@ -69,4 +69,4 @@ class MockInferenceProvider( override fun capabilities(): Set = declaredCapabilities } -class InferenceProviderException(message: String) : Exception(message) \ No newline at end of file +class InferenceProviderException(message: String) : Exception(message) diff --git a/testing/fixtures/src/main/kotlin/com/correx/testing/fixtures/inference/MockTokenizer.kt b/testing/fixtures/src/main/kotlin/com/correx/testing/fixtures/inference/MockTokenizer.kt index bda81c7f..60a81952 100644 --- a/testing/fixtures/src/main/kotlin/com/correx/testing/fixtures/inference/MockTokenizer.kt +++ b/testing/fixtures/src/main/kotlin/com/correx/testing/fixtures/inference/MockTokenizer.kt @@ -14,4 +14,4 @@ class MockTokenizer : Tokenizer { override suspend fun countTokens(text: String): Int = (text.length + 3) / 4 -} \ No newline at end of file +} diff --git a/testing/fixtures/src/main/kotlin/com/correx/testing/fixtures/transitions/TransitionFixtures.kt b/testing/fixtures/src/main/kotlin/com/correx/testing/fixtures/transitions/TransitionFixtures.kt index 93af894d..6bdad4a7 100644 --- a/testing/fixtures/src/main/kotlin/com/correx/testing/fixtures/transitions/TransitionFixtures.kt +++ b/testing/fixtures/src/main/kotlin/com/correx/testing/fixtures/transitions/TransitionFixtures.kt @@ -32,12 +32,13 @@ object TransitionFixtures { val c = StageId("C") return WorkflowGraph( + id = "workflow-test", stages = mapOf(a to StageConfig(), b to StageConfig(), c to StageConfig()), transitions = setOf( TransitionEdge(id = TransitionId("t1"), from = a, to = b, condition = { true }), TransitionEdge(id = TransitionId("t2"), from = b, to = c, condition = { true }), ), - start = a + start = a, ) } diff --git a/testing/integration/src/test/kotlin/SessionOrchestratorIntegrationTest.kt b/testing/integration/src/test/kotlin/SessionOrchestratorIntegrationTest.kt index 54903224..bf023973 100644 --- a/testing/integration/src/test/kotlin/SessionOrchestratorIntegrationTest.kt +++ b/testing/integration/src/test/kotlin/SessionOrchestratorIntegrationTest.kt @@ -5,13 +5,18 @@ import com.correx.core.approvals.domain.DefaultApprovalEngine import com.correx.core.approvals.model.ApprovalContext import com.correx.core.approvals.model.ApprovalDecision import com.correx.core.approvals.model.ApprovalScopeIdentity +import com.correx.core.artifacts.DefaultArtifactReducer +import com.correx.core.artifactstore.ArtifactStore import com.correx.core.events.events.ApprovalRequestedEvent +import com.correx.core.events.events.InferenceCompletedEvent +import com.correx.core.events.events.InferenceStartedEvent import com.correx.core.events.events.OrchestrationPausedEvent import com.correx.core.events.events.OrchestrationResumedEvent 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.execution.RetryPolicy +import com.correx.core.events.types.ArtifactId import com.correx.core.events.types.SessionId import com.correx.core.events.types.StageId import com.correx.core.inference.InferenceRepository @@ -32,23 +37,19 @@ import com.correx.core.sessions.DefaultSessionRepository import com.correx.core.sessions.projections.replay.DefaultEventReplayer import com.correx.core.sessions.projections.replay.EventReplayer import com.correx.core.transitions.graph.StageConfig +import com.correx.core.transitions.graph.TransitionEdge import com.correx.core.transitions.graph.WorkflowGraph +import com.correx.core.utils.TypeId import com.correx.core.validation.approval.ApprovalTrigger import com.correx.core.validation.pipeline.ValidationPipeline -import com.correx.core.artifacts.DefaultArtifactReducer import com.correx.infrastructure.persistence.InMemoryEventStore import com.correx.infrastructure.persistence.artifact.LiveArtifactRepository +import com.correx.testing.contracts.fixtures.artifactstore.NoopArtifactStore import com.correx.testing.fixtures.InferenceFixtures import com.correx.testing.fixtures.context.ContextFixtures import com.correx.testing.fixtures.cyclePolicyMissingValidator import com.correx.testing.fixtures.inference.MockInferenceProvider import com.correx.testing.fixtures.transitions.TransitionFixtures -import com.correx.core.artifactstore.ArtifactStore -import com.correx.core.events.events.InferenceCompletedEvent -import com.correx.core.events.events.InferenceStartedEvent -import com.correx.core.events.types.ArtifactId -import com.correx.core.utils.TypeId -import com.correx.testing.contracts.fixtures.artifactstore.NoopArtifactStore import com.correx.testing.fixtures.transitions.TransitionFixtures.threeStageGraph import com.correx.testing.kernel.MockSessionEventReplayer import kotlinx.coroutines.launch @@ -209,9 +210,10 @@ class SessionOrchestratorIntegrationTest { // Single-stage graph: enterStage(A) triggers exactly one approval; after resume, // the always-true transition moves to the terminal "done" and the workflow completes. val graph = WorkflowGraph( + id = "workflow-test", stages = mapOf(StageId("A") to StageConfig()), transitions = setOf( - com.correx.core.transitions.graph.TransitionEdge( + TransitionEdge( id = com.correx.core.events.types.TransitionId("t1"), from = StageId("A"), to = StageId("done"), @@ -268,6 +270,7 @@ class SessionOrchestratorIntegrationTest { @Test fun `workflow with empty graph fails immediately`(): Unit = runBlocking { val emptyGraph = WorkflowGraph( + id = "workflow-test", stages = mapOf(StageId("start") to StageConfig()), transitions = emptySet(), start = StageId("start"), @@ -310,8 +313,8 @@ class SessionOrchestratorIntegrationTest { assertTrue(recordingStore.puts.size >= 2, "Expected at least 2 puts, got ${recordingStore.puts.size}") val events = eventStore.read(sessionId) - val started = events.mapNotNull { it.payload as? InferenceStartedEvent }.first() - val completed = events.mapNotNull { it.payload as? InferenceCompletedEvent }.first() + val started = events.firstNotNullOf { it.payload as? InferenceStartedEvent } + val completed = events.firstNotNullOf { it.payload as? InferenceCompletedEvent } assertEquals(recordingStore.idForIndex(0), started.promptArtifactId) assertEquals(recordingStore.idForIndex(1), completed.responseArtifactId) @@ -325,17 +328,18 @@ class SessionOrchestratorIntegrationTest { val z = StageId("Z") // ghost is missing from stages map but has an outgoing transition so it is non-terminal val brokenGraph = WorkflowGraph( + id = "workflow-test", stages = mapOf(a to StageConfig(), b to StageConfig()), transitions = setOf( - com.correx.core.transitions.graph.TransitionEdge( + TransitionEdge( id = com.correx.core.events.types.TransitionId("t1"), from = a, to = b, condition = { true }, ), - com.correx.core.transitions.graph.TransitionEdge( + TransitionEdge( id = com.correx.core.events.types.TransitionId("t2"), from = b, to = ghost, condition = { true }, ), - com.correx.core.transitions.graph.TransitionEdge( + TransitionEdge( id = com.correx.core.events.types.TransitionId("t3"), from = ghost, to = z, condition = { true }, ), @@ -391,5 +395,7 @@ private class RecordingArtifactStore : ArtifactStore { override suspend fun get(id: ArtifactId): ByteArray? = byId[id] - override suspend fun flushBefore(commit: suspend () -> Unit) { commit() } + override suspend fun flushBefore(commit: suspend () -> Unit) { + commit() + } } diff --git a/testing/integration/src/test/kotlin/ValidationPipelineIntegrationTest.kt b/testing/integration/src/test/kotlin/ValidationPipelineIntegrationTest.kt index 33eb0957..783d1920 100644 --- a/testing/integration/src/test/kotlin/ValidationPipelineIntegrationTest.kt +++ b/testing/integration/src/test/kotlin/ValidationPipelineIntegrationTest.kt @@ -59,6 +59,7 @@ class ValidationPipelineIntegrationTest { val a = StageId("A") val ghost = StageId("GHOST") val graphWithDanglingTransition = WorkflowGraph( + id = "workflow-test", stages = mapOf(a to StageConfig()), transitions = setOf(TransitionEdge(TransitionId("t1"), from = a, to = ghost) { true }), start = a, @@ -69,4 +70,4 @@ class ValidationPipelineIntegrationTest { assertInstanceOf(ValidationOutcome.Rejected::class.java, outcome) assertFalse((outcome as ValidationOutcome.Rejected).retryable) } -} \ No newline at end of file +} diff --git a/testing/kernel/src/main/kotlin/com/correx/testing/kernel/MockEventReplayer.kt b/testing/kernel/src/main/kotlin/com/correx/testing/kernel/MockEventReplayer.kt index b6034671..0c3fb339 100644 --- a/testing/kernel/src/main/kotlin/com/correx/testing/kernel/MockEventReplayer.kt +++ b/testing/kernel/src/main/kotlin/com/correx/testing/kernel/MockEventReplayer.kt @@ -39,4 +39,4 @@ class MockSessionEventReplayer : EventReplayer { invalidTransitions = 0, ) } -} \ No newline at end of file +} diff --git a/testing/kernel/src/test/kotlin/DefaultRetryCoordinatorTest.kt b/testing/kernel/src/test/kotlin/DefaultRetryCoordinatorTest.kt index 66d5fbf6..9cbe416a 100644 --- a/testing/kernel/src/test/kotlin/DefaultRetryCoordinatorTest.kt +++ b/testing/kernel/src/test/kotlin/DefaultRetryCoordinatorTest.kt @@ -165,4 +165,4 @@ class DefaultRetryCoordinatorTest { // virtual time should have advanced by backoffMs assertEquals(1000L, currentTime) } -} \ No newline at end of file +} diff --git a/testing/kernel/src/test/kotlin/ReplayInferenceProviderTest.kt b/testing/kernel/src/test/kotlin/ReplayInferenceProviderTest.kt index 7510b21e..632d1077 100644 --- a/testing/kernel/src/test/kotlin/ReplayInferenceProviderTest.kt +++ b/testing/kernel/src/test/kotlin/ReplayInferenceProviderTest.kt @@ -158,4 +158,4 @@ class ReplayInferenceProviderTest { fun `id is ReplayProvider`() { Assertions.assertEquals(ProviderId("replay-provider"), replayProvider.id) } -} \ No newline at end of file +} diff --git a/testing/projections/src/test/kotlin/DefaultSessionReducerTest.kt b/testing/projections/src/test/kotlin/DefaultSessionReducerTest.kt index 366ed784..9fbb6377 100644 --- a/testing/projections/src/test/kotlin/DefaultSessionReducerTest.kt +++ b/testing/projections/src/test/kotlin/DefaultSessionReducerTest.kt @@ -241,4 +241,4 @@ class DefaultSessionReducerTest { SessionState( status = SessionStatus.PAUSED ) -} \ No newline at end of file +} diff --git a/testing/projections/src/test/kotlin/InferenceProjectorTest.kt b/testing/projections/src/test/kotlin/InferenceProjectorTest.kt index 17567996..423db118 100644 --- a/testing/projections/src/test/kotlin/InferenceProjectorTest.kt +++ b/testing/projections/src/test/kotlin/InferenceProjectorTest.kt @@ -114,4 +114,4 @@ class InferenceProjectorTest { assertEquals(InferenceStatus.COMPLETED, currentState.records.first().status) assertEquals(200, currentState.records.first().tokensUsed?.totalTokens) } -} \ No newline at end of file +} diff --git a/testing/projections/src/test/kotlin/InferenceReducerTest.kt b/testing/projections/src/test/kotlin/InferenceReducerTest.kt index 22fc3508..14b08b26 100644 --- a/testing/projections/src/test/kotlin/InferenceReducerTest.kt +++ b/testing/projections/src/test/kotlin/InferenceReducerTest.kt @@ -188,4 +188,4 @@ class InferenceReducerTest { assertEquals(1, newState.records.size) assertEquals(InferenceStatus.STARTED, newState.records.first().status) } -} \ No newline at end of file +} diff --git a/testing/projections/src/test/kotlin/OrchestrationProjectorTest.kt b/testing/projections/src/test/kotlin/OrchestrationProjectorTest.kt index 6e40ef16..d301368e 100644 --- a/testing/projections/src/test/kotlin/OrchestrationProjectorTest.kt +++ b/testing/projections/src/test/kotlin/OrchestrationProjectorTest.kt @@ -21,6 +21,7 @@ class OrchestrationProjectorTest { private val projector = OrchestrationProjector(reducer) private val sessionId = SessionId("s1") private val stageId = StageId("stage-1") + private val workflowId = "workflow-test" @Test fun `should initialize orchestration with IDLE status`() { @@ -32,7 +33,7 @@ class OrchestrationProjectorTest { fun `should set RUNNING on WorkflowStartedEvent`() { val initialState = projector.initial() val started = - projector.apply(initialState, stored(payload = WorkflowStartedEvent(sessionId, stageId))) + projector.apply(initialState, stored(payload = WorkflowStartedEvent(sessionId, workflowId, stageId))) assertEquals(OrchestrationStatus.RUNNING, started.status) } @@ -40,7 +41,7 @@ class OrchestrationProjectorTest { fun `should set RUNNING after resume on pause`() { val initialState = projector.initial() val started = - projector.apply(initialState, stored(payload = WorkflowStartedEvent(sessionId, stageId))) + projector.apply(initialState, stored(payload = WorkflowStartedEvent(sessionId, workflowId, stageId))) assertEquals(OrchestrationStatus.RUNNING, started.status) val paused = projector.apply(started, stored(payload = OrchestrationPausedEvent(sessionId, stageId, "Tool approval"))) @@ -56,7 +57,7 @@ class OrchestrationProjectorTest { fun `should set FAILED on WorkflowFailedEvent`() { val initialState = projector.initial() val started = - projector.apply(initialState, stored(payload = WorkflowStartedEvent(sessionId, stageId))) + projector.apply(initialState, stored(payload = WorkflowStartedEvent(sessionId, workflowId, stageId))) val failed = projector.apply( started, @@ -69,7 +70,7 @@ class OrchestrationProjectorTest { fun `same events produce same result`() { val initialState = projector.initial() val events = listOf( - stored(payload = WorkflowStartedEvent(sessionId, stageId)), + stored(payload = WorkflowStartedEvent(sessionId, workflowId, stageId)), stored(payload = WorkflowFailedEvent(sessionId, stageId, "Something went wrong", false)), stored( payload = RetryAttemptedEvent( @@ -80,7 +81,7 @@ class OrchestrationProjectorTest { failureReason = "Something went wrong", ), ), - stored(payload = WorkflowCompletedEvent(sessionId, stageId, 1)), + stored(payload = WorkflowCompletedEvent(sessionId, stageId, 1, workflowId)), ) val result1 = events.fold(initialState) { state, event -> @@ -96,11 +97,11 @@ class OrchestrationProjectorTest { @Test fun `full pause-resume lifecycle`() { val s0 = projector.initial() - val s1 = projector.apply(s0, stored(payload = WorkflowStartedEvent(sessionId, stageId))) + val s1 = projector.apply(s0, stored(payload = WorkflowStartedEvent(sessionId, workflowId, stageId))) val s2 = projector.apply(s1, stored(payload = OrchestrationPausedEvent(sessionId, stageId, "approval required"))) val s3 = projector.apply(s2, stored(payload = OrchestrationResumedEvent(sessionId, stageId))) - val s4 = projector.apply(s3, stored(payload = WorkflowCompletedEvent(sessionId, stageId, 1))) + val s4 = projector.apply(s3, stored(payload = WorkflowCompletedEvent(sessionId, stageId, 1, workflowId))) assertEquals(OrchestrationStatus.RUNNING, s1.status) assertEquals(OrchestrationStatus.PAUSED, s2.status) @@ -109,4 +110,4 @@ class OrchestrationProjectorTest { assertFalse(s3.pendingApproval) assertEquals(OrchestrationStatus.COMPLETED, s4.status) } -} \ No newline at end of file +} diff --git a/testing/projections/src/test/kotlin/OrchestrationReducerTest.kt b/testing/projections/src/test/kotlin/OrchestrationReducerTest.kt index 88f5680b..4e4023be 100644 --- a/testing/projections/src/test/kotlin/OrchestrationReducerTest.kt +++ b/testing/projections/src/test/kotlin/OrchestrationReducerTest.kt @@ -22,12 +22,13 @@ class OrchestrationReducerTest { private val sessionId = SessionId("s1") private val stageId = StageId("stage-1") private val state = OrchestrationState(stageId) + private val workflowId = "workflow-test" @Test fun `WorkflowStartedEvent sets status to RUNNING`() { val state = reducer.reduce( state, - stored(payload = WorkflowStartedEvent(sessionId, stageId)), + stored(payload = WorkflowStartedEvent(sessionId, workflowId, stageId)), ) assertEquals(OrchestrationStatus.RUNNING, state.status) assertEquals(stageId, state.currentStageId) @@ -37,7 +38,7 @@ class OrchestrationReducerTest { fun `WorkflowFailedEvent sets status to FAILED and failure reason`() { val started = reducer.reduce( state, - stored(payload = WorkflowStartedEvent(sessionId, stageId)), + stored(payload = WorkflowStartedEvent(sessionId, workflowId, stageId)), ) val failed = reducer.reduce( @@ -52,12 +53,12 @@ class OrchestrationReducerTest { fun `WorkflowCompletedEvent sets status to COMPLETED`() { val started = reducer.reduce( state, - stored(payload = WorkflowStartedEvent(sessionId, stageId)), + stored(payload = WorkflowStartedEvent(sessionId, workflowId, stageId)), ) val completed = reducer.reduce( started, - stored(payload = WorkflowCompletedEvent(sessionId, stageId, 1)), + stored(payload = WorkflowCompletedEvent(sessionId, stageId, 1, workflowId)), ) assertTrue(completed.failureReason.isNullOrBlank()) assertEquals(OrchestrationStatus.COMPLETED, completed.status) @@ -68,7 +69,7 @@ class OrchestrationReducerTest { fun `OrchestrationPausedEvent sets status to PAUSED with reason and pending approval`() { val started = reducer.reduce( state, - stored(payload = WorkflowStartedEvent(sessionId, stageId)), + stored(payload = WorkflowStartedEvent(sessionId, workflowId, stageId)), ) val paused = reducer.reduce( @@ -84,7 +85,7 @@ class OrchestrationReducerTest { fun `OrchestrationResumedEvent sets status to RUNNING with null reason and no pending approval`() { val started = reducer.reduce( state, - stored(payload = WorkflowStartedEvent(sessionId, stageId)), + stored(payload = WorkflowStartedEvent(sessionId, workflowId, stageId)), ) val paused = reducer.reduce( @@ -105,7 +106,7 @@ class OrchestrationReducerTest { fun `RetryAttemptedEvent sets status to RUNNING with null reason and no pending approval`() { val started = reducer.reduce( state, - stored(payload = WorkflowStartedEvent(sessionId, stageId)), + stored(payload = WorkflowStartedEvent(sessionId, workflowId, stageId)), ) val failed = reducer.reduce( @@ -137,10 +138,10 @@ class OrchestrationReducerTest { fun `unrelated event does nothing`() { val started = reducer.reduce( state, - stored(payload = WorkflowStartedEvent(sessionId, stageId)), + stored(payload = WorkflowStartedEvent(sessionId, workflowId, stageId)), ) val unrelated = reducer.reduce(started, stored()) assertEquals(started, unrelated) } -} \ No newline at end of file +} diff --git a/testing/projections/src/test/kotlin/RouterProjectorTest.kt b/testing/projections/src/test/kotlin/RouterProjectorTest.kt index 90b0a667..44aae4ae 100644 --- a/testing/projections/src/test/kotlin/RouterProjectorTest.kt +++ b/testing/projections/src/test/kotlin/RouterProjectorTest.kt @@ -1,17 +1,17 @@ import com.correx.core.events.events.OrchestrationPausedEvent import com.correx.core.events.events.OrchestrationResumedEvent -import com.correx.core.events.events.SteeringNoteAddedEvent import com.correx.core.events.events.StageCompletedEvent import com.correx.core.events.events.StageFailedEvent +import com.correx.core.events.events.SteeringNoteAddedEvent import com.correx.core.events.events.WorkflowCompletedEvent import com.correx.core.events.events.WorkflowFailedEvent import com.correx.core.events.events.WorkflowStartedEvent +import com.correx.core.events.types.SessionId +import com.correx.core.events.types.StageId import com.correx.core.router.DefaultRouterReducer import com.correx.core.router.RouterProjector import com.correx.core.router.model.StageOutcomeKind import com.correx.core.router.model.WorkflowStatus -import com.correx.core.events.types.SessionId -import com.correx.core.events.types.StageId import com.correx.testing.fixtures.EventFixtures.stored import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertNull @@ -23,6 +23,7 @@ class RouterProjectorTest { private val projector = RouterProjector(reducer) private val sessionId = SessionId("s1") private val stageId = StageId("stage-1") + private val workflowId = "workflow-test" @Test fun `initial state has IDLE status and empty collections`() { @@ -37,7 +38,7 @@ class RouterProjectorTest { fun `should set RUNNING on WorkflowStartedEvent`() { val initialState = projector.initial() val started = - projector.apply(initialState, stored(payload = WorkflowStartedEvent(sessionId, stageId))) + projector.apply(initialState, stored(payload = WorkflowStartedEvent(sessionId, workflowId, stageId))) assertEquals(WorkflowStatus.RUNNING, started.workflowStatus) assertEquals(stageId, started.currentStageId) } @@ -46,9 +47,9 @@ class RouterProjectorTest { fun `should set COMPLETED on WorkflowCompletedEvent`() { val initialState = projector.initial() val started = - projector.apply(initialState, stored(payload = WorkflowStartedEvent(sessionId, stageId))) + projector.apply(initialState, stored(payload = WorkflowStartedEvent(sessionId, workflowId, stageId))) val completed = - projector.apply(started, stored(payload = WorkflowCompletedEvent(sessionId, stageId, 1))) + projector.apply(started, stored(payload = WorkflowCompletedEvent(sessionId, stageId, 1, workflowId))) assertEquals(WorkflowStatus.COMPLETED, completed.workflowStatus) assertNull(completed.currentStageId) } @@ -57,7 +58,7 @@ class RouterProjectorTest { fun `should set FAILED on WorkflowFailedEvent`() { val initialState = projector.initial() val started = - projector.apply(initialState, stored(payload = WorkflowStartedEvent(sessionId, stageId))) + projector.apply(initialState, stored(payload = WorkflowStartedEvent(sessionId, workflowId, stageId))) val failed = projector.apply( started, @@ -71,7 +72,7 @@ class RouterProjectorTest { fun `should set PAUSED on OrchestrationPausedEvent`() { val initialState = projector.initial() val started = - projector.apply(initialState, stored(payload = WorkflowStartedEvent(sessionId, stageId))) + projector.apply(initialState, stored(payload = WorkflowStartedEvent(sessionId, workflowId, stageId))) assertEquals(WorkflowStatus.RUNNING, started.workflowStatus) val paused = projector.apply(started, stored(payload = OrchestrationPausedEvent(sessionId, stageId, "Tool approval"))) @@ -82,7 +83,7 @@ class RouterProjectorTest { fun `should set RUNNING after resume on pause`() { val initialState = projector.initial() val started = - projector.apply(initialState, stored(payload = WorkflowStartedEvent(sessionId, stageId))) + projector.apply(initialState, stored(payload = WorkflowStartedEvent(sessionId, workflowId, stageId))) val paused = projector.apply(started, stored(payload = OrchestrationPausedEvent(sessionId, stageId, "Tool approval"))) assertEquals(WorkflowStatus.PAUSED, paused.workflowStatus) @@ -105,7 +106,7 @@ class RouterProjectorTest { fun `should append l2Memory entry and clear currentStageId on StageFailedEvent`() { val initialState = projector.initial() val started = - projector.apply(initialState, stored(payload = WorkflowStartedEvent(sessionId, stageId))) + projector.apply(initialState, stored(payload = WorkflowStartedEvent(sessionId, workflowId, stageId))) assertEquals(stageId, started.currentStageId) val failed = projector.apply(started, stored(payload = StageFailedEvent(sessionId, stageId, StageId("t1"), "timeout"))) @@ -129,11 +130,11 @@ class RouterProjectorTest { fun `same events produce same result (determinism)`() { val initialState = projector.initial() val events = listOf( - stored(payload = WorkflowStartedEvent(sessionId, stageId)), + stored(payload = WorkflowStartedEvent(sessionId, workflowId, stageId)), stored(payload = StageCompletedEvent(sessionId, StageId("stage-2"), StageId("t1"))), stored(payload = SteeringNoteAddedEvent(sessionId, "note")), stored(payload = StageFailedEvent(sessionId, stageId, StageId("t2"), "timeout")), - stored(payload = WorkflowCompletedEvent(sessionId, stageId, 2)), + stored(payload = WorkflowCompletedEvent(sessionId, stageId, 2, workflowId)), ) val result1 = events.fold(initialState) { state, event -> @@ -149,11 +150,12 @@ class RouterProjectorTest { @Test fun `full lifecycle stage complete then stage fail then resume then complete`() { val s0 = projector.initial() - val s1 = projector.apply(s0, stored(payload = WorkflowStartedEvent(sessionId, stageId))) - val s2 = projector.apply(s1, stored(payload = StageCompletedEvent(sessionId, StageId("stage-2"), StageId("t1")))) + val s1 = projector.apply(s0, stored(payload = WorkflowStartedEvent(sessionId, workflowId, stageId))) + val s2 = + projector.apply(s1, stored(payload = StageCompletedEvent(sessionId, StageId("stage-2"), StageId("t1")))) val s3 = projector.apply(s2, stored(payload = StageFailedEvent(sessionId, stageId, StageId("t2"), "timeout"))) - val s4 = projector.apply(s3, stored(payload = WorkflowStartedEvent(sessionId, StageId("stage-3")))) - val s5 = projector.apply(s4, stored(payload = WorkflowCompletedEvent(sessionId, stageId, 1))) + val s4 = projector.apply(s3, stored(payload = WorkflowStartedEvent(sessionId, workflowId, StageId("stage-3")))) + val s5 = projector.apply(s4, stored(payload = WorkflowCompletedEvent(sessionId, stageId, 1, workflowId))) assertEquals(WorkflowStatus.RUNNING, s1.workflowStatus) assertEquals(StageId("stage-2"), s2.l2Memory[0].stageId) @@ -169,7 +171,8 @@ class RouterProjectorTest { @Test fun `unknown event type returns unchanged state`() { val initialState = projector.initial() - val updated = projector.apply(initialState, stored(payload = com.correx.core.events.events.ToolInvokedEvent("test"))) + val updated = + projector.apply(initialState, stored(payload = com.correx.core.events.events.ToolInvokedEvent("test"))) assertEquals(initialState, updated) } } diff --git a/testing/projections/src/test/kotlin/SessionProjectorTest.kt b/testing/projections/src/test/kotlin/SessionProjectorTest.kt index dc55a55c..7ccab89b 100644 --- a/testing/projections/src/test/kotlin/SessionProjectorTest.kt +++ b/testing/projections/src/test/kotlin/SessionProjectorTest.kt @@ -141,4 +141,4 @@ class SessionProjectorTest { assertEquals(state1, state2) } -} \ No newline at end of file +} diff --git a/testing/replay/src/test/kotlin/ApprovalReplayTest.kt b/testing/replay/src/test/kotlin/ApprovalReplayTest.kt index 0720ed80..c8107b9b 100644 --- a/testing/replay/src/test/kotlin/ApprovalReplayTest.kt +++ b/testing/replay/src/test/kotlin/ApprovalReplayTest.kt @@ -56,4 +56,4 @@ class ApprovalReplayTest { assertEquals(d1.id, d2.id) assertEquals(ApprovalDecisionId("decision:fixed"), d1.id) } -} \ No newline at end of file +} diff --git a/testing/replay/src/test/kotlin/SessionEmptyReplayTest.kt b/testing/replay/src/test/kotlin/SessionEmptyReplayTest.kt index 3ec48951..b70ef465 100644 --- a/testing/replay/src/test/kotlin/SessionEmptyReplayTest.kt +++ b/testing/replay/src/test/kotlin/SessionEmptyReplayTest.kt @@ -21,4 +21,4 @@ class SessionEmptyReplayTest { assertEquals(SessionStatus.CREATED, state.status) } -} \ No newline at end of file +} diff --git a/testing/replay/src/test/kotlin/SessionReplayTest.kt b/testing/replay/src/test/kotlin/SessionReplayTest.kt index 54667f0d..b2b7d6b7 100644 --- a/testing/replay/src/test/kotlin/SessionReplayTest.kt +++ b/testing/replay/src/test/kotlin/SessionReplayTest.kt @@ -54,4 +54,4 @@ class SessionReplayTest { assertEquals(SessionStatus.COMPLETED, state.status) } -} \ No newline at end of file +} diff --git a/testing/replay/src/test/kotlin/TransitionReplayIntegrationTest.kt b/testing/replay/src/test/kotlin/TransitionReplayIntegrationTest.kt index 66f0b903..e0f424a8 100644 --- a/testing/replay/src/test/kotlin/TransitionReplayIntegrationTest.kt +++ b/testing/replay/src/test/kotlin/TransitionReplayIntegrationTest.kt @@ -207,4 +207,4 @@ class TransitionReplayIntegrationTest { assertEquals(state1, state2) } -} \ No newline at end of file +} diff --git a/testing/replay/src/test/kotlin/ValidationReplayTest.kt b/testing/replay/src/test/kotlin/ValidationReplayTest.kt index f8a7109d..8681d4c8 100644 --- a/testing/replay/src/test/kotlin/ValidationReplayTest.kt +++ b/testing/replay/src/test/kotlin/ValidationReplayTest.kt @@ -30,4 +30,4 @@ class ValidationReplayTest { assertTrue(results.distinct().size == 1) } -} \ No newline at end of file +} diff --git a/testing/transitions/src/test/kotlin/DefaultTransitionResolverTest.kt b/testing/transitions/src/test/kotlin/DefaultTransitionResolverTest.kt index b2150bcc..d8240d42 100644 --- a/testing/transitions/src/test/kotlin/DefaultTransitionResolverTest.kt +++ b/testing/transitions/src/test/kotlin/DefaultTransitionResolverTest.kt @@ -19,7 +19,7 @@ class DefaultTransitionResolverTest { private val evaluator = object : TransitionConditionEvaluator { override fun evaluate( condition: TransitionCondition, - context: EvaluationContext + context: EvaluationContext, ): Boolean { return condition.evaluate(context) } @@ -36,20 +36,20 @@ class DefaultTransitionResolverTest { id = "t1", from = "stage-a", to = "stage-b", - condition = alwaysTrue() - ) + condition = alwaysTrue(), + ), ), - stages = setOf("stage-a", "stage-b") + stages = setOf("stage-a", "stage-b"), ) val result = resolver.resolve( graph = graph, - context = context(currentStage = "stage-b") + context = context(currentStage = "stage-b"), ) assertEquals( TransitionDecision.NoMatch, - result + result, ) } @@ -62,19 +62,19 @@ class DefaultTransitionResolverTest { id = "t1", from = "stage-a", to = "stage-b", - condition = alwaysFalse() - ) - ) + condition = alwaysFalse(), + ), + ), ) val result = resolver.resolve( graph = graph, - context = context(currentStage = "stage-a") + context = context(currentStage = "stage-a"), ) assertEquals( TransitionDecision.Stay, - result + result, ) } @@ -87,26 +87,26 @@ class DefaultTransitionResolverTest { id = "t1", from = "stage-a", to = "stage-b", - condition = alwaysTrue() - ) - ) + condition = alwaysTrue(), + ), + ), ) val result = resolver.resolve( graph = graph, - context = context(currentStage = "stage-a") + context = context(currentStage = "stage-a"), ) val move = assertInstanceOf(result) assertEquals( TransitionId("t1"), - move.transitionId + move.transitionId, ) assertEquals( StageId("stage-b"), - move.to + move.to, ) } @@ -119,32 +119,32 @@ class DefaultTransitionResolverTest { id = "t2", from = "stage-a", to = "stage-c", - condition = alwaysTrue() + condition = alwaysTrue(), ), edge( id = "t1", from = "stage-a", to = "stage-b", - condition = alwaysTrue() - ) - ) + condition = alwaysTrue(), + ), + ), ) val result = resolver.resolve( graph = graph, - context = context(currentStage = "stage-a") + context = context(currentStage = "stage-a"), ) val move = assertInstanceOf(result) assertEquals( TransitionId("t1"), - move.transitionId + move.transitionId, ) assertEquals( StageId("stage-b"), - move.to + move.to, ) } @@ -157,27 +157,27 @@ class DefaultTransitionResolverTest { id = "t1", from = "stage-x", to = "stage-y", - condition = alwaysTrue() + condition = alwaysTrue(), ), edge( id = "t2", from = "stage-a", to = "stage-b", - condition = alwaysTrue() - ) - ) + condition = alwaysTrue(), + ), + ), ) val result = resolver.resolve( graph = graph, - context = context(currentStage = "stage-a") + context = context(currentStage = "stage-a"), ) val move = assertInstanceOf(result) assertEquals( TransitionId("t2"), - move.transitionId + move.transitionId, ) } @@ -185,24 +185,25 @@ class DefaultTransitionResolverTest { id: String, from: String, to: String, - condition: TransitionCondition + condition: TransitionCondition, ): TransitionEdge { return TransitionEdge( id = TransitionId(id), from = StageId(from), to = StageId(to), - condition = condition + condition = condition, ) } private fun graph( start: String, transitions: Set, - stages: Set = transitions.flatMap { listOf(it.from.value, it.to.value) }.toSet() + stages: Set = transitions.flatMap { listOf(it.from.value, it.to.value) }.toSet(), ) = WorkflowGraph( + id = "workflow-test", start = StageId(start), stages = stages.associate { StageId(it) to StageConfig() }, - transitions = transitions + transitions = transitions, ) private fun context( @@ -222,4 +223,4 @@ class DefaultTransitionResolverTest { @Suppress("UnusedPrivateMember") private fun alwaysFalse() = TransitionCondition { false } -} \ No newline at end of file +} diff --git a/testing/transitions/src/test/kotlin/TransitionEventSerializationTest.kt b/testing/transitions/src/test/kotlin/TransitionEventSerializationTest.kt index ce9e9e86..69923d9d 100644 --- a/testing/transitions/src/test/kotlin/TransitionEventSerializationTest.kt +++ b/testing/transitions/src/test/kotlin/TransitionEventSerializationTest.kt @@ -69,4 +69,4 @@ class TransitionEventSerializationTest { assertEquals(event, deserialized) } -} \ No newline at end of file +}