From f827685ed0009a53d9effaaab604029cae46320e Mon Sep 17 00:00:00 2001 From: kami Date: Fri, 22 May 2026 00:10:05 +0400 Subject: [PATCH] chore(damn): detekt, build, tests, formatting fixed detekt issues where possible. fixed disttar failing build because tools is added twice in the server module. added workflowId where required. fixed some tests not being recognized because of runBlocking without explicit return type. formatting + imports. --- apps/server/build.gradle | 7 + .../com/correx/apps/server/Application.kt | 2 +- .../kotlin/com/correx/apps/server/Main.kt | 62 +-- .../com/correx/apps/server/ServerModule.kt | 2 +- .../server/approval/ApprovalCoordinator.kt | 4 +- .../apps/server/bridge/DomainEventMapper.kt | 57 ++- .../server/protocol/ProtocolSerializer.kt | 2 +- .../apps/server/protocol/ServerMessage.kt | 17 +- .../apps/server/registry/ProviderRegistry.kt | 2 +- .../apps/server/routes/SessionRoutes.kt | 4 +- .../apps/server/ws/GlobalStreamHandler.kt | 2 +- .../apps/server/ws/SessionStreamHandler.kt | 39 +- .../server/bridge/DomainEventMapperTest.kt | 148 +++--- .../server/bridge/SessionEventBridgeTest.kt | 21 +- .../main/kotlin/com/correx/apps/tui/TuiApp.kt | 4 +- .../correx/apps/tui/components/InputBar.kt | 119 +++-- .../correx/apps/tui/components/RouterPanel.kt | 1 - .../correx/apps/tui/components/SessionList.kt | 6 +- .../correx/apps/tui/input/TambouiKeyMapper.kt | 23 +- .../correx/apps/tui/reducer/RootReducer.kt | 2 + .../apps/tui/reducer/SessionsReducer.kt | 436 ++++++++++-------- .../apps/tui/input/TambouiKeyMapperTest.kt | 61 ++- .../apps/tui/reducer/RootReducerTest.kt | 6 +- .../apps/tui/reducer/SessionsReducerTest.kt | 22 +- .../core/events/events/EventEnvelope.kt | 2 +- .../core/events/events/EventMetadata.kt | 2 +- .../correx/core/events/events/EventPayload.kt | 2 +- .../core/events/events/InferenceEvents.kt | 2 +- .../core/events/events/OrchestrationEvents.kt | 4 +- .../events/serialization/EventSerializer.kt | 2 +- .../serialization/JsonEventSerializer.kt | 2 +- .../events/serialization/Serialization.kt | 4 +- .../correx/core/events/stores/EventStore.kt | 2 +- .../correx/core/events/types/IdentityTypes.kt | 2 +- .../com/correx/core/inference/TokenUsage.kt | 2 +- .../projections/DefaultStateBuilder.kt | 2 +- .../core/sessions/projections/Projection.kt | 2 +- .../core/sessions/projections/StateBuilder.kt | 2 +- .../replay/DefaultEventReplayer.kt | 2 +- .../projections/replay/EventReplayer.kt | 2 +- .../kotlin/com/correx/core/utils/TypeId.kt | 2 +- .../core/inference/DefaultInferenceReducer.kt | 2 +- .../com/correx/core/inference/FinishReason.kt | 2 +- .../correx/core/inference/GenerationConfig.kt | 2 +- .../inference/InferenceCancellationToken.kt | 2 +- .../core/inference/InferenceProjector.kt | 2 +- .../core/inference/InferenceProvider.kt | 2 +- .../correx/core/inference/InferenceRecord.kt | 2 +- .../correx/core/inference/InferenceReducer.kt | 2 +- .../core/inference/InferenceRepository.kt | 2 +- .../correx/core/inference/InferenceRequest.kt | 2 +- .../core/inference/InferenceResponse.kt | 2 +- .../correx/core/inference/InferenceRouter.kt | 2 +- .../correx/core/inference/InferenceState.kt | 2 +- .../correx/core/inference/InferenceStatus.kt | 2 +- .../correx/core/inference/ProviderHealth.kt | 2 +- .../com/correx/core/inference/Tokenizer.kt | 2 +- .../correx/core/inference/ToolCallRequest.kt | 2 +- .../core/kernel/execution/ReplayStrategy.kt | 2 +- .../core/kernel/execution/StageOutcome.kt | 2 +- .../core/kernel/execution/WorkflowResult.kt | 2 +- .../DefaultOrchestrationReducer.kt | 2 +- .../DefaultSessionOrchestrator.kt | 15 +- .../orchestration/OrchestrationConfig.kt | 2 +- .../orchestration/OrchestrationProjector.kt | 2 +- .../orchestration/OrchestrationReducer.kt | 2 +- .../orchestration/OrchestrationRepository.kt | 2 +- .../orchestration/ReplayOrchestrator.kt | 8 +- .../orchestration/SessionOrchestrator.kt | 30 +- .../core/router/RouterContextBuilder.kt | 2 +- .../com/correx/core/router/RouterFacade.kt | 6 +- .../com/correx/core/router/RouterReducer.kt | 2 +- .../com/correx/core/sessions/ApprovalMode.kt | 2 +- .../core/sessions/DefaultSessionReducer.kt | 2 +- .../core/sessions/DefaultSessionRepository.kt | 2 +- .../core/sessions/SessionCounterProjection.kt | 2 +- .../core/sessions/SessionCounterState.kt | 2 +- .../correx/core/sessions/SessionProjector.kt | 2 +- .../correx/core/sessions/SessionReducer.kt | 2 +- .../com/correx/core/sessions/SessionStatus.kt | 2 +- .../correx/core/sessions/TransitionResult.kt | 2 +- .../SessionCounterProjectionTest.kt | 2 +- .../transitions/analysis/CycleExtractor.kt | 2 +- .../transitions/analysis/DetectedCycle.kt | 2 +- .../analysis/DeterministicAdjacencyBuilder.kt | 2 +- .../canonicalization/CycleCanonicalizer.kt | 2 +- .../evaluation/EvaluationContext.kt | 2 +- .../TransitionConditionEvaluator.kt | 2 +- .../execution/StageExecutionResult.kt | 2 +- .../transitions/execution/StageExecutor.kt | 2 +- .../core/transitions/graph/GraphOrdering.kt | 2 +- .../transitions/graph/TransitionCondition.kt | 2 +- .../core/transitions/graph/TransitionEdge.kt | 2 +- .../core/transitions/graph/WorkflowGraph.kt | 1 + .../DefaultStageExecutionEventMapper.kt | 2 +- .../mapper/StageExecutionEventMapper.kt | 2 +- .../core/transitions/policy/CyclePolicy.kt | 2 +- .../transitions/policy/CyclePolicyBinding.kt | 2 +- .../transitions/policy/CyclePolicyResolver.kt | 2 +- .../transitions/policy/PolicyValidation.kt | 2 +- .../resolution/DefaultTransitionResolver.kt | 2 +- .../resolution/TransitionDecision.kt | 2 +- .../resolution/TransitionOrdering.kt | 2 +- .../resolution/TransitionResolver.kt | 2 +- .../core/transitions/state/VisitState.kt | 2 +- .../validation/approval/ApprovalRequest.kt | 2 +- .../validation/approval/ApprovalTrigger.kt | 2 +- .../core/validation/approval/RiskSummary.kt | 2 +- .../artifact/ArtifactPayloadValidator.kt | 2 +- .../core/validation/graph/GraphValidator.kt | 2 +- .../validation/model/ValidationLocation.kt | 2 +- .../core/validation/model/ValidationReport.kt | 2 +- .../validation/model/ValidationSeverity.kt | 2 +- .../validation/pipeline/ValidationPipeline.kt | 2 +- .../core/validation/pipeline/Validator.kt | 2 +- .../core/validation/semantic/SemanticRule.kt | 2 +- .../validation/semantic/SemanticValidator.kt | 2 +- .../semantic/rules/CyclePolicyBindingRule.kt | 2 +- .../validation/session/SessionValidator.kt | 2 +- .../transition/TransitionValidator.kt | 2 +- docs/specs/2026-05-17-tamboui-migration.md | 2 +- .../artifactscas/CasArtifactStore.kt | 2 +- .../artifactscas/segment/SegmentLayout.kt | 2 +- .../artifactscas/compact/CompactorTest.kt | 2 +- .../artifactscas/evict/EvictorTest.kt | 2 +- .../persistence/InMemoryEventStore.kt | 2 +- .../persistence/SqliteEventStore.kt | 4 +- .../artifact/LiveArtifactRepository.kt | 2 +- .../persistence/util/JDBCHelper.kt | 2 +- .../persistence/InMemoryEventStoreTest.kt | 2 +- .../persistence/InMemoryReplayTest.kt | 2 +- .../persistence/SqliteEventStoreTest.kt | 2 +- .../persistence/SqliteReplayTest.kt | 2 +- .../infrastructure/InfrastructureModule.kt | 30 +- .../tools/filesystem/FileReadTool.kt | 2 +- .../tools/filesystem/FileWriteTool.kt | 6 +- .../tools/filesystem/FileEditToolTest.kt | 18 +- .../tools/filesystem/FileReadToolTest.kt | 16 +- .../tools/filesystem/FileWriteToolTest.kt | 27 +- .../tools/SandboxedToolExecutor.kt | 1 - .../correx/infrastructure/tools/ToolConfig.kt | 54 ++- .../tools/shell/ShellToolTest.kt | 14 +- .../workflow/TomlWorkflowLoader.kt | 16 +- .../workflow/FileSystemPromptLoaderTest.kt | 2 +- .../src/test/kotlin/ApprovalTriggerTest.kt | 2 +- .../src/test/kotlin/GrantSemanticsTest.kt | 2 +- .../src/test/kotlin/TierImmutabilityTest.kt | 2 +- .../contracts/utils/ConcurrencyRunner.kt | 2 +- .../contracts/utils/DeterministicBarrier.kt | 2 +- .../testing/contracts/model/StageIdTest.kt | 2 +- .../contracts/model/TransitionIdTest.kt | 2 +- .../model/ValidationReportContractTest.kt | 2 +- .../contracts/model/WorkflowGraphTest.kt | 4 + .../model/orchestration/EventsTest.kt | 7 +- .../src/test/kotlin/GraphValidatorTest.kt | 1 + .../test/kotlin/RouterContextBuilderTest.kt | 21 +- .../src/test/kotlin/RouterFacadeTest.kt | 125 +++-- .../src/test/kotlin/RouterProjectorTest.kt | 27 +- .../src/test/kotlin/RouterReducerTest.kt | 35 +- .../kotlin/SessionReplayDeterminismTest.kt | 2 +- .../correx/testing/fixtures/EventFixtures.kt | 2 +- .../testing/fixtures/InferenceFixtures.kt | 2 +- .../testing/fixtures/WorkflowFixtures.kt | 1 + .../inference/MockInferenceProvider.kt | 2 +- .../fixtures/inference/MockTokenizer.kt | 2 +- .../transitions/TransitionFixtures.kt | 3 +- .../SessionOrchestratorIntegrationTest.kt | 34 +- .../ValidationPipelineIntegrationTest.kt | 3 +- .../testing/kernel/MockEventReplayer.kt | 2 +- .../kotlin/DefaultRetryCoordinatorTest.kt | 2 +- .../kotlin/ReplayInferenceProviderTest.kt | 2 +- .../test/kotlin/DefaultSessionReducerTest.kt | 2 +- .../src/test/kotlin/InferenceProjectorTest.kt | 2 +- .../src/test/kotlin/InferenceReducerTest.kt | 2 +- .../test/kotlin/OrchestrationProjectorTest.kt | 17 +- .../test/kotlin/OrchestrationReducerTest.kt | 19 +- .../src/test/kotlin/RouterProjectorTest.kt | 37 +- .../src/test/kotlin/SessionProjectorTest.kt | 2 +- .../src/test/kotlin/ApprovalReplayTest.kt | 2 +- .../src/test/kotlin/SessionEmptyReplayTest.kt | 2 +- .../src/test/kotlin/SessionReplayTest.kt | 2 +- .../kotlin/TransitionReplayIntegrationTest.kt | 2 +- .../src/test/kotlin/ValidationReplayTest.kt | 2 +- .../kotlin/DefaultTransitionResolverTest.kt | 71 +-- .../TransitionEventSerializationTest.kt | 2 +- 185 files changed, 1134 insertions(+), 832 deletions(-) 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 +}