chore(damn): detekt, build, tests, formatting
fixed detekt issues where possible. fixed disttar failing build because tools is added twice in the server module. added workflowId where required. fixed some tests not being recognized because of runBlocking without explicit return type. formatting + imports.
This commit is contained in:
@@ -8,6 +8,13 @@ application {
|
|||||||
mainClass = 'com.correx.apps.server.MainKt'
|
mainClass = 'com.correx.apps.server.MainKt'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tasks.named('distTar') {
|
||||||
|
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
|
||||||
|
}
|
||||||
|
tasks.named('distZip') {
|
||||||
|
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
|
||||||
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation project(':core:config')
|
implementation project(':core:config')
|
||||||
implementation project(':core:events')
|
implementation project(':core:events')
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import io.ktor.serialization.kotlinx.json.json
|
|||||||
import io.ktor.server.application.Application
|
import io.ktor.server.application.Application
|
||||||
import io.ktor.server.application.install
|
import io.ktor.server.application.install
|
||||||
import io.ktor.server.plugins.calllogging.CallLogging
|
import io.ktor.server.plugins.calllogging.CallLogging
|
||||||
import org.slf4j.event.Level
|
|
||||||
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
|
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
|
||||||
import io.ktor.server.plugins.statuspages.StatusPages
|
import io.ktor.server.plugins.statuspages.StatusPages
|
||||||
import io.ktor.server.response.respond
|
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.WebSockets
|
||||||
import io.ktor.server.websocket.webSocket
|
import io.ktor.server.websocket.webSocket
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
import org.slf4j.event.Level
|
||||||
|
|
||||||
fun Application.configureServer(module: ServerModule) {
|
fun Application.configureServer(module: ServerModule) {
|
||||||
install(CallLogging) { level = Level.INFO }
|
install(CallLogging) { level = Level.INFO }
|
||||||
|
|||||||
@@ -49,9 +49,20 @@ import java.nio.file.Paths
|
|||||||
private val log = LoggerFactory.getLogger("com.correx.apps.server.Main")
|
private val log = LoggerFactory.getLogger("com.correx.apps.server.Main")
|
||||||
|
|
||||||
fun main() {
|
fun main() {
|
||||||
|
log.info("=== correx server starting ===")
|
||||||
|
log.info(" port : 8080")
|
||||||
val artifactStore = InfrastructureModule.createArtifactStore()
|
val artifactStore = InfrastructureModule.createArtifactStore()
|
||||||
val eventStore = LoggingEventStore(InfrastructureModule.createEventStore(artifactStore))
|
val eventStore = LoggingEventStore(InfrastructureModule.createEventStore(artifactStore))
|
||||||
|
|
||||||
|
logStoresInfo(eventStore, artifactStore)
|
||||||
|
|
||||||
val infraRegistry = InfrastructureModule.createProviderRegistry(listOf(buildLlamaProvider()))
|
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 repositories = buildRepositories(eventStore)
|
||||||
val approvalEngine = DefaultApprovalEngine()
|
val approvalEngine = DefaultApprovalEngine()
|
||||||
val correxConfig = ConfigLoader.load()
|
val correxConfig = ConfigLoader.load()
|
||||||
@@ -78,6 +89,9 @@ fun main() {
|
|||||||
eventDispatcher = EventDispatcher(eventStore),
|
eventDispatcher = EventDispatcher(eventStore),
|
||||||
workDir = sandboxRoot,
|
workDir = sandboxRoot,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
logToolInfo(sandboxRoot, workingDir, shellAllowedExecutables, toolRegistry)
|
||||||
|
|
||||||
val inferenceRouter = DefaultInferenceRouter(infraRegistry, FirstAvailableRoutingStrategy())
|
val inferenceRouter = DefaultInferenceRouter(infraRegistry, FirstAvailableRoutingStrategy())
|
||||||
val engines = OrchestratorEngines(
|
val engines = OrchestratorEngines(
|
||||||
transitionResolver = DefaultTransitionResolver { condition, ctx -> condition.evaluate(ctx) },
|
transitionResolver = DefaultTransitionResolver { condition, ctx -> condition.evaluate(ctx) },
|
||||||
@@ -116,52 +130,46 @@ fun main() {
|
|||||||
defaultOrchestrationConfig = defaultOrchestrationConfig,
|
defaultOrchestrationConfig = defaultOrchestrationConfig,
|
||||||
routerFacade = routerFacade,
|
routerFacade = routerFacade,
|
||||||
)
|
)
|
||||||
val modelId = System.getenv("CORREX_MODEL_ID") ?: "default"
|
log.info("==============================")
|
||||||
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,
|
|
||||||
)
|
|
||||||
|
|
||||||
embeddedServer(Netty, port = 8080) { configureServer(module) }.start(wait = true)
|
embeddedServer(Netty, port = 8080) { configureServer(module) }.start(wait = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun logServerStartup(
|
private fun logModelInfo(
|
||||||
modelId: String,
|
modelId: String,
|
||||||
modelPath: String,
|
modelPath: String,
|
||||||
llamaUrl: String,
|
llamaUrl: String,
|
||||||
sandboxRoot: Path?,
|
|
||||||
workingDir: Path?,
|
|
||||||
shellAllowedExecutables: Set<String>,
|
|
||||||
eventStore: LoggingEventStore,
|
|
||||||
artifactStore: ArtifactStore,
|
|
||||||
toolRegistry: ToolRegistry,
|
|
||||||
infraRegistry: DefaultProviderRegistry,
|
infraRegistry: DefaultProviderRegistry,
|
||||||
) {
|
) {
|
||||||
log.info("=== correx server starting ===")
|
log.info("==========MODEL INFO==========")
|
||||||
log.info(" port : 8080")
|
|
||||||
log.info(" model id : {}", modelId)
|
log.info(" model id : {}", modelId)
|
||||||
log.info(" model path : {}", modelPath)
|
log.info(" model path : {}", modelPath)
|
||||||
log.info(" llama url : {}", llamaUrl)
|
log.info(" llama url : {}", llamaUrl)
|
||||||
|
log.info(" providers : {} registered", infraRegistry.listAll().size)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun logToolInfo(
|
||||||
|
sandboxRoot: Path?,
|
||||||
|
workingDir: Path?,
|
||||||
|
shellAllowedExecutables: Set<String>,
|
||||||
|
toolRegistry: ToolRegistry,
|
||||||
|
) {
|
||||||
|
log.info("==========TOOLS INFO==========")
|
||||||
log.info(" sandbox root : {}", sandboxRoot)
|
log.info(" sandbox root : {}", sandboxRoot)
|
||||||
log.info(" working dir : {}", workingDir)
|
log.info(" working dir : {}", workingDir)
|
||||||
if (shellAllowedExecutables.isEmpty()) {
|
if (shellAllowedExecutables.isEmpty()) {
|
||||||
log.warn(" shell tool : no executable allowlist configured — all executables allowed")
|
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(" event store : {}", eventStore::class.simpleName)
|
||||||
log.info(" artifact store: {}", artifactStore::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(
|
private fun buildLlamaProvider(): LlamaCppInferenceProvider = InfrastructureModule.createLlamaCppProvider(
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import com.correx.core.artifactstore.ArtifactStore
|
|||||||
import com.correx.core.events.stores.EventStore
|
import com.correx.core.events.stores.EventStore
|
||||||
import com.correx.core.kernel.orchestration.DefaultSessionOrchestrator
|
import com.correx.core.kernel.orchestration.DefaultSessionOrchestrator
|
||||||
import com.correx.core.kernel.orchestration.OrchestrationConfig
|
import com.correx.core.kernel.orchestration.OrchestrationConfig
|
||||||
import com.correx.core.sessions.DefaultSessionRepository
|
|
||||||
import com.correx.core.router.RouterFacade
|
import com.correx.core.router.RouterFacade
|
||||||
|
import com.correx.core.sessions.DefaultSessionRepository
|
||||||
|
|
||||||
data class ServerModule(
|
data class ServerModule(
|
||||||
val orchestrator: DefaultSessionOrchestrator,
|
val orchestrator: DefaultSessionOrchestrator,
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import com.correx.core.approvals.ApprovalStatus
|
|||||||
import com.correx.core.approvals.Tier
|
import com.correx.core.approvals.Tier
|
||||||
import com.correx.core.approvals.UserSteering
|
import com.correx.core.approvals.UserSteering
|
||||||
import com.correx.core.approvals.model.ApprovalContext
|
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.model.ApprovalScopeIdentity
|
||||||
import com.correx.core.events.events.ApprovalRequestedEvent
|
import com.correx.core.events.events.ApprovalRequestedEvent
|
||||||
import com.correx.core.events.types.ApprovalRequestId
|
import com.correx.core.events.types.ApprovalRequestId
|
||||||
@@ -25,7 +24,8 @@ import kotlinx.coroutines.Job
|
|||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.datetime.Clock
|
import kotlinx.datetime.Clock
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.*
|
||||||
|
import com.correx.core.approvals.model.ApprovalDecision as DomainApprovalDecision
|
||||||
|
|
||||||
class ApprovalCoordinator(
|
class ApprovalCoordinator(
|
||||||
private val orchestrator: ApprovalGateway,
|
private val orchestrator: ApprovalGateway,
|
||||||
|
|||||||
@@ -5,13 +5,13 @@ import com.correx.apps.server.protocol.RiskSummaryDto
|
|||||||
import com.correx.apps.server.protocol.ServerMessage
|
import com.correx.apps.server.protocol.ServerMessage
|
||||||
import com.correx.core.artifactstore.ArtifactStore
|
import com.correx.core.artifactstore.ArtifactStore
|
||||||
import com.correx.core.events.events.ApprovalRequestedEvent
|
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.InferenceCompletedEvent
|
||||||
import com.correx.core.events.events.InferenceStartedEvent
|
import com.correx.core.events.events.InferenceStartedEvent
|
||||||
import com.correx.core.events.events.InferenceTimeoutEvent
|
import com.correx.core.events.events.InferenceTimeoutEvent
|
||||||
import com.correx.core.events.events.OrchestrationPausedEvent
|
import com.correx.core.events.events.OrchestrationPausedEvent
|
||||||
import com.correx.core.events.events.StageCompletedEvent
|
import com.correx.core.events.events.StageCompletedEvent
|
||||||
import com.correx.core.events.events.StageFailedEvent
|
import com.correx.core.events.events.StageFailedEvent
|
||||||
|
import com.correx.core.events.events.StoredEvent
|
||||||
import com.correx.core.events.events.ToolExecutionCompletedEvent
|
import com.correx.core.events.events.ToolExecutionCompletedEvent
|
||||||
import com.correx.core.events.events.ToolExecutionFailedEvent
|
import com.correx.core.events.events.ToolExecutionFailedEvent
|
||||||
import com.correx.core.events.events.ToolExecutionRejectedEvent
|
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.WorkflowCompletedEvent
|
||||||
import com.correx.core.events.events.WorkflowFailedEvent
|
import com.correx.core.events.events.WorkflowFailedEvent
|
||||||
import com.correx.core.events.events.WorkflowStartedEvent
|
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) {
|
class DomainEventMapper(private val artifactStore: ArtifactStore = NoopArtifactStore) {
|
||||||
suspend fun map(event: StoredEvent): ServerMessage? =
|
suspend fun map(event: StoredEvent): ServerMessage? =
|
||||||
@@ -39,47 +39,80 @@ suspend fun domainEventToServerMessage(event: StoredEvent, artifactStore: Artifa
|
|||||||
is WorkflowStartedEvent -> mapWorkflowStarted(p)
|
is WorkflowStartedEvent -> mapWorkflowStarted(p)
|
||||||
is WorkflowCompletedEvent -> ServerMessage.SessionCompleted(sessionId = p.sessionId)
|
is WorkflowCompletedEvent -> ServerMessage.SessionCompleted(sessionId = p.sessionId)
|
||||||
is WorkflowFailedEvent -> ServerMessage.SessionFailed(sessionId = p.sessionId, reason = p.reason)
|
is WorkflowFailedEvent -> ServerMessage.SessionFailed(sessionId = p.sessionId, reason = p.reason)
|
||||||
is TransitionExecutedEvent -> ServerMessage.StageStarted(sessionId = p.sessionId, stageId = p.to)
|
is TransitionExecutedEvent -> ServerMessage.StageStarted(
|
||||||
is StageCompletedEvent -> ServerMessage.StageCompleted(sessionId = p.sessionId, stageId = p.stageId)
|
sessionId = p.sessionId,
|
||||||
is StageFailedEvent -> ServerMessage.StageFailed(
|
stageId = p.to,
|
||||||
sessionId = p.sessionId, stageId = p.stageId, reason = p.reason,
|
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 OrchestrationPausedEvent -> mapOrchestrationPaused(p)
|
||||||
is InferenceStartedEvent -> ServerMessage.InferenceStarted(sessionId = p.sessionId, stageId = p.stageId)
|
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(
|
is InferenceTimeoutEvent -> ServerMessage.InferenceTimedOut(
|
||||||
sessionId = p.sessionId, stageId = p.stageId, elapsedMs = p.timeoutMs,
|
sessionId = p.sessionId, stageId = p.stageId, elapsedMs = p.timeoutMs,
|
||||||
)
|
)
|
||||||
|
|
||||||
is ToolInvocationRequestedEvent -> ServerMessage.ToolStarted(
|
is ToolInvocationRequestedEvent -> ServerMessage.ToolStarted(
|
||||||
sessionId = p.sessionId, toolName = p.toolName, tier = p.tier,
|
sessionId = p.sessionId, toolName = p.toolName, tier = p.tier,
|
||||||
)
|
)
|
||||||
|
|
||||||
is ToolExecutionCompletedEvent -> ServerMessage.ToolCompleted(
|
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(
|
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(
|
is ToolExecutionRejectedEvent -> ServerMessage.ToolRejected(
|
||||||
sessionId = p.sessionId, toolName = p.toolName, reason = p.reason,
|
sessionId = p.sessionId, toolName = p.toolName, reason = p.reason,
|
||||||
)
|
)
|
||||||
|
|
||||||
is ApprovalRequestedEvent -> mapApprovalRequested(p)
|
is ApprovalRequestedEvent -> mapApprovalRequested(p)
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun mapWorkflowStarted(p: WorkflowStartedEvent): ServerMessage =
|
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 {
|
private fun mapOrchestrationPaused(p: OrchestrationPausedEvent): ServerMessage {
|
||||||
val reason = if (p.reason == "APPROVAL_PENDING") PauseReason.APPROVAL_PENDING else PauseReason.USER_REQUESTED
|
val reason = if (p.reason == "APPROVAL_PENDING") PauseReason.APPROVAL_PENDING else PauseReason.USER_REQUESTED
|
||||||
return ServerMessage.SessionPaused(sessionId = p.sessionId, reason = reason)
|
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 {
|
val text = runCatching {
|
||||||
artifactStore.get(p.responseArtifactId)?.toString(Charsets.UTF_8) ?: ""
|
artifactStore.get(p.responseArtifactId)?.toString(Charsets.UTF_8) ?: ""
|
||||||
}.getOrElse { "" }
|
}.getOrElse { "" }
|
||||||
return ServerMessage.InferenceCompleted(
|
return ServerMessage.InferenceCompleted(
|
||||||
sessionId = p.sessionId, stageId = p.stageId, outputSummary = text, responseText = text,
|
sessionId = p.sessionId,
|
||||||
|
stageId = p.stageId,
|
||||||
|
outputSummary = text,
|
||||||
|
responseText = text,
|
||||||
|
occurredAt = event.metadata.timestamp.toEpochMilliseconds(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package com.correx.apps.server.protocol
|
package com.correx.apps.server.protocol
|
||||||
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
class ProtocolException(message: String, cause: Throwable? = null) : RuntimeException(message, cause)
|
class ProtocolException(message: String, cause: Throwable? = null) : RuntimeException(message, cause)
|
||||||
|
|
||||||
|
|||||||
@@ -21,13 +21,13 @@ sealed class ServerMessage {
|
|||||||
data class SessionFailed(val sessionId: SessionId, val reason: String) : ServerMessage()
|
data class SessionFailed(val sessionId: SessionId, val reason: String) : ServerMessage()
|
||||||
|
|
||||||
@Serializable
|
@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
|
@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
|
@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()
|
ServerMessage()
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@@ -39,6 +39,7 @@ sealed class ServerMessage {
|
|||||||
val stageId: StageId,
|
val stageId: StageId,
|
||||||
val outputSummary: String,
|
val outputSummary: String,
|
||||||
val responseText: String = "",
|
val responseText: String = "",
|
||||||
|
val occurredAt: Long,
|
||||||
) : ServerMessage()
|
) : ServerMessage()
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@@ -50,11 +51,15 @@ sealed class ServerMessage {
|
|||||||
ServerMessage()
|
ServerMessage()
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class ToolCompleted(val sessionId: SessionId, val toolName: String, val outputSummary: String) :
|
data class ToolCompleted(
|
||||||
ServerMessage()
|
val sessionId: SessionId,
|
||||||
|
val toolName: String,
|
||||||
|
val outputSummary: String,
|
||||||
|
val occurredAt: Long,
|
||||||
|
) : ServerMessage()
|
||||||
|
|
||||||
@Serializable
|
@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()
|
ServerMessage()
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
package com.correx.apps.server.registry
|
package com.correx.apps.server.registry
|
||||||
|
|
||||||
|
import com.correx.core.events.types.ProviderId
|
||||||
import com.correx.core.inference.InferenceProvider
|
import com.correx.core.inference.InferenceProvider
|
||||||
import com.correx.core.inference.ProviderHealth
|
import com.correx.core.inference.ProviderHealth
|
||||||
import com.correx.core.events.types.ProviderId
|
|
||||||
|
|
||||||
interface ProviderRegistry {
|
interface ProviderRegistry {
|
||||||
fun listAll(): List<InferenceProvider>
|
fun listAll(): List<InferenceProvider>
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import com.correx.core.events.types.EventId
|
|||||||
import com.correx.core.events.types.SessionId
|
import com.correx.core.events.types.SessionId
|
||||||
import com.correx.core.utils.TypeId
|
import com.correx.core.utils.TypeId
|
||||||
import io.ktor.http.HttpStatusCode
|
import io.ktor.http.HttpStatusCode
|
||||||
import org.slf4j.LoggerFactory
|
|
||||||
import io.ktor.server.request.receive
|
import io.ktor.server.request.receive
|
||||||
import io.ktor.server.response.respond
|
import io.ktor.server.response.respond
|
||||||
import io.ktor.server.routing.Route
|
import io.ktor.server.routing.Route
|
||||||
@@ -21,7 +20,8 @@ import io.ktor.server.websocket.webSocket
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.datetime.Clock
|
import kotlinx.datetime.Clock
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import java.util.UUID
|
import org.slf4j.LoggerFactory
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class StartSessionRequest(val workflowId: String, val config: SessionConfigDto? = null)
|
data class StartSessionRequest(val workflowId: String, val config: SessionConfigDto? = null)
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import kotlinx.coroutines.channels.ClosedReceiveChannelException
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.datetime.Clock
|
import kotlinx.datetime.Clock
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import java.util.UUID
|
import java.util.*
|
||||||
|
|
||||||
private val log = LoggerFactory.getLogger(GlobalStreamHandler::class.java)
|
private val log = LoggerFactory.getLogger(GlobalStreamHandler::class.java)
|
||||||
|
|
||||||
|
|||||||
@@ -10,12 +10,10 @@ import com.correx.apps.server.protocol.ServerMessage.RouterResponseMessage
|
|||||||
import com.correx.core.approvals.ApprovalOutcome
|
import com.correx.core.approvals.ApprovalOutcome
|
||||||
import com.correx.core.approvals.ApprovalStatus
|
import com.correx.core.approvals.ApprovalStatus
|
||||||
import com.correx.core.approvals.Tier
|
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.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.events.types.SessionId
|
||||||
import com.correx.core.router.RouterFacade
|
|
||||||
import com.correx.core.sessions.ApprovalMode
|
import com.correx.core.sessions.ApprovalMode
|
||||||
import com.correx.core.utils.TypeId
|
import com.correx.core.utils.TypeId
|
||||||
import io.ktor.server.websocket.DefaultWebSocketServerSession
|
import io.ktor.server.websocket.DefaultWebSocketServerSession
|
||||||
@@ -24,6 +22,7 @@ import io.ktor.websocket.readText
|
|||||||
import kotlinx.coroutines.channels.ClosedReceiveChannelException
|
import kotlinx.coroutines.channels.ClosedReceiveChannelException
|
||||||
import kotlinx.datetime.Clock
|
import kotlinx.datetime.Clock
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
|
import com.correx.core.approvals.model.ApprovalDecision as DomainApprovalDecision
|
||||||
|
|
||||||
private val log = LoggerFactory.getLogger(SessionStreamHandler::class.java)
|
private val log = LoggerFactory.getLogger(SessionStreamHandler::class.java)
|
||||||
|
|
||||||
@@ -75,11 +74,20 @@ class SessionStreamHandler(private val module: ServerModule) {
|
|||||||
runCatching { module.orchestrator.cancel(msg.sessionId) }
|
runCatching { module.orchestrator.cancel(msg.sessionId) }
|
||||||
.onFailure { log.error("cancel failed for session={}: {}", msg.sessionId.value, it.message, it) }
|
.onFailure { log.error("cancel failed for session={}: {}", msg.sessionId.value, it.message, it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
is ClientMessage.ApprovalResponse -> {
|
is ClientMessage.ApprovalResponse -> {
|
||||||
val domainDecision = msg.toDomainDecision(msg.requestId.value)
|
val domainDecision = msg.toDomainDecision(msg.requestId.value)
|
||||||
runCatching { module.orchestrator.submitApprovalDecision(msg.requestId, domainDecision) }
|
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 -> {
|
is ClientMessage.ChatInput -> {
|
||||||
runCatching {
|
runCatching {
|
||||||
module.routerFacade.onUserInput(
|
module.routerFacade.onUserInput(
|
||||||
@@ -88,21 +96,24 @@ class SessionStreamHandler(private val module: ServerModule) {
|
|||||||
mode = msg.mode,
|
mode = msg.mode,
|
||||||
)
|
)
|
||||||
}.onSuccess { response ->
|
}.onSuccess { response ->
|
||||||
session.send(Frame.Text(
|
session.send(
|
||||||
|
Frame.Text(
|
||||||
ProtocolSerializer.encodeServerMessage(
|
ProtocolSerializer.encodeServerMessage(
|
||||||
RouterResponseMessage(
|
RouterResponseMessage(
|
||||||
sessionId = msg.sessionId,
|
sessionId = msg.sessionId,
|
||||||
content = response.content,
|
content = response.content,
|
||||||
steeringEmitted = response.steeringEmitted,
|
steeringEmitted = response.steeringEmitted,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
)
|
|
||||||
))
|
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
log.error("routerFacade.onUserInput failed for session={}: {}", msg.sessionId.value, it.message, it)
|
log.error("routerFacade.onUserInput failed for session={}: {}", msg.sessionId.value, it.message, it)
|
||||||
val error = ServerMessage.ProtocolError("Router error: ${it.message}")
|
val error = ServerMessage.ProtocolError("Router error: ${it.message}")
|
||||||
session.send(Frame.Text(ProtocolSerializer.encodeServerMessage(error)))
|
session.send(Frame.Text(ProtocolSerializer.encodeServerMessage(error)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
log.warn("unexpected message type={} in session stream", msg::class.simpleName)
|
log.warn("unexpected message type={} in session stream", msg::class.simpleName)
|
||||||
val error = ServerMessage.ProtocolError("Unexpected message type in session stream")
|
val error = ServerMessage.ProtocolError("Unexpected message type in session stream")
|
||||||
|
|||||||
+48
-24
@@ -24,8 +24,8 @@ import com.correx.core.events.events.TransitionExecutedEvent
|
|||||||
import com.correx.core.events.events.WorkflowCompletedEvent
|
import com.correx.core.events.events.WorkflowCompletedEvent
|
||||||
import com.correx.core.events.events.WorkflowFailedEvent
|
import com.correx.core.events.events.WorkflowFailedEvent
|
||||||
import com.correx.core.events.events.WorkflowStartedEvent
|
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.ApprovalRequestId
|
||||||
|
import com.correx.core.events.types.ArtifactId
|
||||||
import com.correx.core.events.types.EventId
|
import com.correx.core.events.types.EventId
|
||||||
import com.correx.core.events.types.InferenceRequestId
|
import com.correx.core.events.types.InferenceRequestId
|
||||||
import com.correx.core.events.types.ProviderId
|
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.TransitionId
|
||||||
import com.correx.core.events.types.ValidationReportId
|
import com.correx.core.events.types.ValidationReportId
|
||||||
import com.correx.core.inference.TokenUsage
|
import com.correx.core.inference.TokenUsage
|
||||||
import kotlinx.datetime.Instant
|
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import kotlinx.datetime.Instant
|
||||||
import org.junit.jupiter.api.Assertions.assertEquals
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
import org.junit.jupiter.api.Assertions.assertNull
|
import org.junit.jupiter.api.Assertions.assertNull
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
@@ -45,7 +45,9 @@ class DomainEventMapperTest {
|
|||||||
|
|
||||||
private val sessionId = SessionId("session-1")
|
private val sessionId = SessionId("session-1")
|
||||||
private val stageId = StageId("stage-1")
|
private val stageId = StageId("stage-1")
|
||||||
|
private val workflowId = "workflow-test"
|
||||||
private val timestamp = Instant.parse("2026-01-01T00:00:00Z")
|
private val timestamp = Instant.parse("2026-01-01T00:00:00Z")
|
||||||
|
private val occurredAt = timestamp.toEpochMilliseconds()
|
||||||
private val noopStore: ArtifactStore = object : ArtifactStore {
|
private val noopStore: ArtifactStore = object : ArtifactStore {
|
||||||
override suspend fun put(bytes: ByteArray): ArtifactId = ArtifactId("art-1")
|
override suspend fun put(bytes: ByteArray): ArtifactId = ArtifactId("art-1")
|
||||||
override suspend fun get(id: ArtifactId): ByteArray? = null
|
override suspend fun get(id: ArtifactId): ByteArray? = null
|
||||||
@@ -68,15 +70,21 @@ class DomainEventMapperTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `WorkflowStartedEvent maps to SessionStarted`(): Unit = runTest {
|
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)
|
val result = domainEventToServerMessage(event, noopStore)
|
||||||
assertEquals(ServerMessage.SessionStarted(sessionId, stageId.value), result)
|
assertEquals(ServerMessage.SessionStarted(sessionId, workflowId), result)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `WorkflowCompletedEvent maps to SessionCompleted`(): Unit = runTest {
|
fun `WorkflowCompletedEvent maps to SessionCompleted`(): Unit = runTest {
|
||||||
val event = storedEvent(
|
val event = storedEvent(
|
||||||
WorkflowCompletedEvent(sessionId = sessionId, terminalStageId = stageId, totalStages = 1),
|
WorkflowCompletedEvent(
|
||||||
|
sessionId = sessionId,
|
||||||
|
terminalStageId = stageId,
|
||||||
|
totalStages = 1,
|
||||||
|
workflowId = workflowId,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
val result = domainEventToServerMessage(event, noopStore)
|
val result = domainEventToServerMessage(event, noopStore)
|
||||||
assertEquals(ServerMessage.SessionCompleted(sessionId), result)
|
assertEquals(ServerMessage.SessionCompleted(sessionId), result)
|
||||||
@@ -99,7 +107,7 @@ class DomainEventMapperTest {
|
|||||||
TransitionExecutedEvent(sessionId = sessionId, from = from, to = to, transitionId = TransitionId("t1")),
|
TransitionExecutedEvent(sessionId = sessionId, from = from, to = to, transitionId = TransitionId("t1")),
|
||||||
)
|
)
|
||||||
val result = domainEventToServerMessage(event, noopStore)
|
val result = domainEventToServerMessage(event, noopStore)
|
||||||
assertEquals(ServerMessage.StageStarted(sessionId, to), result)
|
assertEquals(ServerMessage.StageStarted(sessionId, to, occurredAt), result)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -108,7 +116,7 @@ class DomainEventMapperTest {
|
|||||||
StageCompletedEvent(sessionId = sessionId, stageId = stageId, transitionId = TransitionId("t1")),
|
StageCompletedEvent(sessionId = sessionId, stageId = stageId, transitionId = TransitionId("t1")),
|
||||||
)
|
)
|
||||||
val result = domainEventToServerMessage(event, noopStore)
|
val result = domainEventToServerMessage(event, noopStore)
|
||||||
assertEquals(ServerMessage.StageCompleted(sessionId, stageId), result)
|
assertEquals(ServerMessage.StageCompleted(sessionId, stageId, occurredAt), result)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -119,7 +127,7 @@ class DomainEventMapperTest {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
val result = domainEventToServerMessage(event, noopStore)
|
val result = domainEventToServerMessage(event, noopStore)
|
||||||
assertEquals(ServerMessage.StageFailed(sessionId, stageId, "err"), result)
|
assertEquals(ServerMessage.StageFailed(sessionId, stageId, "err", occurredAt), result)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -142,13 +150,15 @@ class DomainEventMapperTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `InferenceStartedEvent maps to InferenceStarted`(): Unit = runTest {
|
fun `InferenceStartedEvent maps to InferenceStarted`(): Unit = runTest {
|
||||||
val event = storedEvent(InferenceStartedEvent(
|
val event = storedEvent(
|
||||||
|
InferenceStartedEvent(
|
||||||
requestId = InferenceRequestId("req-1"),
|
requestId = InferenceRequestId("req-1"),
|
||||||
sessionId = sessionId,
|
sessionId = sessionId,
|
||||||
stageId = stageId,
|
stageId = stageId,
|
||||||
providerId = ProviderId("p1"),
|
providerId = ProviderId("p1"),
|
||||||
promptArtifactId = ArtifactId("art-prompt"),
|
promptArtifactId = ArtifactId("art-prompt"),
|
||||||
))
|
),
|
||||||
|
)
|
||||||
val result = domainEventToServerMessage(event, noopStore)
|
val result = domainEventToServerMessage(event, noopStore)
|
||||||
assertEquals(ServerMessage.InferenceStarted(sessionId, stageId), result)
|
assertEquals(ServerMessage.InferenceStarted(sessionId, stageId), result)
|
||||||
}
|
}
|
||||||
@@ -161,9 +171,11 @@ class DomainEventMapperTest {
|
|||||||
override suspend fun put(bytes: ByteArray): ArtifactId = ArtifactId("x")
|
override suspend fun put(bytes: ByteArray): ArtifactId = ArtifactId("x")
|
||||||
override suspend fun get(id: ArtifactId): ByteArray? =
|
override suspend fun get(id: ArtifactId): ByteArray? =
|
||||||
if (id == artifactId) responseText.toByteArray(Charsets.UTF_8) else null
|
if (id == artifactId) responseText.toByteArray(Charsets.UTF_8) else null
|
||||||
|
|
||||||
override suspend fun flushBefore(commit: suspend () -> Unit) = commit()
|
override suspend fun flushBefore(commit: suspend () -> Unit) = commit()
|
||||||
}
|
}
|
||||||
val event = storedEvent(InferenceCompletedEvent(
|
val event = storedEvent(
|
||||||
|
InferenceCompletedEvent(
|
||||||
requestId = InferenceRequestId("req-1"),
|
requestId = InferenceRequestId("req-1"),
|
||||||
sessionId = sessionId,
|
sessionId = sessionId,
|
||||||
stageId = stageId,
|
stageId = stageId,
|
||||||
@@ -171,14 +183,19 @@ class DomainEventMapperTest {
|
|||||||
tokensUsed = TokenUsage(10, 5),
|
tokensUsed = TokenUsage(10, 5),
|
||||||
latencyMs = 100L,
|
latencyMs = 100L,
|
||||||
responseArtifactId = artifactId,
|
responseArtifactId = artifactId,
|
||||||
))
|
),
|
||||||
|
)
|
||||||
val result = domainEventToServerMessage(event, store)
|
val result = domainEventToServerMessage(event, store)
|
||||||
assertEquals(ServerMessage.InferenceCompleted(sessionId, stageId, responseText, responseText), result)
|
assertEquals(
|
||||||
|
ServerMessage.InferenceCompleted(sessionId, stageId, responseText, responseText, occurredAt),
|
||||||
|
result,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `InferenceCompletedEvent falls back to empty string when artifact missing`(): Unit = runTest {
|
fun `InferenceCompletedEvent falls back to empty string when artifact missing`(): Unit = runTest {
|
||||||
val event = storedEvent(InferenceCompletedEvent(
|
val event = storedEvent(
|
||||||
|
InferenceCompletedEvent(
|
||||||
requestId = InferenceRequestId("req-1"),
|
requestId = InferenceRequestId("req-1"),
|
||||||
sessionId = sessionId,
|
sessionId = sessionId,
|
||||||
stageId = stageId,
|
stageId = stageId,
|
||||||
@@ -186,20 +203,23 @@ class DomainEventMapperTest {
|
|||||||
tokensUsed = TokenUsage(10, 5),
|
tokensUsed = TokenUsage(10, 5),
|
||||||
latencyMs = 100L,
|
latencyMs = 100L,
|
||||||
responseArtifactId = ArtifactId("missing"),
|
responseArtifactId = ArtifactId("missing"),
|
||||||
))
|
),
|
||||||
|
)
|
||||||
val result = domainEventToServerMessage(event, noopStore)
|
val result = domainEventToServerMessage(event, noopStore)
|
||||||
assertEquals(ServerMessage.InferenceCompleted(sessionId, stageId, "", ""), result)
|
assertEquals(ServerMessage.InferenceCompleted(sessionId, stageId, "", "", occurredAt), result)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `InferenceTimeoutEvent maps to InferenceTimedOut`(): Unit = runTest {
|
fun `InferenceTimeoutEvent maps to InferenceTimedOut`(): Unit = runTest {
|
||||||
val event = storedEvent(InferenceTimeoutEvent(
|
val event = storedEvent(
|
||||||
|
InferenceTimeoutEvent(
|
||||||
requestId = InferenceRequestId("req-1"),
|
requestId = InferenceRequestId("req-1"),
|
||||||
sessionId = sessionId,
|
sessionId = sessionId,
|
||||||
stageId = stageId,
|
stageId = stageId,
|
||||||
providerId = ProviderId("p1"),
|
providerId = ProviderId("p1"),
|
||||||
timeoutMs = 5000L,
|
timeoutMs = 5000L,
|
||||||
))
|
),
|
||||||
|
)
|
||||||
val result = domainEventToServerMessage(event, noopStore)
|
val result = domainEventToServerMessage(event, noopStore)
|
||||||
assertEquals(ServerMessage.InferenceTimedOut(sessionId, stageId, 5000L), result)
|
assertEquals(ServerMessage.InferenceTimedOut(sessionId, stageId, 5000L), result)
|
||||||
}
|
}
|
||||||
@@ -207,7 +227,8 @@ class DomainEventMapperTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `ToolInvocationRequestedEvent maps to ToolStarted`(): Unit = runTest {
|
fun `ToolInvocationRequestedEvent maps to ToolStarted`(): Unit = runTest {
|
||||||
val invId = ToolInvocationId("inv-1")
|
val invId = ToolInvocationId("inv-1")
|
||||||
val event = storedEvent(ToolInvocationRequestedEvent(
|
val event = storedEvent(
|
||||||
|
ToolInvocationRequestedEvent(
|
||||||
invocationId = invId,
|
invocationId = invId,
|
||||||
sessionId = sessionId,
|
sessionId = sessionId,
|
||||||
stageId = stageId,
|
stageId = stageId,
|
||||||
@@ -216,7 +237,8 @@ class DomainEventMapperTest {
|
|||||||
request = ToolRequest(
|
request = ToolRequest(
|
||||||
invocationId = invId, sessionId = sessionId, stageId = stageId, toolName = "file_write",
|
invocationId = invId, sessionId = sessionId, stageId = stageId, toolName = "file_write",
|
||||||
),
|
),
|
||||||
))
|
),
|
||||||
|
)
|
||||||
val result = domainEventToServerMessage(event, noopStore)
|
val result = domainEventToServerMessage(event, noopStore)
|
||||||
assertEquals(ServerMessage.ToolStarted(sessionId, "file_write", Tier.T2), result)
|
assertEquals(ServerMessage.ToolStarted(sessionId, "file_write", Tier.T2), result)
|
||||||
}
|
}
|
||||||
@@ -239,7 +261,7 @@ class DomainEventMapperTest {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
val result = domainEventToServerMessage(event, noopStore)
|
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
|
@Test
|
||||||
@@ -253,7 +275,7 @@ class DomainEventMapperTest {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
val result = domainEventToServerMessage(event, noopStore)
|
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
|
@Test
|
||||||
@@ -274,7 +296,8 @@ class DomainEventMapperTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `ApprovalRequestedEvent maps to ApprovalRequired`(): Unit = runTest {
|
fun `ApprovalRequestedEvent maps to ApprovalRequired`(): Unit = runTest {
|
||||||
val requestId = ApprovalRequestId("req-1")
|
val requestId = ApprovalRequestId("req-1")
|
||||||
val event = storedEvent(ApprovalRequestedEvent(
|
val event = storedEvent(
|
||||||
|
ApprovalRequestedEvent(
|
||||||
requestId = requestId,
|
requestId = requestId,
|
||||||
tier = Tier.T3,
|
tier = Tier.T3,
|
||||||
validationReportId = ValidationReportId("vr-1"),
|
validationReportId = ValidationReportId("vr-1"),
|
||||||
@@ -282,7 +305,8 @@ class DomainEventMapperTest {
|
|||||||
sessionId = sessionId,
|
sessionId = sessionId,
|
||||||
stageId = stageId,
|
stageId = stageId,
|
||||||
projectId = null,
|
projectId = null,
|
||||||
))
|
),
|
||||||
|
)
|
||||||
val result = domainEventToServerMessage(event, noopStore) as ServerMessage.ApprovalRequired
|
val result = domainEventToServerMessage(event, noopStore) as ServerMessage.ApprovalRequired
|
||||||
assertEquals(requestId, result.requestId)
|
assertEquals(requestId, result.requestId)
|
||||||
assertEquals(Tier.T3, result.tier)
|
assertEquals(Tier.T3, result.tier)
|
||||||
|
|||||||
+11
-10
@@ -5,8 +5,8 @@ import com.correx.core.artifactstore.ArtifactStore
|
|||||||
import com.correx.core.events.events.EventMetadata
|
import com.correx.core.events.events.EventMetadata
|
||||||
import com.correx.core.events.events.EventPayload
|
import com.correx.core.events.events.EventPayload
|
||||||
import com.correx.core.events.events.NewEvent
|
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.OrchestrationResumedEvent
|
||||||
|
import com.correx.core.events.events.StoredEvent
|
||||||
import com.correx.core.events.events.WorkflowCompletedEvent
|
import com.correx.core.events.events.WorkflowCompletedEvent
|
||||||
import com.correx.core.events.events.WorkflowStartedEvent
|
import com.correx.core.events.events.WorkflowStartedEvent
|
||||||
import com.correx.core.events.stores.EventStore
|
import com.correx.core.events.stores.EventStore
|
||||||
@@ -27,6 +27,7 @@ class SessionEventBridgeTest {
|
|||||||
|
|
||||||
private val sessionId = SessionId("session-1")
|
private val sessionId = SessionId("session-1")
|
||||||
private val stageId = StageId("stage-1")
|
private val stageId = StageId("stage-1")
|
||||||
|
private val workflowId = "workflow-test"
|
||||||
private val timestamp = Instant.parse("2026-01-01T00:00:00Z")
|
private val timestamp = Instant.parse("2026-01-01T00:00:00Z")
|
||||||
|
|
||||||
private val noopArtifactStore: ArtifactStore = object : ArtifactStore {
|
private val noopArtifactStore: ArtifactStore = object : ArtifactStore {
|
||||||
@@ -65,8 +66,8 @@ class SessionEventBridgeTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `replaySnapshot sends mapped messages for all events`() = runTest {
|
fun `replaySnapshot sends mapped messages for all events`() = runTest {
|
||||||
val events = listOf(
|
val events = listOf(
|
||||||
storedEvent(WorkflowStartedEvent(sessionId, stageId), seq = 1L),
|
storedEvent(WorkflowStartedEvent(sessionId, workflowId, stageId), seq = 1L),
|
||||||
storedEvent(WorkflowCompletedEvent(sessionId, stageId, 1), seq = 2L),
|
storedEvent(WorkflowCompletedEvent(sessionId, stageId, 1, workflowId), seq = 2L),
|
||||||
)
|
)
|
||||||
val store = fakeEventStore(allEventsList = events)
|
val store = fakeEventStore(allEventsList = events)
|
||||||
val sent = mutableListOf<ServerMessage>()
|
val sent = mutableListOf<ServerMessage>()
|
||||||
@@ -75,7 +76,7 @@ class SessionEventBridgeTest {
|
|||||||
bridge.replaySnapshot()
|
bridge.replaySnapshot()
|
||||||
|
|
||||||
assertEquals(2, sent.size)
|
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])
|
assertEquals(ServerMessage.SessionCompleted(sessionId), sent[1])
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,7 +84,7 @@ class SessionEventBridgeTest {
|
|||||||
fun `replaySnapshot skips unmapped events`() = runTest {
|
fun `replaySnapshot skips unmapped events`() = runTest {
|
||||||
val events = listOf(
|
val events = listOf(
|
||||||
storedEvent(OrchestrationResumedEvent(sessionId, stageId), seq = 1L),
|
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 store = fakeEventStore(allEventsList = events)
|
||||||
val sent = mutableListOf<ServerMessage>()
|
val sent = mutableListOf<ServerMessage>()
|
||||||
@@ -98,8 +99,8 @@ class SessionEventBridgeTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `streamLive sends mapped messages from live flow`() = runTest {
|
fun `streamLive sends mapped messages from live flow`() = runTest {
|
||||||
val events = listOf(
|
val events = listOf(
|
||||||
storedEvent(WorkflowStartedEvent(sessionId, stageId), seq = 1L),
|
storedEvent(WorkflowStartedEvent(sessionId, workflowId, stageId), seq = 1L),
|
||||||
storedEvent(WorkflowCompletedEvent(sessionId, stageId, 1), seq = 2L),
|
storedEvent(WorkflowCompletedEvent(sessionId, stageId, 1, workflowId), seq = 2L),
|
||||||
)
|
)
|
||||||
val liveFlow: Flow<StoredEvent> = flow { events.forEach { emit(it) } }
|
val liveFlow: Flow<StoredEvent> = flow { events.forEach { emit(it) } }
|
||||||
val store = fakeEventStore(liveFlow = liveFlow)
|
val store = fakeEventStore(liveFlow = liveFlow)
|
||||||
@@ -109,7 +110,7 @@ class SessionEventBridgeTest {
|
|||||||
bridge.streamLive(sessionId)
|
bridge.streamLive(sessionId)
|
||||||
|
|
||||||
assertEquals(2, sent.size)
|
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])
|
assertEquals(ServerMessage.SessionCompleted(sessionId), sent[1])
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,10 +124,10 @@ class SessionEventBridgeTest {
|
|||||||
val sent = mutableListOf<ServerMessage>()
|
val sent = mutableListOf<ServerMessage>()
|
||||||
val bridge = SessionEventBridge(store, noopArtifactStore) { sent.add(it) }
|
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) }
|
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()
|
channel.close()
|
||||||
job.join()
|
job.join()
|
||||||
|
|
||||||
|
|||||||
@@ -20,12 +20,12 @@ import dev.tamboui.terminal.Frame
|
|||||||
import dev.tamboui.tui.EventHandler
|
import dev.tamboui.tui.EventHandler
|
||||||
import dev.tamboui.tui.Renderer
|
import dev.tamboui.tui.Renderer
|
||||||
import dev.tamboui.tui.TuiRunner
|
import dev.tamboui.tui.TuiRunner
|
||||||
import dev.tamboui.tui.event.KeyEvent as TambouiKeyEvent
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import dev.tamboui.tui.event.KeyEvent as TambouiKeyEvent
|
||||||
|
|
||||||
private const val DEFAULT_PORT = 8080
|
private const val DEFAULT_PORT = 8080
|
||||||
private const val STATUS_HEIGHT = 1
|
private const val STATUS_HEIGHT = 1
|
||||||
@@ -70,7 +70,7 @@ fun main(args: Array<String>) {
|
|||||||
|
|
||||||
val handler = EventHandler { event, _ ->
|
val handler = EventHandler { event, _ ->
|
||||||
if (event is TambouiKeyEvent) {
|
if (event is TambouiKeyEvent) {
|
||||||
mapKey(event, state.inputMode)
|
mapKey(event)
|
||||||
?.let { KeyResolver.resolve(it, state.inputMode, state.inputBuffer) }
|
?.let { KeyResolver.resolve(it, state.inputMode, state.inputBuffer) }
|
||||||
?.let(::dispatch)
|
?.let(::dispatch)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,50 +23,11 @@ fun inputBarWidget(state: TuiState): Paragraph {
|
|||||||
?: "(no session)"
|
?: "(no session)"
|
||||||
|
|
||||||
val (row1, row2) = when (state.inputMode) {
|
val (row1, row2) = when (state.inputMode) {
|
||||||
InputMode.ROUTER -> {
|
InputMode.ROUTER -> createRowsForRouter(state, dimStyle, sessionName, hasApproval, hasSession)
|
||||||
val cursor = if (state.inputBuffer.isNotEmpty()) {
|
InputMode.NAVIGATE -> createRowsForNavigate(dimStyle, sessionName, hasApproval, hasSession)
|
||||||
Line.from(Span.raw("▌ "), Span.raw(state.inputBuffer))
|
InputMode.FILTER -> createRowsForFilter(state, dimStyle)
|
||||||
} else {
|
InputMode.STEER -> createRowsForSteer(state, dimStyle, sessionName)
|
||||||
Line.from(Span.raw("▌ "), Span.styled("Ask anything…", dimStyle))
|
|
||||||
}
|
|
||||||
val hints = buildList<String> {
|
|
||||||
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<String> {
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val block = Block.builder()
|
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()
|
return Paragraph.builder().text(Text.from(listOf(row1, row2))).block(block).build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun createRowsForSteer(
|
||||||
|
state: TuiState,
|
||||||
|
dimStyle: Style?,
|
||||||
|
sessionName: String,
|
||||||
|
): Pair<Line?, Line?> {
|
||||||
|
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<Line?, Line?> {
|
||||||
|
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<Line?, Line?> {
|
||||||
|
val row1Line = Line.from(Span.styled(" ↑↓ sessions enter select", dimStyle))
|
||||||
|
val hints = buildList<String> {
|
||||||
|
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<Line?, Line?> {
|
||||||
|
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<String> {
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package com.correx.apps.tui.components
|
package com.correx.apps.tui.components
|
||||||
|
|
||||||
import com.correx.apps.tui.state.TuiState
|
import com.correx.apps.tui.state.TuiState
|
||||||
import dev.tamboui.style.Color
|
|
||||||
import dev.tamboui.style.Style
|
import dev.tamboui.style.Style
|
||||||
import dev.tamboui.text.Line
|
import dev.tamboui.text.Line
|
||||||
import dev.tamboui.text.Span
|
import dev.tamboui.text.Span
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ fun sessionListWidget(state: TuiState): Paragraph {
|
|||||||
val isSelected = s.id == state.sessions.selectedId
|
val isSelected = s.id == state.sessions.selectedId
|
||||||
val prefix = if (isSelected) Span.styled("▶ ", Style.create().blue()) else Span.raw(" ")
|
val prefix = if (isSelected) Span.styled("▶ ", Style.create().blue()) else Span.raw(" ")
|
||||||
val idSpan = Span.styled("[${s.id.take(ID_LEN)}]", dimStyle)
|
val idSpan = Span.styled("[${s.id.take(ID_LEN)}]", dimStyle)
|
||||||
val statusStr = s.status.uppercase().padEnd(STATUS_WIDTH)
|
|
||||||
val statusSpan = statusSpan(s)
|
val statusSpan = statusSpan(s)
|
||||||
val rawName = s.name.ifEmpty { s.workflowId }
|
val rawName = s.name.ifEmpty { s.workflowId }
|
||||||
val truncated = if (rawName.length > NAME_MAX) rawName.take(NAME_MAX - 1) + "…" else rawName
|
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<SessionSummary> {
|
fun filteredSessions(sessions: SessionsState): List<SessionSummary> {
|
||||||
val f = sessions.filter
|
val f = sessions.filter
|
||||||
return if (f.isEmpty()) sessions.sessions
|
val sorted = sessions.sessions.sortedByDescending { it.lastEventAt }
|
||||||
else sessions.sessions.filter { it.workflowId.contains(f, ignoreCase = true) }
|
return if (f.isEmpty()) sorted
|
||||||
|
else sorted.filter { it.workflowId.contains(f, ignoreCase = true) }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,23 @@
|
|||||||
package com.correx.apps.tui.input
|
package com.correx.apps.tui.input
|
||||||
|
|
||||||
import com.correx.apps.tui.KeyEvent
|
import com.correx.apps.tui.KeyEvent
|
||||||
import com.correx.apps.tui.state.InputMode
|
|
||||||
import dev.tamboui.tui.event.KeyCode
|
import dev.tamboui.tui.event.KeyCode
|
||||||
import dev.tamboui.tui.event.KeyEvent as TambouiKeyEvent
|
import dev.tamboui.tui.event.KeyEvent as TambouiKeyEvent
|
||||||
|
|
||||||
@Suppress("CyclomaticComplexMethod", "ReturnCount")
|
@Suppress("CyclomaticComplexMethod", "ReturnCount")
|
||||||
fun mapKey(event: TambouiKeyEvent, mode: InputMode): KeyEvent? {
|
fun mapKey(event: TambouiKeyEvent): KeyEvent? {
|
||||||
if (event.isCtrlC()) return KeyEvent.Quit
|
if (event.isCtrlC) return KeyEvent.Quit
|
||||||
if (event.hasAlt()) return null
|
if (event.hasAlt()) return null
|
||||||
if (event.hasCtrl()) {
|
if (event.hasCtrl()) {
|
||||||
return when (event.character()) {
|
return when {
|
||||||
'q' -> KeyEvent.Quit
|
event.isChar('q') -> KeyEvent.Quit
|
||||||
'n' -> KeyEvent.NewSession
|
event.isChar('n') -> KeyEvent.NewSession
|
||||||
'c' -> KeyEvent.Cancel
|
event.isChar('c') -> KeyEvent.Cancel
|
||||||
'a' -> KeyEvent.Approve
|
event.isChar('a') -> KeyEvent.Approve
|
||||||
'r' -> KeyEvent.Reject
|
event.isChar('r') -> KeyEvent.Reject
|
||||||
's' -> KeyEvent.Steer
|
event.isChar('s') -> KeyEvent.Steer
|
||||||
'h' -> KeyEvent.ToggleApprovalOverlay
|
event.isChar('h') -> KeyEvent.ToggleApprovalOverlay
|
||||||
'e' -> KeyEvent.ToggleEventOverlay
|
event.isChar('e') -> KeyEvent.ToggleEventOverlay
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,11 +17,13 @@ object RootReducer {
|
|||||||
val (approval, ae) = ApprovalReducer.reduce(afterInput.approval, prevInputMode, action)
|
val (approval, ae) = ApprovalReducer.reduce(afterInput.approval, prevInputMode, action)
|
||||||
val (connection, ce) = ConnectionReducer.reduce(afterInput.connection, action)
|
val (connection, ce) = ConnectionReducer.reduce(afterInput.connection, action)
|
||||||
val (provider, pe) = ProviderReducer.reduce(afterInput.provider, action)
|
val (provider, pe) = ProviderReducer.reduce(afterInput.provider, action)
|
||||||
|
val approvalJustArrived = approval.active != null && afterInput.approval.active == null
|
||||||
val withSubReducers = afterInput.copy(
|
val withSubReducers = afterInput.copy(
|
||||||
sessions = sessions,
|
sessions = sessions,
|
||||||
approval = approval,
|
approval = approval,
|
||||||
connection = connection,
|
connection = connection,
|
||||||
provider = provider,
|
provider = provider,
|
||||||
|
approvalOverlayVisible = if (approvalJustArrived) true else afterInput.approvalOverlayVisible,
|
||||||
)
|
)
|
||||||
val final = when (action) {
|
val final = when (action) {
|
||||||
is Action.ToggleApprovalOverlay -> withSubReducers.copy(
|
is Action.ToggleApprovalOverlay -> withSubReducers.copy(
|
||||||
|
|||||||
@@ -25,8 +25,18 @@ object SessionsReducer {
|
|||||||
action: Action,
|
action: Action,
|
||||||
clock: () -> Long = System::currentTimeMillis,
|
clock: () -> Long = System::currentTimeMillis,
|
||||||
): Pair<SessionsState, List<Effect>> = when (action) {
|
): Pair<SessionsState, List<Effect>> = when (action) {
|
||||||
is Action.NavigateUp -> if (inputMode == InputMode.NAVIGATE) navigateUp(sessions) to emptyList() else sessions to emptyList()
|
is Action.NavigateUp -> if (inputMode == InputMode.NAVIGATE) {
|
||||||
is Action.NavigateDown -> if (inputMode == InputMode.NAVIGATE) navigateDown(sessions) to emptyList() else sessions to emptyList()
|
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) {
|
is Action.SubmitInput -> if (inputMode == InputMode.FILTER) {
|
||||||
sessions.copy(filter = inputText) to emptyList()
|
sessions.copy(filter = inputText) to emptyList()
|
||||||
} else {
|
} else {
|
||||||
@@ -52,8 +62,12 @@ object SessionsReducer {
|
|||||||
else -> sessions to emptyList()
|
else -> sessions to emptyList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun filteredSessions(sessions: SessionsState): List<SessionSummary> =
|
||||||
|
if (sessions.filter.isBlank()) sessions.sessions
|
||||||
|
else sessions.sessions.filter { it.workflowId.contains(sessions.filter, ignoreCase = true) }
|
||||||
|
|
||||||
private fun navigateUp(sessions: SessionsState): SessionsState {
|
private fun navigateUp(sessions: SessionsState): SessionsState {
|
||||||
val list = sessions.sessions
|
val list = filteredSessions(sessions)
|
||||||
if (list.isEmpty()) return sessions
|
if (list.isEmpty()) return sessions
|
||||||
val idx = list.indexOfFirst { it.id == sessions.selectedId }
|
val idx = list.indexOfFirst { it.id == sessions.selectedId }
|
||||||
val newIdx = if (idx <= 0) list.lastIndex else idx - 1
|
val newIdx = if (idx <= 0) list.lastIndex else idx - 1
|
||||||
@@ -61,7 +75,7 @@ object SessionsReducer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun navigateDown(sessions: SessionsState): SessionsState {
|
private fun navigateDown(sessions: SessionsState): SessionsState {
|
||||||
val list = sessions.sessions
|
val list = filteredSessions(sessions)
|
||||||
if (list.isEmpty()) return sessions
|
if (list.isEmpty()) return sessions
|
||||||
val idx = list.indexOfFirst { it.id == sessions.selectedId }
|
val idx = list.indexOfFirst { it.id == sessions.selectedId }
|
||||||
val newIdx = if (idx >= list.lastIndex) 0 else idx + 1
|
val newIdx = if (idx >= list.lastIndex) 0 else idx + 1
|
||||||
@@ -77,58 +91,38 @@ object SessionsReducer {
|
|||||||
msg: ServerMessage,
|
msg: ServerMessage,
|
||||||
clock: () -> Long,
|
clock: () -> Long,
|
||||||
): Pair<SessionsState, List<Effect>> = when (msg) {
|
): Pair<SessionsState, List<Effect>> = when (msg) {
|
||||||
is ServerMessage.SessionStarted -> {
|
is ServerMessage.SessionStarted -> processSessionStartedMessage(msg, clock, sessions)
|
||||||
val summary = SessionSummary(
|
is ServerMessage.SessionPaused -> processSessionPausedMessage(msg, sessions, clock)
|
||||||
id = msg.sessionId.value,
|
is ServerMessage.SessionCompleted -> processSessionCompletedMessage(sessions, msg, clock)
|
||||||
status = "ACTIVE",
|
is ServerMessage.SessionFailed -> processSessionFailedMessage(sessions, msg, clock)
|
||||||
workflowId = msg.workflowId,
|
is ServerMessage.StageStarted -> processStageStartedMessage(msg, sessions)
|
||||||
name = msg.workflowId,
|
is ServerMessage.StageCompleted -> processStageCompletedMessage(msg, sessions)
|
||||||
lastEventAt = clock(),
|
is ServerMessage.StageFailed -> processStageFailedMessage(msg, sessions)
|
||||||
currentStage = null,
|
is ServerMessage.ToolStarted -> processToolStartedMessage(clock, sessions, msg)
|
||||||
lastOutput = null,
|
is ServerMessage.InferenceCompleted -> processInferenceCompletedMessage(sessions, msg)
|
||||||
)
|
is ServerMessage.ToolCompleted -> processToolCompletedMessage(msg, sessions)
|
||||||
val selected = sessions.selectedId ?: msg.sessionId.value
|
is ServerMessage.ToolFailed -> processToolFailedMessage(msg, sessions)
|
||||||
sessions.copy(sessions = sessions.sessions + summary, selectedId = selected) to
|
is ServerMessage.ToolRejected -> processToolRejectedMessage(clock, sessions, msg)
|
||||||
listOf(Effect.ConnectSession(msg.sessionId))
|
else -> sessions to emptyList()
|
||||||
}
|
}
|
||||||
|
|
||||||
is ServerMessage.SessionPaused -> {
|
private fun processToolRejectedMessage(
|
||||||
val statusLabel = if (msg.reason == PauseReason.APPROVAL_PENDING) {
|
clock: () -> Long,
|
||||||
"PAUSED awaiting approval"
|
sessions: SessionsState,
|
||||||
} else {
|
msg: ServerMessage.ToolRejected,
|
||||||
"PAUSED"
|
): Pair<SessionsState, List<Effect>> {
|
||||||
}
|
|
||||||
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()
|
val now = clock()
|
||||||
sessions.copy(
|
return sessions.copy(
|
||||||
sessions = sessions.sessions.map { s ->
|
sessions = sessions.sessions.map { s ->
|
||||||
if (s.id == msg.sessionId.value) {
|
if (s.id == msg.sessionId.value) {
|
||||||
val entry = TuiEventEntry(formatTime(now), "StageStarted", msg.stageId.value)
|
val updatedTools = s.tools.map { t ->
|
||||||
s.copy(
|
if (t.name == msg.toolName && t.status == ToolDisplayStatus.STARTED) {
|
||||||
currentStage = msg.stageId.value,
|
t.copy(status = ToolDisplayStatus.REJECTED)
|
||||||
currentStageId = msg.stageId.value,
|
} else {
|
||||||
tools = emptyList(),
|
t
|
||||||
recentEvents = (s.recentEvents + entry).takeLast(7),
|
}
|
||||||
lastEventAt = now,
|
}
|
||||||
)
|
s.copy(tools = updatedTools, lastEventAt = now)
|
||||||
} else {
|
} else {
|
||||||
s
|
s
|
||||||
}
|
}
|
||||||
@@ -136,17 +130,22 @@ object SessionsReducer {
|
|||||||
) to emptyList()
|
) to emptyList()
|
||||||
}
|
}
|
||||||
|
|
||||||
is ServerMessage.StageCompleted -> {
|
private fun processToolFailedMessage(
|
||||||
val now = clock()
|
msg: ServerMessage.ToolFailed,
|
||||||
sessions.copy(
|
sessions: SessionsState,
|
||||||
|
): Pair<SessionsState, List<Effect>> {
|
||||||
|
val now = msg.occurredAt
|
||||||
|
return sessions.copy(
|
||||||
sessions = sessions.sessions.map { s ->
|
sessions = sessions.sessions.map { s ->
|
||||||
if (s.id == msg.sessionId.value) {
|
if (s.id == msg.sessionId.value) {
|
||||||
val entry = TuiEventEntry(formatTime(now), "StageCompleted", msg.stageId.value)
|
val updatedTools = s.tools.map { t ->
|
||||||
s.copy(
|
if (t.name == msg.toolName && t.status == ToolDisplayStatus.STARTED) {
|
||||||
currentStage = null,
|
t.copy(status = ToolDisplayStatus.FAILED)
|
||||||
recentEvents = (s.recentEvents + entry).takeLast(7),
|
} else {
|
||||||
lastEventAt = now,
|
t
|
||||||
)
|
}
|
||||||
|
}
|
||||||
|
s.copy(tools = updatedTools, lastEventAt = now)
|
||||||
} else {
|
} else {
|
||||||
s
|
s
|
||||||
}
|
}
|
||||||
@@ -154,45 +153,12 @@ object SessionsReducer {
|
|||||||
) to emptyList()
|
) to emptyList()
|
||||||
}
|
}
|
||||||
|
|
||||||
is ServerMessage.StageFailed -> {
|
private fun processToolCompletedMessage(
|
||||||
val now = clock()
|
msg: ServerMessage.ToolCompleted,
|
||||||
sessions.copy(
|
sessions: SessionsState,
|
||||||
sessions = sessions.sessions.map { s ->
|
): Pair<SessionsState, List<Effect>> {
|
||||||
if (s.id == msg.sessionId.value) {
|
val now = msg.occurredAt
|
||||||
val entry = TuiEventEntry(formatTime(now), "StageFailed", msg.stageId.value)
|
return sessions.copy(
|
||||||
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 ->
|
sessions = sessions.sessions.map { s ->
|
||||||
if (s.id == msg.sessionId.value) {
|
if (s.id == msg.sessionId.value) {
|
||||||
val updatedTools = s.tools.map { t ->
|
val updatedTools = s.tools.map { t ->
|
||||||
@@ -216,55 +182,153 @@ object SessionsReducer {
|
|||||||
) to emptyList()
|
) to emptyList()
|
||||||
}
|
}
|
||||||
|
|
||||||
is ServerMessage.ToolFailed -> {
|
private fun processToolStartedMessage(
|
||||||
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()
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> sessions to emptyList()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun applyInferenceCompleted(
|
|
||||||
sessions: SessionsState,
|
|
||||||
msg: ServerMessage.InferenceCompleted,
|
|
||||||
clock: () -> Long,
|
clock: () -> Long,
|
||||||
|
sessions: SessionsState,
|
||||||
|
msg: ServerMessage.ToolStarted,
|
||||||
): Pair<SessionsState, List<Effect>> {
|
): Pair<SessionsState, List<Effect>> {
|
||||||
val now = clock()
|
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<SessionsState, List<Effect>> {
|
||||||
|
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<SessionsState, List<Effect>> {
|
||||||
|
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<SessionsState, List<Effect>> {
|
||||||
|
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<SessionsState, List<Effect.DisconnectSession>> = touchSession(
|
||||||
|
sessions,
|
||||||
|
msg.sessionId.value,
|
||||||
|
"FAILED",
|
||||||
|
clock,
|
||||||
|
) to listOf(Effect.DisconnectSession)
|
||||||
|
|
||||||
|
private fun processSessionCompletedMessage(
|
||||||
|
sessions: SessionsState,
|
||||||
|
msg: ServerMessage.SessionCompleted,
|
||||||
|
clock: () -> Long,
|
||||||
|
): Pair<SessionsState, List<Effect.DisconnectSession>> =
|
||||||
|
touchSession(sessions, msg.sessionId.value, "COMPLETED", clock) to listOf(
|
||||||
|
Effect.DisconnectSession,
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun processSessionPausedMessage(
|
||||||
|
msg: ServerMessage.SessionPaused,
|
||||||
|
sessions: SessionsState,
|
||||||
|
clock: () -> Long,
|
||||||
|
): Pair<SessionsState, List<Effect>> {
|
||||||
|
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<SessionsState, List<Effect.ConnectSession>> {
|
||||||
|
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<SessionsState, List<Effect>> {
|
||||||
|
val now = msg.occurredAt
|
||||||
return sessions.copy(
|
return sessions.copy(
|
||||||
sessions = sessions.sessions.map { s ->
|
sessions = sessions.sessions.map { s ->
|
||||||
if (s.id == msg.sessionId.value) {
|
if (s.id == msg.sessionId.value) {
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
package com.correx.apps.tui.input
|
package com.correx.apps.tui.input
|
||||||
|
|
||||||
import com.correx.apps.tui.KeyEvent
|
import com.correx.apps.tui.KeyEvent
|
||||||
import com.correx.apps.tui.state.InputMode
|
|
||||||
import dev.tamboui.tui.event.KeyCode
|
import dev.tamboui.tui.event.KeyCode
|
||||||
import dev.tamboui.tui.event.KeyEvent as TambouiKeyEvent
|
|
||||||
import dev.tamboui.tui.event.KeyModifiers
|
import dev.tamboui.tui.event.KeyModifiers
|
||||||
import org.junit.jupiter.api.Assertions.assertEquals
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
import org.junit.jupiter.api.Assertions.assertNull
|
import org.junit.jupiter.api.Assertions.assertNull
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
|
import dev.tamboui.tui.event.KeyEvent as TambouiKeyEvent
|
||||||
|
|
||||||
class TambouiKeyMapperTest {
|
class TambouiKeyMapperTest {
|
||||||
|
|
||||||
@@ -15,156 +14,156 @@ class TambouiKeyMapperTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `ctrl-c maps to Quit in InputMode ROUTER`() {
|
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
|
@Test
|
||||||
fun `ctrl-c maps to Quit in InputMode FILTER`() {
|
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
|
@Test
|
||||||
fun `ctrl-c maps to Quit in InputMode NAVIGATE`() {
|
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
|
@Test
|
||||||
fun `ctrl-c maps to Quit in InputMode STEER`() {
|
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 ---
|
// --- Ctrl+key → keybind events ---
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `ctrl-q maps to Quit`() {
|
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
|
@Test
|
||||||
fun `ctrl-a maps to Approve`() {
|
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
|
@Test
|
||||||
fun `ctrl-r maps to Reject`() {
|
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
|
@Test
|
||||||
fun `ctrl-n maps to NewSession`() {
|
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
|
@Test
|
||||||
fun `ctrl-s maps to Steer`() {
|
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
|
@Test
|
||||||
fun `ctrl-h maps to ToggleApprovalOverlay`() {
|
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
|
@Test
|
||||||
fun `ctrl-e maps to ToggleEventOverlay`() {
|
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
|
@Test
|
||||||
fun `ctrl+unbound key maps to null`() {
|
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 ---
|
// --- Alt always → null ---
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `alt modifier maps to null`() {
|
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) ---
|
// --- Bare chars always → CharInput (never trigger keybinds) ---
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `bare a maps to CharInput in ROUTER`() {
|
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
|
@Test
|
||||||
fun `bare q maps to CharInput in ROUTER`() {
|
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
|
@Test
|
||||||
fun `bare s maps to CharInput in ROUTER`() {
|
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
|
@Test
|
||||||
fun `bare x maps to CharInput in ROUTER`() {
|
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
|
@Test
|
||||||
fun `bare q maps to CharInput in FILTER`() {
|
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
|
@Test
|
||||||
fun `bare q maps to CharInput in STEER`() {
|
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
|
@Test
|
||||||
fun `bare q maps to CharInput in NAVIGATE`() {
|
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 ---
|
// --- KeyCode mappings ---
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `UP maps to NavUp`() {
|
fun `UP maps to NavUp`() {
|
||||||
assertEquals(KeyEvent.NavUp, mapKey(TambouiKeyEvent.ofKey(KeyCode.UP), InputMode.ROUTER))
|
assertEquals(KeyEvent.NavUp, mapKey(TambouiKeyEvent.ofKey(KeyCode.UP)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `DOWN maps to NavDown`() {
|
fun `DOWN maps to NavDown`() {
|
||||||
assertEquals(KeyEvent.NavDown, mapKey(TambouiKeyEvent.ofKey(KeyCode.DOWN), InputMode.ROUTER))
|
assertEquals(KeyEvent.NavDown, mapKey(TambouiKeyEvent.ofKey(KeyCode.DOWN)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `ENTER maps to Enter`() {
|
fun `ENTER maps to Enter`() {
|
||||||
assertEquals(KeyEvent.Enter, mapKey(TambouiKeyEvent.ofKey(KeyCode.ENTER), InputMode.ROUTER))
|
assertEquals(KeyEvent.Enter, mapKey(TambouiKeyEvent.ofKey(KeyCode.ENTER)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `ESCAPE maps to Escape`() {
|
fun `ESCAPE maps to Escape`() {
|
||||||
assertEquals(KeyEvent.Escape, mapKey(TambouiKeyEvent.ofKey(KeyCode.ESCAPE), InputMode.ROUTER))
|
assertEquals(KeyEvent.Escape, mapKey(TambouiKeyEvent.ofKey(KeyCode.ESCAPE)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `BACKSPACE maps to Backspace`() {
|
fun `BACKSPACE maps to Backspace`() {
|
||||||
assertEquals(KeyEvent.Backspace, mapKey(TambouiKeyEvent.ofKey(KeyCode.BACKSPACE), InputMode.ROUTER))
|
assertEquals(KeyEvent.Backspace, mapKey(TambouiKeyEvent.ofKey(KeyCode.BACKSPACE)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `TAB maps to Filter`() {
|
fun `TAB maps to Filter`() {
|
||||||
assertEquals(KeyEvent.Filter, mapKey(TambouiKeyEvent.ofKey(KeyCode.TAB), InputMode.ROUTER))
|
assertEquals(KeyEvent.Filter, mapKey(TambouiKeyEvent.ofKey(KeyCode.TAB)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `TAB maps to Filter in FILTER mode`() {
|
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 ---
|
// --- ISO control chars → null ---
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `ESC codepoint as CHAR event maps to null`() {
|
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
|
@Test
|
||||||
fun `NUL codepoint as CHAR event maps to null`() {
|
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)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,7 +51,11 @@ class RootReducerTest {
|
|||||||
)
|
)
|
||||||
val (state2, _) = RootReducer.reduce(state1, Action.ServerEventReceived(startMsg2), fixedClock)
|
val (state2, _) = RootReducer.reduce(state1, Action.ServerEventReceived(startMsg2), fixedClock)
|
||||||
// selected is s1 (first); navigating down should move to s2
|
// 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)
|
assertEquals("s2", state3.sessions.selectedId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -132,7 +132,11 @@ class SessionsReducerTest {
|
|||||||
sessions = listOf(session("s1").copy(currentStage = "stage-1")),
|
sessions = listOf(session("s1").copy(currentStage = "stage-1")),
|
||||||
selectedId = "s1",
|
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))
|
val (state, _) = reduce(sessions = s, action = Action.ServerEventReceived(msg))
|
||||||
assertEquals(null, state.sessions[0].currentStage)
|
assertEquals(null, state.sessions[0].currentStage)
|
||||||
assertEquals(1000L, state.sessions[0].lastEventAt)
|
assertEquals(1000L, state.sessions[0].lastEventAt)
|
||||||
@@ -148,6 +152,7 @@ class SessionsReducerTest {
|
|||||||
sessionId = SessionId("s1"),
|
sessionId = SessionId("s1"),
|
||||||
stageId = StageId("stage-1"),
|
stageId = StageId("stage-1"),
|
||||||
reason = "error",
|
reason = "error",
|
||||||
|
occurredAt = fixedClock(),
|
||||||
)
|
)
|
||||||
val (state, _) = reduce(sessions = s, action = Action.ServerEventReceived(msg))
|
val (state, _) = reduce(sessions = s, action = Action.ServerEventReceived(msg))
|
||||||
assertEquals(null, state.sessions[0].currentStage)
|
assertEquals(null, state.sessions[0].currentStage)
|
||||||
@@ -177,6 +182,7 @@ class SessionsReducerTest {
|
|||||||
val msg = ServerMessage.InferenceCompleted(
|
val msg = ServerMessage.InferenceCompleted(
|
||||||
sessionId = SessionId("s1"), stageId = StageId("stage-1"),
|
sessionId = SessionId("s1"), stageId = StageId("stage-1"),
|
||||||
outputSummary = "summary", responseText = "full response",
|
outputSummary = "summary", responseText = "full response",
|
||||||
|
occurredAt = fixedClock(),
|
||||||
)
|
)
|
||||||
val (state, _) = reduce(sessions = s, action = Action.ServerEventReceived(msg))
|
val (state, _) = reduce(sessions = s, action = Action.ServerEventReceived(msg))
|
||||||
assertEquals("summary", state.sessions[0].lastOutput)
|
assertEquals("summary", state.sessions[0].lastOutput)
|
||||||
@@ -190,6 +196,7 @@ class SessionsReducerTest {
|
|||||||
val msg = ServerMessage.InferenceCompleted(
|
val msg = ServerMessage.InferenceCompleted(
|
||||||
sessionId = SessionId("s1"), stageId = StageId("stage-1"),
|
sessionId = SessionId("s1"), stageId = StageId("stage-1"),
|
||||||
outputSummary = "", responseText = "",
|
outputSummary = "", responseText = "",
|
||||||
|
occurredAt = fixedClock(),
|
||||||
)
|
)
|
||||||
val (state, _) = reduce(sessions = s, action = Action.ServerEventReceived(msg))
|
val (state, _) = reduce(sessions = s, action = Action.ServerEventReceived(msg))
|
||||||
assertEquals(null, state.sessions[0].lastResponseText)
|
assertEquals(null, state.sessions[0].lastResponseText)
|
||||||
@@ -198,7 +205,11 @@ class SessionsReducerTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `ServerEventReceived StageStarted sets currentStage`() {
|
fun `ServerEventReceived StageStarted sets currentStage`() {
|
||||||
val s = SessionsState(sessions = listOf(session("s1")), selectedId = "s1")
|
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))
|
val (state, _) = reduce(sessions = s, action = Action.ServerEventReceived(msg))
|
||||||
assertEquals("stage-1", state.sessions[0].currentStage)
|
assertEquals("stage-1", state.sessions[0].currentStage)
|
||||||
assertEquals(1000L, state.sessions[0].lastEventAt)
|
assertEquals(1000L, state.sessions[0].lastEventAt)
|
||||||
@@ -207,7 +218,12 @@ class SessionsReducerTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `ServerEventReceived ToolCompleted updates lastOutput`() {
|
fun `ServerEventReceived ToolCompleted updates lastOutput`() {
|
||||||
val s = SessionsState(sessions = listOf(session("s1")), selectedId = "s1")
|
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))
|
val (state, _) = reduce(sessions = s, action = Action.ServerEventReceived(msg))
|
||||||
assertEquals("file_write: wrote 3 lines", state.sessions[0].lastOutput)
|
assertEquals("file_write: wrote 3 lines", state.sessions[0].lastOutput)
|
||||||
assertEquals(1000L, state.sessions[0].lastEventAt)
|
assertEquals(1000L, state.sessions[0].lastEventAt)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import kotlinx.serialization.Serializable
|
|||||||
@Serializable
|
@Serializable
|
||||||
data class WorkflowStartedEvent(
|
data class WorkflowStartedEvent(
|
||||||
val sessionId: SessionId,
|
val sessionId: SessionId,
|
||||||
|
val workflowId: String,
|
||||||
val startStageId: StageId,
|
val startStageId: StageId,
|
||||||
val retryPolicy: RetryPolicy? = null,
|
val retryPolicy: RetryPolicy? = null,
|
||||||
) : EventPayload
|
) : EventPayload
|
||||||
@@ -17,6 +18,7 @@ data class WorkflowCompletedEvent(
|
|||||||
val sessionId: SessionId,
|
val sessionId: SessionId,
|
||||||
val terminalStageId: StageId,
|
val terminalStageId: StageId,
|
||||||
val totalStages: Int,
|
val totalStages: Int,
|
||||||
|
val workflowId: String,
|
||||||
) : EventPayload
|
) : EventPayload
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
|||||||
@@ -33,10 +33,10 @@ import com.correx.core.events.events.SessionFailedEvent
|
|||||||
import com.correx.core.events.events.SessionPausedEvent
|
import com.correx.core.events.events.SessionPausedEvent
|
||||||
import com.correx.core.events.events.SessionResumedEvent
|
import com.correx.core.events.events.SessionResumedEvent
|
||||||
import com.correx.core.events.events.SessionStartedEvent
|
import com.correx.core.events.events.SessionStartedEvent
|
||||||
import com.correx.core.events.events.SteeringNoteAddedEvent
|
|
||||||
import com.correx.core.events.events.StageCompletedEvent
|
import com.correx.core.events.events.StageCompletedEvent
|
||||||
import com.correx.core.events.events.StageFailedEvent
|
import com.correx.core.events.events.StageFailedEvent
|
||||||
import com.correx.core.events.events.StageStartedEvent
|
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.ToolExecutionCompletedEvent
|
||||||
import com.correx.core.events.events.ToolExecutionFailedEvent
|
import com.correx.core.events.events.ToolExecutionFailedEvent
|
||||||
import com.correx.core.events.events.ToolExecutionRejectedEvent
|
import com.correx.core.events.events.ToolExecutionRejectedEvent
|
||||||
|
|||||||
+7
-6
@@ -1,14 +1,13 @@
|
|||||||
package com.correx.core.kernel.orchestration
|
package com.correx.core.kernel.orchestration
|
||||||
|
|
||||||
import com.correx.core.approvals.model.ApprovalDecision
|
import com.correx.core.approvals.model.ApprovalDecision
|
||||||
import com.correx.core.artifactstore.ArtifactStore
|
|
||||||
import com.correx.core.artifacts.ArtifactState
|
import com.correx.core.artifacts.ArtifactState
|
||||||
|
import com.correx.core.artifactstore.ArtifactStore
|
||||||
import com.correx.core.events.events.ArtifactValidatedEvent
|
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.orchestration.OrchestrationState
|
||||||
import com.correx.core.events.types.ApprovalRequestId
|
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.SessionId
|
||||||
import com.correx.core.events.types.StageId
|
import com.correx.core.events.types.StageId
|
||||||
import com.correx.core.kernel.execution.WorkflowResult
|
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.execution.StageExecutionResult
|
||||||
import com.correx.core.transitions.graph.WorkflowGraph
|
import com.correx.core.transitions.graph.WorkflowGraph
|
||||||
import com.correx.core.transitions.resolution.TransitionDecision
|
import com.correx.core.transitions.resolution.TransitionDecision
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
import java.util.concurrent.*
|
import java.util.concurrent.*
|
||||||
import java.util.concurrent.atomic.*
|
import java.util.concurrent.atomic.*
|
||||||
|
|
||||||
@@ -94,7 +94,7 @@ class DefaultSessionOrchestrator(
|
|||||||
}
|
}
|
||||||
|
|
||||||
is TransitionDecision.Stay -> if (isTerminal(enriched.graph, enriched.currentStageId)) {
|
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 {
|
} else {
|
||||||
failWorkflow(
|
failWorkflow(
|
||||||
sessionId = enriched.sessionId,
|
sessionId = enriched.sessionId,
|
||||||
@@ -140,7 +140,7 @@ class DefaultSessionOrchestrator(
|
|||||||
if (isTerminal(ctx.graph, nextStageId)) {
|
if (isTerminal(ctx.graph, nextStageId)) {
|
||||||
// Terminal stage is a sentinel — not executed, so don't count it.
|
// Terminal stage is a sentinel — not executed, so don't count it.
|
||||||
return StepResult.Terminal(
|
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),
|
currentStageId = advanceStage(ctx.sessionId, ctx.currentStageId, decision),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
is StepResult.Terminal -> result
|
is StepResult.Terminal -> result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-4
@@ -2,7 +2,6 @@ package com.correx.core.kernel.orchestration
|
|||||||
|
|
||||||
import com.correx.core.approvals.domain.NoOpApprovalEngine
|
import com.correx.core.approvals.domain.NoOpApprovalEngine
|
||||||
import com.correx.core.artifactstore.ArtifactStore
|
import com.correx.core.artifactstore.ArtifactStore
|
||||||
import org.slf4j.LoggerFactory
|
|
||||||
import com.correx.core.context.model.ContextPack
|
import com.correx.core.context.model.ContextPack
|
||||||
import com.correx.core.events.events.ArtifactValidatedEvent
|
import com.correx.core.events.events.ArtifactValidatedEvent
|
||||||
import com.correx.core.events.events.ArtifactValidatingEvent
|
import com.correx.core.events.events.ArtifactValidatingEvent
|
||||||
@@ -12,17 +11,18 @@ import com.correx.core.events.types.StageId
|
|||||||
import com.correx.core.inference.GenerationConfig
|
import com.correx.core.inference.GenerationConfig
|
||||||
import com.correx.core.inference.InferenceRequest
|
import com.correx.core.inference.InferenceRequest
|
||||||
import com.correx.core.inference.ResponseFormat
|
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.ReplayStrategy
|
||||||
import com.correx.core.kernel.execution.WorkflowResult
|
import com.correx.core.kernel.execution.WorkflowResult
|
||||||
import com.correx.core.kernel.replay.ReplayInferenceProvider
|
import com.correx.core.kernel.replay.ReplayInferenceProvider
|
||||||
import com.correx.core.kernel.retry.exception.ReplayArtifactMissingException
|
import com.correx.core.kernel.retry.exception.ReplayArtifactMissingException
|
||||||
import com.correx.core.risk.NoOpRiskAssessor
|
import com.correx.core.risk.NoOpRiskAssessor
|
||||||
|
import com.correx.core.sessions.Session
|
||||||
import com.correx.core.transitions.execution.StageExecutionResult
|
import com.correx.core.transitions.execution.StageExecutionResult
|
||||||
import com.correx.core.transitions.graph.StageConfig
|
import com.correx.core.transitions.graph.StageConfig
|
||||||
import com.correx.core.transitions.graph.WorkflowGraph
|
import com.correx.core.transitions.graph.WorkflowGraph
|
||||||
import com.correx.core.transitions.resolution.TransitionDecision
|
import com.correx.core.transitions.resolution.TransitionDecision
|
||||||
import com.correx.core.validation.model.ValidationContext
|
import com.correx.core.validation.model.ValidationContext
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.*
|
import java.util.concurrent.*
|
||||||
import java.util.concurrent.atomic.*
|
import java.util.concurrent.atomic.*
|
||||||
@@ -87,7 +87,7 @@ class ReplayOrchestrator(
|
|||||||
val nextStageId = decision.to
|
val nextStageId = decision.to
|
||||||
|
|
||||||
if (isTerminal(ctx.graph, nextStageId)) {
|
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)) {
|
when (val result = enterStage(ctx, nextStageId, session)) {
|
||||||
@@ -101,7 +101,7 @@ class ReplayOrchestrator(
|
|||||||
}
|
}
|
||||||
|
|
||||||
is TransitionDecision.Stay -> if (isTerminal(ctx.graph, ctx.currentStageId)) {
|
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 {
|
} else {
|
||||||
step(ctx)
|
step(ctx)
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-14
@@ -2,22 +2,24 @@ package com.correx.core.kernel.orchestration
|
|||||||
|
|
||||||
import com.correx.core.approvals.ApprovalOutcome
|
import com.correx.core.approvals.ApprovalOutcome
|
||||||
import com.correx.core.approvals.ApprovalStatus
|
import com.correx.core.approvals.ApprovalStatus
|
||||||
|
import com.correx.core.approvals.Tier
|
||||||
import com.correx.core.approvals.model.ApprovalDecision
|
import com.correx.core.approvals.model.ApprovalDecision
|
||||||
import com.correx.core.approvals.model.DomainApprovalRequest
|
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.builder.ContextPackBuilder
|
||||||
import com.correx.core.context.model.ContextEntry
|
import com.correx.core.context.model.ContextEntry
|
||||||
import com.correx.core.context.model.ContextLayer
|
import com.correx.core.context.model.ContextLayer
|
||||||
import com.correx.core.context.model.ContextPack
|
import com.correx.core.context.model.ContextPack
|
||||||
import com.correx.core.context.model.TokenBudget
|
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.ArtifactCreatedEvent
|
||||||
import com.correx.core.events.events.ArtifactValidatedEvent
|
import com.correx.core.events.events.ArtifactValidatedEvent
|
||||||
import com.correx.core.events.events.ArtifactValidatingEvent
|
import com.correx.core.events.events.ArtifactValidatingEvent
|
||||||
import com.correx.core.events.events.ApprovalDecisionResolvedEvent
|
|
||||||
import com.correx.core.events.events.ApprovalRequestedEvent
|
|
||||||
import com.correx.core.events.events.EventMetadata
|
import com.correx.core.events.events.EventMetadata
|
||||||
import com.correx.core.events.events.EventPayload
|
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.InferenceCompletedEvent
|
||||||
import com.correx.core.events.events.InferenceFailedEvent
|
import com.correx.core.events.events.InferenceFailedEvent
|
||||||
import com.correx.core.events.events.InferenceStartedEvent
|
import com.correx.core.events.events.InferenceStartedEvent
|
||||||
@@ -26,27 +28,25 @@ import com.correx.core.events.events.NewEvent
|
|||||||
import com.correx.core.events.events.OrchestrationPausedEvent
|
import com.correx.core.events.events.OrchestrationPausedEvent
|
||||||
import com.correx.core.events.events.OrchestrationResumedEvent
|
import com.correx.core.events.events.OrchestrationResumedEvent
|
||||||
import com.correx.core.events.events.RiskAssessedEvent
|
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.TransitionExecutedEvent
|
||||||
import com.correx.core.events.events.WorkflowCompletedEvent
|
import com.correx.core.events.events.WorkflowCompletedEvent
|
||||||
import com.correx.core.events.events.WorkflowFailedEvent
|
import com.correx.core.events.events.WorkflowFailedEvent
|
||||||
import com.correx.core.events.events.WorkflowStartedEvent
|
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.stores.EventStore
|
||||||
import com.correx.core.events.types.ApprovalDecisionId
|
import com.correx.core.events.types.ApprovalDecisionId
|
||||||
import com.correx.core.events.types.ApprovalRequestId
|
import com.correx.core.events.types.ApprovalRequestId
|
||||||
import com.correx.core.events.types.ArtifactId
|
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.ContextPackId
|
||||||
import com.correx.core.events.types.EventId
|
import com.correx.core.events.types.EventId
|
||||||
import com.correx.core.events.types.InferenceRequestId
|
import com.correx.core.events.types.InferenceRequestId
|
||||||
import com.correx.core.events.types.RiskSummaryId
|
import com.correx.core.events.types.RiskSummaryId
|
||||||
import com.correx.core.events.types.SessionId
|
import com.correx.core.events.types.SessionId
|
||||||
import com.correx.core.events.types.StageId
|
import com.correx.core.events.types.StageId
|
||||||
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.ToolInvocationId
|
||||||
|
import com.correx.core.events.types.ValidationReportId
|
||||||
import com.correx.core.inference.FinishReason
|
import com.correx.core.inference.FinishReason
|
||||||
import com.correx.core.inference.InferenceRepository
|
import com.correx.core.inference.InferenceRepository
|
||||||
import com.correx.core.inference.InferenceRequest
|
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.ToolCallRequest
|
||||||
import com.correx.core.inference.ToolDefinition
|
import com.correx.core.inference.ToolDefinition
|
||||||
import com.correx.core.inference.ToolFunction
|
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.kernel.execution.WorkflowResult
|
||||||
import com.correx.core.risk.RiskAssessor
|
import com.correx.core.risk.RiskAssessor
|
||||||
import com.correx.core.risk.RiskContext
|
import com.correx.core.risk.RiskContext
|
||||||
import com.correx.core.risk.toApprovalTier
|
import com.correx.core.risk.toApprovalTier
|
||||||
import com.correx.core.sessions.Session
|
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.EvaluationContext
|
||||||
import com.correx.core.transitions.evaluation.PromptResolver
|
import com.correx.core.transitions.evaluation.PromptResolver
|
||||||
import com.correx.core.transitions.execution.StageExecutionResult
|
import com.correx.core.transitions.execution.StageExecutionResult
|
||||||
@@ -587,6 +587,7 @@ abstract class SessionOrchestrator(
|
|||||||
sessionId,
|
sessionId,
|
||||||
WorkflowStartedEvent(
|
WorkflowStartedEvent(
|
||||||
sessionId = sessionId,
|
sessionId = sessionId,
|
||||||
|
workflowId = graph.id,
|
||||||
startStageId = graph.start,
|
startStageId = graph.start,
|
||||||
retryPolicy = config.retryPolicy,
|
retryPolicy = config.retryPolicy,
|
||||||
),
|
),
|
||||||
@@ -597,12 +598,13 @@ abstract class SessionOrchestrator(
|
|||||||
sessionId: SessionId,
|
sessionId: SessionId,
|
||||||
terminalStageId: StageId,
|
terminalStageId: StageId,
|
||||||
stageCount: Int,
|
stageCount: Int,
|
||||||
|
workflowId: String,
|
||||||
): WorkflowResult.Completed {
|
): WorkflowResult.Completed {
|
||||||
log.debug(
|
log.debug(
|
||||||
"[Orchestrator] COMPLETED session={} terminalStage={} stages={}",
|
"[Orchestrator] COMPLETED session={} terminalStage={} stages={}",
|
||||||
sessionId.value, terminalStageId.value, stageCount,
|
sessionId.value, terminalStageId.value, stageCount,
|
||||||
)
|
)
|
||||||
emit(sessionId, WorkflowCompletedEvent(sessionId, terminalStageId, stageCount))
|
emit(sessionId, WorkflowCompletedEvent(sessionId, terminalStageId, stageCount, workflowId))
|
||||||
cancellations.remove(sessionId)
|
cancellations.remove(sessionId)
|
||||||
return WorkflowResult.Completed(sessionId, terminalStageId)
|
return WorkflowResult.Completed(sessionId, terminalStageId)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import com.correx.core.router.model.RouterConfig
|
|||||||
import com.correx.core.router.model.RouterL2Entry
|
import com.correx.core.router.model.RouterL2Entry
|
||||||
import com.correx.core.router.model.RouterState
|
import com.correx.core.router.model.RouterState
|
||||||
import com.correx.core.router.model.RouterTurn
|
import com.correx.core.router.model.RouterTurn
|
||||||
import java.util.UUID
|
import java.util.*
|
||||||
|
|
||||||
interface RouterContextBuilder {
|
interface RouterContextBuilder {
|
||||||
fun build(state: RouterState, budget: TokenBudget): ContextPack
|
fun build(state: RouterState, budget: TokenBudget): ContextPack
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ import com.correx.core.events.events.NewEvent
|
|||||||
import com.correx.core.events.events.SteeringNoteAddedEvent
|
import com.correx.core.events.events.SteeringNoteAddedEvent
|
||||||
import com.correx.core.events.stores.EventStore
|
import com.correx.core.events.stores.EventStore
|
||||||
import com.correx.core.events.types.EventId
|
import com.correx.core.events.types.EventId
|
||||||
|
import com.correx.core.events.types.InferenceRequestId
|
||||||
import com.correx.core.events.types.SessionId
|
import com.correx.core.events.types.SessionId
|
||||||
import com.correx.core.events.types.StageId
|
import com.correx.core.events.types.StageId
|
||||||
import com.correx.core.inference.GenerationConfig
|
import com.correx.core.inference.GenerationConfig
|
||||||
import com.correx.core.inference.InferenceRequest
|
import com.correx.core.inference.InferenceRequest
|
||||||
import com.correx.core.events.types.InferenceRequestId
|
|
||||||
import com.correx.core.inference.InferenceRouter
|
import com.correx.core.inference.InferenceRouter
|
||||||
import com.correx.core.inference.ModelCapability
|
import com.correx.core.inference.ModelCapability
|
||||||
import com.correx.core.inference.ResponseFormat
|
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.RouterTurn
|
||||||
import com.correx.core.router.model.TurnRole
|
import com.correx.core.router.model.TurnRole
|
||||||
import kotlinx.datetime.Clock
|
import kotlinx.datetime.Clock
|
||||||
import java.util.UUID
|
import java.util.*
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.*
|
||||||
|
|
||||||
interface RouterFacade {
|
interface RouterFacade {
|
||||||
suspend fun onUserInput(
|
suspend fun onUserInput(
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ package com.correx.core.router
|
|||||||
|
|
||||||
import com.correx.core.events.events.OrchestrationPausedEvent
|
import com.correx.core.events.events.OrchestrationPausedEvent
|
||||||
import com.correx.core.events.events.OrchestrationResumedEvent
|
import com.correx.core.events.events.OrchestrationResumedEvent
|
||||||
import com.correx.core.events.events.StoredEvent
|
|
||||||
import com.correx.core.events.events.StageCompletedEvent
|
import com.correx.core.events.events.StageCompletedEvent
|
||||||
import com.correx.core.events.events.StageFailedEvent
|
import com.correx.core.events.events.StageFailedEvent
|
||||||
|
import com.correx.core.events.events.StoredEvent
|
||||||
import com.correx.core.events.events.WorkflowCompletedEvent
|
import com.correx.core.events.events.WorkflowCompletedEvent
|
||||||
import com.correx.core.events.events.WorkflowFailedEvent
|
import com.correx.core.events.events.WorkflowFailedEvent
|
||||||
import com.correx.core.events.events.WorkflowStartedEvent
|
import com.correx.core.events.events.WorkflowStartedEvent
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ data class WorkflowGraph(
|
|||||||
) {
|
) {
|
||||||
val stageIds: Set<StageId> get() = stages.keys
|
val stageIds: Set<StageId> get() = stages.keys
|
||||||
init {
|
init {
|
||||||
|
require(id.isNotBlank()) { "workflow id must not be blank" }
|
||||||
require(start in stages) { "start stage must exist in stages" }
|
require(start in stages) { "start stage must exist in stages" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,9 +1,9 @@
|
|||||||
package com.correx.core.validation.artifact
|
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.FileWrittenArtifact
|
||||||
import com.correx.core.artifacts.kind.ProcessResultArtifact
|
import com.correx.core.artifacts.kind.ProcessResultArtifact
|
||||||
import com.correx.core.artifacts.kind.TypedArtifactSlot
|
import com.correx.core.artifacts.kind.TypedArtifactSlot
|
||||||
|
import com.correx.core.artifactstore.ArtifactStore
|
||||||
import com.correx.core.events.types.ArtifactId
|
import com.correx.core.events.types.ArtifactId
|
||||||
import com.correx.core.validation.model.ValidationContext
|
import com.correx.core.validation.model.ValidationContext
|
||||||
import com.correx.core.validation.model.ValidationIssue
|
import com.correx.core.validation.model.ValidationIssue
|
||||||
|
|||||||
@@ -381,7 +381,7 @@ end-to-end example.
|
|||||||
### Skeleton
|
### Skeleton
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
fun main() = runBlocking {
|
fun main(): Unit = runBlocking {
|
||||||
var state = TuiState()
|
var state = TuiState()
|
||||||
val ws = TuiWsClient(host = "localhost", port = 8080)
|
val ws = TuiWsClient(host = "localhost", port = 8080)
|
||||||
val effectScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
val effectScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||||
|
|||||||
+1
-1
@@ -8,8 +8,8 @@ import com.correx.infrastructure.artifactscas.compact.CompactionReport
|
|||||||
import com.correx.infrastructure.artifactscas.compact.Compactor
|
import com.correx.infrastructure.artifactscas.compact.Compactor
|
||||||
import com.correx.infrastructure.artifactscas.compact.LivenessScanner
|
import com.correx.infrastructure.artifactscas.compact.LivenessScanner
|
||||||
import com.correx.infrastructure.artifactscas.config.CasConfig
|
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.EvictionReport
|
||||||
|
import com.correx.infrastructure.artifactscas.evict.Evictor
|
||||||
import com.correx.infrastructure.artifactscas.index.ArtifactIndex
|
import com.correx.infrastructure.artifactscas.index.ArtifactIndex
|
||||||
import com.correx.infrastructure.artifactscas.recovery.TailScanReport
|
import com.correx.infrastructure.artifactscas.recovery.TailScanReport
|
||||||
import com.correx.infrastructure.artifactscas.recovery.TailScanner
|
import com.correx.infrastructure.artifactscas.recovery.TailScanner
|
||||||
|
|||||||
+1
-1
@@ -3,7 +3,7 @@ package com.correx.infrastructure.artifactscas.segment
|
|||||||
import org.bouncycastle.crypto.digests.Blake3Digest
|
import org.bouncycastle.crypto.digests.Blake3Digest
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
import java.nio.ByteOrder
|
import java.nio.ByteOrder
|
||||||
import java.util.zip.CRC32C
|
import java.util.zip.*
|
||||||
|
|
||||||
object SegmentLayout {
|
object SegmentLayout {
|
||||||
const val HASH_SIZE: Int = 32
|
const val HASH_SIZE: Int = 32
|
||||||
|
|||||||
+1
-1
@@ -24,7 +24,7 @@ import org.junit.jupiter.api.Test
|
|||||||
import org.junit.jupiter.api.io.TempDir
|
import org.junit.jupiter.api.io.TempDir
|
||||||
import java.nio.file.Files
|
import java.nio.file.Files
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import java.util.UUID
|
import java.util.*
|
||||||
import kotlin.io.path.listDirectoryEntries
|
import kotlin.io.path.listDirectoryEntries
|
||||||
|
|
||||||
class CompactorTest {
|
class CompactorTest {
|
||||||
|
|||||||
+1
-1
@@ -21,7 +21,7 @@ import org.junit.jupiter.api.Test
|
|||||||
import org.junit.jupiter.api.io.TempDir
|
import org.junit.jupiter.api.io.TempDir
|
||||||
import java.nio.file.Files
|
import java.nio.file.Files
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import java.util.UUID
|
import java.util.*
|
||||||
import kotlin.io.path.listDirectoryEntries
|
import kotlin.io.path.listDirectoryEntries
|
||||||
|
|
||||||
class EvictorTest {
|
class EvictorTest {
|
||||||
|
|||||||
+1
-1
@@ -19,7 +19,7 @@ import kotlinx.coroutines.withContext
|
|||||||
import kotlinx.datetime.Instant
|
import kotlinx.datetime.Instant
|
||||||
import java.sql.Connection
|
import java.sql.Connection
|
||||||
import java.sql.ResultSet
|
import java.sql.ResultSet
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.*
|
||||||
|
|
||||||
class SqliteEventStore(
|
class SqliteEventStore(
|
||||||
private val connection: Connection,
|
private val connection: Connection,
|
||||||
|
|||||||
+1
-1
@@ -21,7 +21,7 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.*
|
||||||
|
|
||||||
class LiveArtifactRepository(
|
class LiveArtifactRepository(
|
||||||
private val eventStore: EventStore,
|
private val eventStore: EventStore,
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.correx.infrastructure
|
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.artifactstore.ArtifactStore
|
||||||
import com.correx.core.events.EventDispatcher
|
import com.correx.core.events.EventDispatcher
|
||||||
import com.correx.core.events.stores.EventStore
|
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.InferenceRouter
|
||||||
import com.correx.core.inference.ProviderRegistry
|
import com.correx.core.inference.ProviderRegistry
|
||||||
import com.correx.core.inference.RoutingStrategy
|
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.contract.ToolExecutor
|
||||||
import com.correx.core.tools.registry.ToolRegistry
|
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.DefaultProviderRegistry
|
||||||
import com.correx.infrastructure.inference.FirstAvailableRoutingStrategy
|
import com.correx.infrastructure.inference.FirstAvailableRoutingStrategy
|
||||||
import com.correx.infrastructure.inference.commons.ModelDescriptor
|
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.commons.ResidencyMode
|
||||||
import com.correx.infrastructure.inference.llama.cpp.DefaultModelManager
|
import com.correx.infrastructure.inference.llama.cpp.DefaultModelManager
|
||||||
import com.correx.infrastructure.inference.llama.cpp.LlamaCppInferenceProvider
|
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.SqliteEventStore
|
||||||
|
import com.correx.infrastructure.persistence.artifact.LiveArtifactRepository
|
||||||
import com.correx.infrastructure.tools.DefaultToolRegistry
|
import com.correx.infrastructure.tools.DefaultToolRegistry
|
||||||
import com.correx.infrastructure.tools.DispatchingToolExecutor
|
import com.correx.infrastructure.tools.DispatchingToolExecutor
|
||||||
import com.correx.infrastructure.tools.SandboxedToolExecutor
|
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.PromptLoader
|
||||||
import com.correx.infrastructure.workflow.TomlWorkflowLoader
|
import com.correx.infrastructure.workflow.TomlWorkflowLoader
|
||||||
import com.correx.infrastructure.workflow.WorkflowLoader
|
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 io.ktor.client.HttpClient
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
import java.nio.file.Files
|
import java.nio.file.Files
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import java.nio.file.Paths
|
import java.nio.file.Paths
|
||||||
|
|||||||
+1
-1
@@ -32,7 +32,7 @@ class FileReadTool(
|
|||||||
(request.parameters["path"] as? String)?.let { pathString ->
|
(request.parameters["path"] as? String)?.let { pathString ->
|
||||||
runCatching {
|
runCatching {
|
||||||
val path = Paths.get(pathString).normalize().toAbsolutePath()
|
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")
|
return@runCatching ValidationResult.Invalid("Path '$pathString' is not in the allowed list")
|
||||||
}
|
}
|
||||||
ValidationResult.Valid
|
ValidationResult.Valid
|
||||||
|
|||||||
+5
-1
@@ -65,7 +65,11 @@ class FileWriteTool(
|
|||||||
}
|
}
|
||||||
put(
|
put(
|
||||||
"required",
|
"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
|
override val tier: Tier = Tier.T2
|
||||||
|
|||||||
+9
-9
@@ -33,7 +33,7 @@ class FileEditToolTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `execute patch success`() = runBlocking {
|
fun `execute patch success`(): Unit = runBlocking {
|
||||||
val tempDir = Files.createTempDirectory("file_edit_patch")
|
val tempDir = Files.createTempDirectory("file_edit_patch")
|
||||||
val filePath = tempDir.resolve("test.txt")
|
val filePath = tempDir.resolve("test.txt")
|
||||||
Files.writeString(filePath, "line1\nline2\nline3\n")
|
Files.writeString(filePath, "line1\nline2\nline3\n")
|
||||||
@@ -63,7 +63,7 @@ class FileEditToolTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@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 tempDir = Files.createTempDirectory("file_edit_test")
|
||||||
val otherDir = Files.createTempDirectory("other_dir")
|
val otherDir = Files.createTempDirectory("other_dir")
|
||||||
val tool = FileEditTool(allowedPaths = setOf(tempDir))
|
val tool = FileEditTool(allowedPaths = setOf(tempDir))
|
||||||
@@ -80,7 +80,7 @@ class FileEditToolTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@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 tempDir = Files.createTempDirectory("file_edit_test")
|
||||||
val tool = FileEditTool(allowedPaths = setOf(tempDir))
|
val tool = FileEditTool(allowedPaths = setOf(tempDir))
|
||||||
val request = createRequest(
|
val request = createRequest(
|
||||||
@@ -96,7 +96,7 @@ class FileEditToolTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@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 tempDir = Files.createTempDirectory("file_edit_test")
|
||||||
val filePath = tempDir.resolve("test.txt")
|
val filePath = tempDir.resolve("test.txt")
|
||||||
Files.writeString(filePath, "content")
|
Files.writeString(filePath, "content")
|
||||||
@@ -120,7 +120,7 @@ class FileEditToolTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `execute append success`() = runBlocking {
|
fun `execute append success`(): Unit = runBlocking {
|
||||||
val tempDir = Files.createTempDirectory("file_edit_append")
|
val tempDir = Files.createTempDirectory("file_edit_append")
|
||||||
val filePath = tempDir.resolve("test.txt")
|
val filePath = tempDir.resolve("test.txt")
|
||||||
Files.writeString(filePath, "hello")
|
Files.writeString(filePath, "hello")
|
||||||
@@ -139,7 +139,7 @@ class FileEditToolTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `execute replace success`() = runBlocking {
|
fun `execute replace success`(): Unit = runBlocking {
|
||||||
val tempDir = Files.createTempDirectory("file_edit_replace")
|
val tempDir = Files.createTempDirectory("file_edit_replace")
|
||||||
val filePath = tempDir.resolve("test.txt")
|
val filePath = tempDir.resolve("test.txt")
|
||||||
Files.writeString(filePath, "the quick brown fox")
|
Files.writeString(filePath, "the quick brown fox")
|
||||||
@@ -159,7 +159,7 @@ class FileEditToolTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@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 tempDir = Files.createTempDirectory("file_edit_replace_fail")
|
||||||
val filePath = tempDir.resolve("test.txt")
|
val filePath = tempDir.resolve("test.txt")
|
||||||
Files.writeString(filePath, "the quick brown fox")
|
Files.writeString(filePath, "the quick brown fox")
|
||||||
@@ -180,7 +180,7 @@ class FileEditToolTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@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 tempDir = Files.createTempDirectory("file_edit_replace_fail_multi")
|
||||||
val filePath = tempDir.resolve("test.txt")
|
val filePath = tempDir.resolve("test.txt")
|
||||||
Files.writeString(filePath, "fox fox fox")
|
Files.writeString(filePath, "fox fox fox")
|
||||||
@@ -202,7 +202,7 @@ class FileEditToolTest {
|
|||||||
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `execute patch failure`() = runBlocking {
|
fun `execute patch failure`(): Unit = runBlocking {
|
||||||
val tempDir = Files.createTempDirectory("file_edit_patch_fail")
|
val tempDir = Files.createTempDirectory("file_edit_patch_fail")
|
||||||
val filePath = tempDir.resolve("test.txt")
|
val filePath = tempDir.resolve("test.txt")
|
||||||
Files.writeString(filePath, "original")
|
Files.writeString(filePath, "original")
|
||||||
|
|||||||
+8
-8
@@ -31,15 +31,15 @@ class FileReadToolTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `validateRequest returns Valid for allowed path`() = runBlocking {
|
fun `validateRequest returns Valid for allowed path`(): Unit = runBlocking {
|
||||||
val tool = FileReadTool(allowedPaths = emptySet())
|
|
||||||
val tempFile = Files.createTempFile("test_allowed", ".txt")
|
val tempFile = Files.createTempFile("test_allowed", ".txt")
|
||||||
|
val tool = FileReadTool(allowedPaths = setOf(tempFile.parent))
|
||||||
val request = createRequest(tempFile.toString())
|
val request = createRequest(tempFile.toString())
|
||||||
assertEquals(ValidationResult.Valid, tool.validateRequest(request))
|
assertEquals(ValidationResult.Valid, tool.validateRequest(request))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@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 tempFile = Files.createTempFile("test_disallowed", ".txt")
|
||||||
val tool = FileReadTool(allowedPaths = setOf(tempFile.parent.resolve("subdir")))
|
val tool = FileReadTool(allowedPaths = setOf(tempFile.parent.resolve("subdir")))
|
||||||
val request = createRequest(tempFile.toString())
|
val request = createRequest(tempFile.toString())
|
||||||
@@ -49,7 +49,7 @@ class FileReadToolTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `validateRequest returns Invalid for missing path parameter`() = runBlocking {
|
fun `validateRequest returns Invalid for missing path parameter`(): Unit = runBlocking {
|
||||||
val tool = FileReadTool()
|
val tool = FileReadTool()
|
||||||
val request = ToolRequest(
|
val request = ToolRequest(
|
||||||
invocationId = invocationId,
|
invocationId = invocationId,
|
||||||
@@ -67,12 +67,12 @@ class FileReadToolTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `execute returns Success for valid file`() = runBlocking {
|
fun `execute returns Success for valid file`(): Unit = runBlocking {
|
||||||
val content = "Hello, world!"
|
val content = "Hello, world!"
|
||||||
val tempFile = Files.createTempFile("test_content", ".txt")
|
val tempFile = Files.createTempFile("test_content", ".txt")
|
||||||
Files.writeString(tempFile, content)
|
Files.writeString(tempFile, content)
|
||||||
|
|
||||||
val tool = FileReadTool(allowedPaths = emptySet())
|
val tool = FileReadTool(allowedPaths = setOf(tempFile.parent))
|
||||||
val request = createRequest(tempFile.toString())
|
val request = createRequest(tempFile.toString())
|
||||||
val result = tool.execute(request)
|
val result = tool.execute(request)
|
||||||
|
|
||||||
@@ -83,7 +83,7 @@ class FileReadToolTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@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 tempDir = Files.createTempDirectory("test_dir")
|
||||||
val nonExistentFile = tempDir.resolve("missing.txt")
|
val nonExistentFile = tempDir.resolve("missing.txt")
|
||||||
|
|
||||||
@@ -98,7 +98,7 @@ class FileReadToolTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@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 tempFile = Files.createTempFile("test_disallowed_exec", ".txt")
|
||||||
val tool = FileReadTool(allowedPaths = setOf(tempFile.parent.resolve("other")))
|
val tool = FileReadTool(allowedPaths = setOf(tempFile.parent.resolve("other")))
|
||||||
val request = createRequest(tempFile.toString())
|
val request = createRequest(tempFile.toString())
|
||||||
|
|||||||
+11
-16
@@ -34,7 +34,7 @@ class FileWriteToolTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@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 tempDir = Files.createTempDirectory("file_write_test")
|
||||||
val tool = FileWriteTool(allowedPaths = setOf(tempDir))
|
val tool = FileWriteTool(allowedPaths = setOf(tempDir))
|
||||||
val request = createRequest(
|
val request = createRequest(
|
||||||
@@ -48,7 +48,7 @@ class FileWriteToolTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@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 tempDir = Files.createTempDirectory("file_write_test")
|
||||||
val otherDir = Files.createTempDirectory("other_dir")
|
val otherDir = Files.createTempDirectory("other_dir")
|
||||||
val tool = FileWriteTool(allowedPaths = setOf(tempDir))
|
val tool = FileWriteTool(allowedPaths = setOf(tempDir))
|
||||||
@@ -65,7 +65,7 @@ class FileWriteToolTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@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 tempDir = Files.createTempDirectory("file_write_test")
|
||||||
val tool = FileWriteTool(allowedPaths = setOf(tempDir))
|
val tool = FileWriteTool(allowedPaths = setOf(tempDir))
|
||||||
val request = createRequest(
|
val request = createRequest(
|
||||||
@@ -83,7 +83,7 @@ class FileWriteToolTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@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 tempDir = Files.createTempDirectory("file_write_test")
|
||||||
val tool = FileWriteTool(allowedPaths = setOf(tempDir))
|
val tool = FileWriteTool(allowedPaths = setOf(tempDir))
|
||||||
val request = createRequest(
|
val request = createRequest(
|
||||||
@@ -98,7 +98,7 @@ class FileWriteToolTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@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 tempDir = Files.createTempDirectory("file_write_test")
|
||||||
val tool = FileWriteTool(allowedPaths = setOf(tempDir))
|
val tool = FileWriteTool(allowedPaths = setOf(tempDir))
|
||||||
val request = createRequest(
|
val request = createRequest(
|
||||||
@@ -113,7 +113,7 @@ class FileWriteToolTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@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 tempDir = Files.createTempDirectory("file_write_test")
|
||||||
val tool = FileWriteTool(allowedPaths = setOf(tempDir))
|
val tool = FileWriteTool(allowedPaths = setOf(tempDir))
|
||||||
val request = createRequest(
|
val request = createRequest(
|
||||||
@@ -128,7 +128,7 @@ class FileWriteToolTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@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 tempDir = Files.createTempDirectory("file_write_test")
|
||||||
val tool = FileWriteTool(allowedPaths = setOf(tempDir))
|
val tool = FileWriteTool(allowedPaths = setOf(tempDir))
|
||||||
val filePath = tempDir.resolve("test.txt")
|
val filePath = tempDir.resolve("test.txt")
|
||||||
@@ -151,7 +151,7 @@ class FileWriteToolTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@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 tempDir = Files.createTempDirectory("file_write_test")
|
||||||
val tool = FileWriteTool(allowedPaths = setOf(tempDir))
|
val tool = FileWriteTool(allowedPaths = setOf(tempDir))
|
||||||
val filePath = tempDir.resolve("to_delete.txt")
|
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)
|
val result = tool.execute(request)
|
||||||
println("Result: $result")
|
|
||||||
println("Exists after: ${Files.exists(filePath)}")
|
|
||||||
assertTrue(result is ToolResult.Success)
|
assertTrue(result is ToolResult.Success)
|
||||||
val success = result as ToolResult.Success
|
val success = result as ToolResult.Success
|
||||||
assertEquals("File deleted successfully: $filePath", success.output)
|
assertEquals("File deleted successfully: $filePath", success.output)
|
||||||
@@ -178,7 +173,7 @@ class FileWriteToolTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@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 tempDir = Files.createTempDirectory("file_write_test")
|
||||||
val tool = FileWriteTool(allowedPaths = setOf(tempDir))
|
val tool = FileWriteTool(allowedPaths = setOf(tempDir))
|
||||||
val filePath = tempDir.resolve("non_existent.txt")
|
val filePath = tempDir.resolve("non_existent.txt")
|
||||||
@@ -199,7 +194,7 @@ class FileWriteToolTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@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 tempDir = Files.createTempDirectory("file_write_test")
|
||||||
val otherDir = Files.createTempDirectory("other_dir")
|
val otherDir = Files.createTempDirectory("other_dir")
|
||||||
val tool = FileWriteTool(allowedPaths = setOf(tempDir))
|
val tool = FileWriteTool(allowedPaths = setOf(tempDir))
|
||||||
@@ -221,7 +216,7 @@ class FileWriteToolTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@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 tempDir = Files.createTempDirectory("file_write_test_existing")
|
||||||
val filePath = tempDir.resolve("existing.txt")
|
val filePath = tempDir.resolve("existing.txt")
|
||||||
Files.writeString(filePath, "content")
|
Files.writeString(filePath, "content")
|
||||||
|
|||||||
-1
@@ -21,7 +21,6 @@ import java.io.IOException
|
|||||||
import java.nio.file.Files
|
import java.nio.file.Files
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import java.nio.file.StandardCopyOption
|
import java.nio.file.StandardCopyOption
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
@SuppressWarnings("TooManyFunctions", "LongParameterList")
|
@SuppressWarnings("TooManyFunctions", "LongParameterList")
|
||||||
class SandboxedToolExecutor(
|
class SandboxedToolExecutor(
|
||||||
|
|||||||
+25
-11
@@ -1,7 +1,7 @@
|
|||||||
package com.correx.infrastructure.tools
|
package com.correx.infrastructure.tools
|
||||||
|
|
||||||
import com.correx.core.artifactstore.ArtifactStore
|
|
||||||
import com.correx.core.artifacts.MaterializingArtifactWriter
|
import com.correx.core.artifacts.MaterializingArtifactWriter
|
||||||
|
import com.correx.core.artifactstore.ArtifactStore
|
||||||
import com.correx.core.tools.contract.Tool
|
import com.correx.core.tools.contract.Tool
|
||||||
import com.correx.infrastructure.tools.filesystem.FileEditTool
|
import com.correx.infrastructure.tools.filesystem.FileEditTool
|
||||||
import com.correx.infrastructure.tools.filesystem.FileReadTool
|
import com.correx.infrastructure.tools.filesystem.FileReadTool
|
||||||
@@ -25,41 +25,55 @@ data class FileWriteConfig(
|
|||||||
val sandboxRoot: Path? = null,
|
val sandboxRoot: Path? = null,
|
||||||
val workingDir: Path? = null,
|
val workingDir: Path? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class FileEditConfig(
|
data class FileEditConfig(
|
||||||
val enabled: Boolean = false,
|
val enabled: Boolean = false,
|
||||||
val allowedPaths: Set<Path> = emptySet(),
|
val allowedPaths: Set<Path> = emptySet(),
|
||||||
val workingDir: Path? = null,
|
val workingDir: Path? = null,
|
||||||
)
|
)
|
||||||
data class ShellConfig(val enabled: Boolean = false, val allowedExecutables: Set<String> = emptySet(), val workingDir: Path? = null)
|
|
||||||
|
data class ShellConfig(
|
||||||
|
val enabled: Boolean = false,
|
||||||
|
val allowedExecutables: Set<String> = emptySet(),
|
||||||
|
val workingDir: Path? = null,
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extension function to convert [ToolConfig] into a list of [Tool] implementations.
|
* Extension function to convert [ToolConfig] into a list of [Tool] implementations.
|
||||||
*/
|
*/
|
||||||
fun ToolConfig.buildTools(): List<Tool> = buildList {
|
fun ToolConfig.buildTools(): List<Tool> = buildList {
|
||||||
if (shell.enabled) {
|
if (shell.enabled) {
|
||||||
add(ShellTool(
|
add(
|
||||||
|
ShellTool(
|
||||||
allowedExecutables = shell.allowedExecutables,
|
allowedExecutables = shell.allowedExecutables,
|
||||||
workingDir = shell.workingDir,
|
workingDir = shell.workingDir,
|
||||||
))
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if (fileRead.enabled) {
|
if (fileRead.enabled) {
|
||||||
add(FileReadTool(
|
add(
|
||||||
allowedPaths = fileRead.allowedPaths
|
FileReadTool(
|
||||||
))
|
allowedPaths = fileRead.allowedPaths,
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if (fileWrite.enabled) {
|
if (fileWrite.enabled) {
|
||||||
add(FileWriteTool(
|
add(
|
||||||
|
FileWriteTool(
|
||||||
allowedPaths = fileWrite.allowedPaths,
|
allowedPaths = fileWrite.allowedPaths,
|
||||||
artifactStore = fileWrite.artifactStore,
|
artifactStore = fileWrite.artifactStore,
|
||||||
materializingWriter = fileWrite.materializingWriter,
|
materializingWriter = fileWrite.materializingWriter,
|
||||||
sandboxRoot = fileWrite.sandboxRoot,
|
sandboxRoot = fileWrite.sandboxRoot,
|
||||||
workingDir = fileWrite.workingDir,
|
workingDir = fileWrite.workingDir,
|
||||||
))
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if (fileEdit.enabled) {
|
if (fileEdit.enabled) {
|
||||||
add(FileEditTool(
|
add(
|
||||||
|
FileEditTool(
|
||||||
allowedPaths = fileEdit.allowedPaths,
|
allowedPaths = fileEdit.allowedPaths,
|
||||||
workingDir = fileEdit.workingDir,
|
workingDir = fileEdit.workingDir,
|
||||||
))
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-7
@@ -30,14 +30,14 @@ class ShellToolTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@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 tool = ShellTool(allowedExecutables = setOf("echo"))
|
||||||
val request = createRequest(listOf("echo", "hello"))
|
val request = createRequest(listOf("echo", "hello"))
|
||||||
assertEquals(ValidationResult.Valid, tool.validateRequest(request))
|
assertEquals(ValidationResult.Valid, tool.validateRequest(request))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@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 tool = ShellTool(allowedExecutables = setOf("echo"))
|
||||||
val request = createRequest(listOf("ls"))
|
val request = createRequest(listOf("ls"))
|
||||||
val result = tool.validateRequest(request)
|
val result = tool.validateRequest(request)
|
||||||
@@ -46,7 +46,7 @@ class ShellToolTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@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 tool = ShellTool(allowedExecutables = setOf("echo"))
|
||||||
val request = createRequest(emptyList())
|
val request = createRequest(emptyList())
|
||||||
val result = tool.validateRequest(request)
|
val result = tool.validateRequest(request)
|
||||||
@@ -58,7 +58,7 @@ class ShellToolTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@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 tool = ShellTool(allowedExecutables = setOf("echo"))
|
||||||
val request = ToolRequest(
|
val request = ToolRequest(
|
||||||
invocationId = invocationId,
|
invocationId = invocationId,
|
||||||
@@ -76,7 +76,7 @@ class ShellToolTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@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 tool = ShellTool(allowedExecutables = setOf("echo"))
|
||||||
val request = createRequest(listOf("echo", "hello"))
|
val request = createRequest(listOf("echo", "hello"))
|
||||||
val result = tool.execute(request)
|
val result = tool.execute(request)
|
||||||
@@ -89,7 +89,7 @@ class ShellToolTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@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 tool = ShellTool(allowedExecutables = setOf("false"))
|
||||||
val request = createRequest(listOf("false"))
|
val request = createRequest(listOf("false"))
|
||||||
val result = tool.execute(request)
|
val result = tool.execute(request)
|
||||||
@@ -101,7 +101,7 @@ class ShellToolTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `execute returns Failure on timeout`() = runBlocking {
|
fun `execute returns Failure on timeout`(): Unit = runBlocking {
|
||||||
// Using sleep command to simulate timeout
|
// Using sleep command to simulate timeout
|
||||||
val tool = ShellTool(allowedExecutables = setOf("sleep"), timeoutMs = 100L)
|
val tool = ShellTool(allowedExecutables = setOf("sleep"), timeoutMs = 100L)
|
||||||
val request = createRequest(listOf("sleep", "1"))
|
val request = createRequest(listOf("sleep", "1"))
|
||||||
|
|||||||
+8
-8
@@ -32,12 +32,12 @@ private data class ProducesEntry(
|
|||||||
private data class StageSection(
|
private data class StageSection(
|
||||||
val id: String = "",
|
val id: String = "",
|
||||||
val prompt: String? = null,
|
val prompt: String? = null,
|
||||||
@JsonProperty("system_prompt") val systemPrompt: String? = null,
|
@param:JsonProperty("system_prompt") val systemPrompt: String? = null,
|
||||||
val produces: List<ProducesEntry> = emptyList(),
|
val produces: List<ProducesEntry> = emptyList(),
|
||||||
val needs: List<String> = emptyList(),
|
val needs: List<String> = emptyList(),
|
||||||
@JsonProperty("allowed_tools") val allowedTools: List<String> = emptyList(),
|
@param:JsonProperty("allowed_tools") val allowedTools: List<String> = emptyList(),
|
||||||
@JsonProperty("token_budget") val tokenBudget: Int = 4096,
|
@param:JsonProperty("token_budget") val tokenBudget: Int = 4096,
|
||||||
@JsonProperty("max_retries") val maxRetries: Int = 3,
|
@param:JsonProperty("max_retries") val maxRetries: Int = 3,
|
||||||
)
|
)
|
||||||
|
|
||||||
// condition fields flattened into the transition row
|
// condition fields flattened into the transition row
|
||||||
@@ -45,10 +45,10 @@ private data class TransitionSection(
|
|||||||
val id: String = "",
|
val id: String = "",
|
||||||
val from: String = "",
|
val from: String = "",
|
||||||
val to: String = "",
|
val to: String = "",
|
||||||
@JsonProperty("condition_type") val conditionType: String = "",
|
@param:JsonProperty("condition_type") val conditionType: String = "",
|
||||||
@JsonProperty("condition_artifact_id") val conditionArtifactId: String? = null,
|
@param:JsonProperty("condition_artifact_id") val conditionArtifactId: String? = null,
|
||||||
@JsonProperty("condition_key") val conditionKey: String? = null,
|
@param:JsonProperty("condition_key") val conditionKey: String? = null,
|
||||||
@JsonProperty("condition_value") val conditionValue: String? = null,
|
@param:JsonProperty("condition_value") val conditionValue: String? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
private const val TERMINAL = "done"
|
private const val TERMINAL = "done"
|
||||||
|
|||||||
+1
-1
@@ -4,8 +4,8 @@ import org.junit.jupiter.api.Test
|
|||||||
import org.junit.jupiter.api.assertThrows
|
import org.junit.jupiter.api.assertThrows
|
||||||
import java.nio.file.Files
|
import java.nio.file.Files
|
||||||
import kotlin.io.path.writeText
|
import kotlin.io.path.writeText
|
||||||
import kotlin.test.assertEquals
|
|
||||||
import kotlin.test.assertContains
|
import kotlin.test.assertContains
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
class FileSystemPromptLoaderTest {
|
class FileSystemPromptLoaderTest {
|
||||||
|
|
||||||
|
|||||||
+4
@@ -17,6 +17,7 @@ class WorkflowGraphTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `graph stores immutable structure`() {
|
fun `graph stores immutable structure`() {
|
||||||
val graph = WorkflowGraph(
|
val graph = WorkflowGraph(
|
||||||
|
id = "workflow-test",
|
||||||
stages = mapOf(s1 to StageConfig(), s2 to StageConfig()),
|
stages = mapOf(s1 to StageConfig(), s2 to StageConfig()),
|
||||||
transitions = setOf(
|
transitions = setOf(
|
||||||
TransitionEdge(
|
TransitionEdge(
|
||||||
@@ -39,6 +40,7 @@ class WorkflowGraphTest {
|
|||||||
val node = s1
|
val node = s1
|
||||||
|
|
||||||
val graph = WorkflowGraph(
|
val graph = WorkflowGraph(
|
||||||
|
id = "workflow-test",
|
||||||
stages = mapOf(node to StageConfig()),
|
stages = mapOf(node to StageConfig()),
|
||||||
transitions = emptySet(),
|
transitions = emptySet(),
|
||||||
start = s1
|
start = s1
|
||||||
@@ -51,12 +53,14 @@ class WorkflowGraphTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `graph equality is structural`() {
|
fun `graph equality is structural`() {
|
||||||
val g1 = WorkflowGraph(
|
val g1 = WorkflowGraph(
|
||||||
|
id = "workflow-test",
|
||||||
stages = mapOf(s1 to StageConfig()),
|
stages = mapOf(s1 to StageConfig()),
|
||||||
transitions = emptySet(),
|
transitions = emptySet(),
|
||||||
start = s1
|
start = s1
|
||||||
)
|
)
|
||||||
|
|
||||||
val g2 = WorkflowGraph(
|
val g2 = WorkflowGraph(
|
||||||
|
id = "workflow-test",
|
||||||
stages = mapOf(s1 to StageConfig()),
|
stages = mapOf(s1 to StageConfig()),
|
||||||
transitions = emptySet(),
|
transitions = emptySet(),
|
||||||
start = s1
|
start = s1
|
||||||
|
|||||||
+5
@@ -14,11 +14,15 @@ import org.junit.jupiter.api.Assertions.assertEquals
|
|||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
class EventsTest {
|
class EventsTest {
|
||||||
|
|
||||||
|
private val workflowId = "workflow-test"
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `WorkflowStartedEvent survives round-trip`() {
|
fun `WorkflowStartedEvent survives round-trip`() {
|
||||||
val event: EventPayload = WorkflowStartedEvent(
|
val event: EventPayload = WorkflowStartedEvent(
|
||||||
sessionId = SessionId("s1"),
|
sessionId = SessionId("s1"),
|
||||||
startStageId = StageId("stage-a"),
|
startStageId = StageId("stage-a"),
|
||||||
|
workflowId = workflowId,
|
||||||
)
|
)
|
||||||
|
|
||||||
val json = eventJson.encodeToString(EventPayload.serializer(), event)
|
val json = eventJson.encodeToString(EventPayload.serializer(), event)
|
||||||
@@ -33,6 +37,7 @@ class EventsTest {
|
|||||||
sessionId = SessionId("s1"),
|
sessionId = SessionId("s1"),
|
||||||
terminalStageId = StageId("stage-a"),
|
terminalStageId = StageId("stage-a"),
|
||||||
totalStages = 1,
|
totalStages = 1,
|
||||||
|
workflowId = workflowId,
|
||||||
)
|
)
|
||||||
|
|
||||||
val json = eventJson.encodeToString(EventPayload.serializer(), event)
|
val json = eventJson.encodeToString(EventPayload.serializer(), event)
|
||||||
|
|||||||
@@ -103,6 +103,7 @@ class GraphValidatorTest {
|
|||||||
)
|
)
|
||||||
|
|
||||||
val graph = WorkflowGraph(
|
val graph = WorkflowGraph(
|
||||||
|
id = "workflow-test",
|
||||||
stages = mapOf(a to StageConfig(), b to StageConfig()),
|
stages = mapOf(a to StageConfig(), b to StageConfig()),
|
||||||
transitions = setOf(edge1, edge2),
|
transitions = setOf(edge1, edge2),
|
||||||
start = a
|
start = a
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import com.correx.core.context.model.CompressionMetadata
|
|
||||||
import com.correx.core.context.model.ContextLayer
|
import com.correx.core.context.model.ContextLayer
|
||||||
import com.correx.core.context.model.TokenBudget
|
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.DefaultRouterContextBuilder
|
||||||
import com.correx.core.router.model.RouterConfig
|
import com.correx.core.router.model.RouterConfig
|
||||||
import com.correx.core.router.model.RouterL2Entry
|
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.StageOutcomeKind
|
||||||
import com.correx.core.router.model.TurnRole
|
import com.correx.core.router.model.TurnRole
|
||||||
import com.correx.core.router.model.WorkflowStatus
|
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.Clock
|
||||||
import kotlinx.datetime.Instant
|
import kotlinx.datetime.Instant
|
||||||
import org.junit.jupiter.api.Assertions.assertEquals
|
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")),
|
RouterTurn(TurnRole.ROUTER, "second reply", Instant.parse("2026-01-04T00:00:00Z")),
|
||||||
),
|
),
|
||||||
l2Memory = listOf(
|
l2Memory = listOf(
|
||||||
RouterL2Entry(StageId("s1"), "stage one done", StageOutcomeKind.SUCCESS, Instant.parse("2026-01-01T10:00:00Z")),
|
RouterL2Entry(
|
||||||
RouterL2Entry(StageId("s2"), "stage two failed", StageOutcomeKind.FAILURE, Instant.parse("2026-01-02T10:00:00Z")),
|
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))
|
val pack = builder.build(state, TokenBudget(limit = 10000))
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
|
import com.correx.core.context.model.ContextPack
|
||||||
import com.correx.core.context.model.TokenBudget
|
import com.correx.core.context.model.TokenBudget
|
||||||
import com.correx.core.events.events.NewEvent
|
import com.correx.core.events.events.NewEvent
|
||||||
import com.correx.core.events.events.SteeringNoteAddedEvent
|
import com.correx.core.events.events.SteeringNoteAddedEvent
|
||||||
import com.correx.core.events.events.StoredEvent
|
import com.correx.core.events.events.StoredEvent
|
||||||
import com.correx.core.events.stores.EventStore
|
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.InferenceRequestId
|
||||||
import com.correx.core.events.types.SessionId
|
import com.correx.core.events.types.SessionId
|
||||||
import com.correx.core.events.types.StageId
|
import com.correx.core.events.types.StageId
|
||||||
import com.correx.core.inference.GenerationConfig
|
|
||||||
import com.correx.core.inference.InferenceProvider
|
import com.correx.core.inference.InferenceProvider
|
||||||
import com.correx.core.inference.InferenceRequest
|
import com.correx.core.inference.InferenceRequest
|
||||||
import com.correx.core.inference.InferenceResponse
|
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.ModelCapability
|
||||||
import com.correx.core.inference.TokenUsage
|
import com.correx.core.inference.TokenUsage
|
||||||
import com.correx.core.router.ChatMode
|
import com.correx.core.router.ChatMode
|
||||||
import com.correx.core.router.DefaultRouterContextBuilder
|
|
||||||
import com.correx.core.router.DefaultRouterFacade
|
import com.correx.core.router.DefaultRouterFacade
|
||||||
import com.correx.core.router.RouterContextBuilder
|
import com.correx.core.router.RouterContextBuilder
|
||||||
import com.correx.core.router.RouterFacade
|
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.RouterState
|
||||||
import com.correx.core.router.model.TurnRole
|
import com.correx.core.router.model.TurnRole
|
||||||
import com.correx.core.router.model.WorkflowStatus
|
import com.correx.core.router.model.WorkflowStatus
|
||||||
import com.correx.core.context.model.ContextPack
|
import kotlinx.coroutines.runBlocking
|
||||||
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 org.junit.jupiter.api.Assertions.assertEquals
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
import org.junit.jupiter.api.Assertions.assertFalse
|
import org.junit.jupiter.api.Assertions.assertFalse
|
||||||
import org.junit.jupiter.api.Assertions.assertNotNull
|
import org.junit.jupiter.api.Assertions.assertNotNull
|
||||||
import org.junit.jupiter.api.Assertions.assertNull
|
import org.junit.jupiter.api.Assertions.assertNull
|
||||||
import org.junit.jupiter.api.Assertions.assertTrue
|
import org.junit.jupiter.api.Assertions.assertTrue
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
|
|
||||||
class RouterFacadeTest {
|
class RouterFacadeTest {
|
||||||
|
|
||||||
@@ -44,7 +39,7 @@ class RouterFacadeTest {
|
|||||||
// --------------------------------------------------------------------------
|
// --------------------------------------------------------------------------
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `CHAT mode returns inference response content`() = runBlocking {
|
fun `CHAT mode returns inference response content`(): Unit = runBlocking {
|
||||||
val mockStore = mockEventStore()
|
val mockStore = mockEventStore()
|
||||||
val facade = facadeWithMocks(eventStore = mockStore, chatMode = ChatMode.CHAT)
|
val facade = facadeWithMocks(eventStore = mockStore, chatMode = ChatMode.CHAT)
|
||||||
val response = facade.onUserInput(sessionId = SessionId("test-session"), input = "Hello, world!")
|
val response = facade.onUserInput(sessionId = SessionId("test-session"), input = "Hello, world!")
|
||||||
@@ -52,7 +47,7 @@ class RouterFacadeTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `CHAT mode sets steeringEmitted to false`() = runBlocking {
|
fun `CHAT mode sets steeringEmitted to false`(): Unit = runBlocking {
|
||||||
val mockStore = mockEventStore()
|
val mockStore = mockEventStore()
|
||||||
val facade = facadeWithMocks(eventStore = mockStore, chatMode = ChatMode.CHAT)
|
val facade = facadeWithMocks(eventStore = mockStore, chatMode = ChatMode.CHAT)
|
||||||
val response = facade.onUserInput(sessionId = SessionId("test-session"), input = "Hello!")
|
val response = facade.onUserInput(sessionId = SessionId("test-session"), input = "Hello!")
|
||||||
@@ -60,7 +55,7 @@ class RouterFacadeTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `CHAT mode does not append to EventStore`() = runBlocking {
|
fun `CHAT mode does not append to EventStore`(): Unit = runBlocking {
|
||||||
val mockStore = mockEventStore()
|
val mockStore = mockEventStore()
|
||||||
val facade = facadeWithMocks(eventStore = mockStore, chatMode = ChatMode.CHAT)
|
val facade = facadeWithMocks(eventStore = mockStore, chatMode = ChatMode.CHAT)
|
||||||
facade.onUserInput(sessionId = SessionId("test-session"), input = "Hello!")
|
facade.onUserInput(sessionId = SessionId("test-session"), input = "Hello!")
|
||||||
@@ -72,7 +67,7 @@ class RouterFacadeTest {
|
|||||||
// --------------------------------------------------------------------------
|
// --------------------------------------------------------------------------
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `STEERING mode sets steeringEmitted to true`() = runBlocking {
|
fun `STEERING mode sets steeringEmitted to true`(): Unit = runBlocking {
|
||||||
val mockStore = mockEventStore()
|
val mockStore = mockEventStore()
|
||||||
val facade = facadeWithMocks(eventStore = mockStore, chatMode = ChatMode.STEERING)
|
val facade = facadeWithMocks(eventStore = mockStore, chatMode = ChatMode.STEERING)
|
||||||
val response = facade.onUserInput(sessionId = SessionId("test-session"), input = "Hello!")
|
val response = facade.onUserInput(sessionId = SessionId("test-session"), input = "Hello!")
|
||||||
@@ -80,7 +75,7 @@ class RouterFacadeTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@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 mockStore = mockEventStore()
|
||||||
val facade = facadeWithMocks(eventStore = mockStore, chatMode = ChatMode.STEERING)
|
val facade = facadeWithMocks(eventStore = mockStore, chatMode = ChatMode.STEERING)
|
||||||
facade.onUserInput(sessionId = SessionId("session-xyz"), input = "steer this way")
|
facade.onUserInput(sessionId = SessionId("session-xyz"), input = "steer this way")
|
||||||
@@ -93,13 +88,17 @@ class RouterFacadeTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@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 mockStore = mockEventStore()
|
||||||
val stageId = StageId("stage-A")
|
val stageId = StageId("stage-A")
|
||||||
val facade = DefaultRouterFacade(
|
val facade = DefaultRouterFacade(
|
||||||
routerRepository = object : RouterRepository {
|
routerRepository = object : RouterRepository {
|
||||||
override suspend fun getRouterState(sessionId: SessionId): RouterState =
|
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 {
|
routerContextBuilder = object : RouterContextBuilder {
|
||||||
override fun build(state: RouterState, budget: TokenBudget): ContextPack = emptyContextPack()
|
override fun build(state: RouterState, budget: TokenBudget): ContextPack = emptyContextPack()
|
||||||
@@ -114,7 +113,7 @@ class RouterFacadeTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@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 mockStore = mockEventStore()
|
||||||
val facade = DefaultRouterFacade(
|
val facade = DefaultRouterFacade(
|
||||||
routerRepository = object : RouterRepository {
|
routerRepository = object : RouterRepository {
|
||||||
@@ -138,12 +137,16 @@ class RouterFacadeTest {
|
|||||||
// --------------------------------------------------------------------------
|
// --------------------------------------------------------------------------
|
||||||
|
|
||||||
@Test
|
@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<RouterState>()
|
val capturedStates = mutableListOf<RouterState>()
|
||||||
val facade = DefaultRouterFacade(
|
val facade = DefaultRouterFacade(
|
||||||
routerRepository = object : RouterRepository {
|
routerRepository = object : RouterRepository {
|
||||||
override suspend fun getRouterState(sessionId: SessionId): RouterState =
|
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 {
|
routerContextBuilder = object : RouterContextBuilder {
|
||||||
override fun build(state: RouterState, budget: TokenBudget): ContextPack {
|
override fun build(state: RouterState, budget: TokenBudget): ContextPack {
|
||||||
@@ -171,7 +174,7 @@ class RouterFacadeTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@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<RouterState>()
|
val capturedStates = mutableListOf<RouterState>()
|
||||||
val facade = DefaultRouterFacade(
|
val facade = DefaultRouterFacade(
|
||||||
routerRepository = object : RouterRepository {
|
routerRepository = object : RouterRepository {
|
||||||
@@ -204,7 +207,7 @@ class RouterFacadeTest {
|
|||||||
// --------------------------------------------------------------------------
|
// --------------------------------------------------------------------------
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `state is passed through to context builder`() = runBlocking {
|
fun `state is passed through to context builder`(): Unit = runBlocking {
|
||||||
val capturedState = mutableListOf<RouterState>()
|
val capturedState = mutableListOf<RouterState>()
|
||||||
val mockContextBuilder = object : RouterContextBuilder {
|
val mockContextBuilder = object : RouterContextBuilder {
|
||||||
override fun build(state: RouterState, budget: TokenBudget): ContextPack {
|
override fun build(state: RouterState, budget: TokenBudget): ContextPack {
|
||||||
@@ -215,7 +218,11 @@ class RouterFacadeTest {
|
|||||||
val facade = DefaultRouterFacade(
|
val facade = DefaultRouterFacade(
|
||||||
routerRepository = object : RouterRepository {
|
routerRepository = object : RouterRepository {
|
||||||
override suspend fun getRouterState(sessionId: SessionId): RouterState =
|
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,
|
routerContextBuilder = mockContextBuilder,
|
||||||
inferenceRouter = mockInferenceRouter("inference response"),
|
inferenceRouter = mockInferenceRouter("inference response"),
|
||||||
@@ -229,7 +236,7 @@ class RouterFacadeTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `budget is passed through to context builder`() = runBlocking {
|
fun `budget is passed through to context builder`(): Unit = runBlocking {
|
||||||
val capturedBudget = mutableListOf<TokenBudget>()
|
val capturedBudget = mutableListOf<TokenBudget>()
|
||||||
val mockContextBuilder = object : RouterContextBuilder {
|
val mockContextBuilder = object : RouterContextBuilder {
|
||||||
override fun build(state: RouterState, budget: TokenBudget): ContextPack {
|
override fun build(state: RouterState, budget: TokenBudget): ContextPack {
|
||||||
@@ -252,10 +259,13 @@ class RouterFacadeTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `stage ID uses state currentStageId`() = runBlocking {
|
fun `stage ID uses state currentStageId`(): Unit = runBlocking {
|
||||||
val capturedStageId = mutableListOf<StageId>()
|
val capturedStageId = mutableListOf<StageId>()
|
||||||
val mockInferenceRouter = object : InferenceRouter {
|
val mockInferenceRouter = object : InferenceRouter {
|
||||||
override suspend fun route(stageId: StageId, requiredCapabilities: Set<ModelCapability>): InferenceProvider {
|
override suspend fun route(
|
||||||
|
stageId: StageId,
|
||||||
|
requiredCapabilities: Set<ModelCapability>,
|
||||||
|
): InferenceProvider {
|
||||||
capturedStageId.add(stageId)
|
capturedStageId.add(stageId)
|
||||||
return mockProvider("response")
|
return mockProvider("response")
|
||||||
}
|
}
|
||||||
@@ -281,10 +291,13 @@ class RouterFacadeTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@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<StageId>()
|
val capturedStageId = mutableListOf<StageId>()
|
||||||
val mockInferenceRouter = object : InferenceRouter {
|
val mockInferenceRouter = object : InferenceRouter {
|
||||||
override suspend fun route(stageId: StageId, requiredCapabilities: Set<ModelCapability>): InferenceProvider {
|
override suspend fun route(
|
||||||
|
stageId: StageId,
|
||||||
|
requiredCapabilities: Set<ModelCapability>,
|
||||||
|
): InferenceProvider {
|
||||||
capturedStageId.add(stageId)
|
capturedStageId.add(stageId)
|
||||||
return mockProvider("response")
|
return mockProvider("response")
|
||||||
}
|
}
|
||||||
@@ -309,10 +322,13 @@ class RouterFacadeTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `new InferenceRequestId per call`() = runBlocking {
|
fun `new InferenceRequestId per call`(): Unit = runBlocking {
|
||||||
val capturedRequestIds = mutableListOf<InferenceRequestId>()
|
val capturedRequestIds = mutableListOf<InferenceRequestId>()
|
||||||
val mockInferenceRouter = object : InferenceRouter {
|
val mockInferenceRouter = object : InferenceRouter {
|
||||||
override suspend fun route(stageId: StageId, requiredCapabilities: Set<ModelCapability>): InferenceProvider {
|
override suspend fun route(
|
||||||
|
stageId: StageId,
|
||||||
|
requiredCapabilities: Set<ModelCapability>,
|
||||||
|
): InferenceProvider {
|
||||||
return mockProviderWithCapture("response", capturedRequestIds)
|
return mockProviderWithCapture("response", capturedRequestIds)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -334,10 +350,13 @@ class RouterFacadeTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@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<InferenceRequest>()
|
val capturedRequests = mutableListOf<InferenceRequest>()
|
||||||
val mockInferenceRouter = object : InferenceRouter {
|
val mockInferenceRouter = object : InferenceRouter {
|
||||||
override suspend fun route(stageId: StageId, requiredCapabilities: Set<ModelCapability>): InferenceProvider {
|
override suspend fun route(
|
||||||
|
stageId: StageId,
|
||||||
|
requiredCapabilities: Set<ModelCapability>,
|
||||||
|
): InferenceProvider {
|
||||||
return mockProviderWithRequestCapture("response", capturedRequests)
|
return mockProviderWithRequestCapture("response", capturedRequests)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -360,7 +379,7 @@ class RouterFacadeTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `context pack is passed to inference provider`() = runBlocking {
|
fun `context pack is passed to inference provider`(): Unit = runBlocking {
|
||||||
val capturedContextPacks = mutableListOf<ContextPack>()
|
val capturedContextPacks = mutableListOf<ContextPack>()
|
||||||
val facade = DefaultRouterFacade(
|
val facade = DefaultRouterFacade(
|
||||||
routerRepository = object : RouterRepository {
|
routerRepository = object : RouterRepository {
|
||||||
@@ -381,7 +400,10 @@ class RouterFacadeTest {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
inferenceRouter = object : InferenceRouter {
|
inferenceRouter = object : InferenceRouter {
|
||||||
override suspend fun route(stageId: StageId, requiredCapabilities: Set<ModelCapability>): InferenceProvider {
|
override suspend fun route(
|
||||||
|
stageId: StageId,
|
||||||
|
requiredCapabilities: Set<ModelCapability>,
|
||||||
|
): InferenceProvider {
|
||||||
return object : InferenceProvider {
|
return object : InferenceProvider {
|
||||||
override val id = com.correx.core.events.types.ProviderId("mock")
|
override val id = com.correx.core.events.types.ProviderId("mock")
|
||||||
override val name = "Mock"
|
override val name = "Mock"
|
||||||
@@ -397,8 +419,10 @@ class RouterFacadeTest {
|
|||||||
latencyMs = 0,
|
latencyMs = 0,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun healthCheck(): com.correx.core.inference.ProviderHealth =
|
override suspend fun healthCheck(): com.correx.core.inference.ProviderHealth =
|
||||||
com.correx.core.inference.ProviderHealth.Healthy
|
com.correx.core.inference.ProviderHealth.Healthy
|
||||||
|
|
||||||
override fun capabilities(): Set<com.correx.core.inference.CapabilityScore> = emptySet()
|
override fun capabilities(): Set<com.correx.core.inference.CapabilityScore> = emptySet()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -410,7 +434,7 @@ class RouterFacadeTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `responseFormat defaults to Text`() = runBlocking {
|
fun `responseFormat defaults to Text`(): Unit = runBlocking {
|
||||||
val capturedRequests = mutableListOf<InferenceRequest>()
|
val capturedRequests = mutableListOf<InferenceRequest>()
|
||||||
val facade = DefaultRouterFacade(
|
val facade = DefaultRouterFacade(
|
||||||
routerRepository = object : RouterRepository {
|
routerRepository = object : RouterRepository {
|
||||||
@@ -420,7 +444,10 @@ class RouterFacadeTest {
|
|||||||
override fun build(state: RouterState, budget: TokenBudget): ContextPack = emptyContextPack()
|
override fun build(state: RouterState, budget: TokenBudget): ContextPack = emptyContextPack()
|
||||||
},
|
},
|
||||||
inferenceRouter = object : InferenceRouter {
|
inferenceRouter = object : InferenceRouter {
|
||||||
override suspend fun route(stageId: StageId, requiredCapabilities: Set<ModelCapability>): InferenceProvider {
|
override suspend fun route(
|
||||||
|
stageId: StageId,
|
||||||
|
requiredCapabilities: Set<ModelCapability>,
|
||||||
|
): InferenceProvider {
|
||||||
return mockProviderWithRequestCapture("response", capturedRequests)
|
return mockProviderWithRequestCapture("response", capturedRequests)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -433,7 +460,7 @@ class RouterFacadeTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@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 mockStore = mockEventStore()
|
||||||
val facade = facadeWithMocks(eventStore = mockStore, chatMode = ChatMode.STEERING)
|
val facade = facadeWithMocks(eventStore = mockStore, chatMode = ChatMode.STEERING)
|
||||||
val response = facade.onUserInput(sessionId = SessionId("test-session"), input = "Hello!")
|
val response = facade.onUserInput(sessionId = SessionId("test-session"), input = "Hello!")
|
||||||
@@ -455,7 +482,11 @@ class RouterFacadeTest {
|
|||||||
): RouterFacade = DefaultRouterFacade(
|
): RouterFacade = DefaultRouterFacade(
|
||||||
routerRepository = object : RouterRepository {
|
routerRepository = object : RouterRepository {
|
||||||
override suspend fun getRouterState(sessionId: SessionId): RouterState =
|
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 {
|
routerContextBuilder = object : RouterContextBuilder {
|
||||||
override fun build(state: RouterState, budget: TokenBudget): ContextPack = emptyContextPack()
|
override fun build(state: RouterState, budget: TokenBudget): ContextPack = emptyContextPack()
|
||||||
@@ -472,7 +503,10 @@ class RouterFacadeTest {
|
|||||||
|
|
||||||
private fun mockInferenceRouter(responseText: String): InferenceRouter =
|
private fun mockInferenceRouter(responseText: String): InferenceRouter =
|
||||||
object : InferenceRouter {
|
object : InferenceRouter {
|
||||||
override suspend fun route(stageId: StageId, requiredCapabilities: Set<ModelCapability>): InferenceProvider =
|
override suspend fun route(
|
||||||
|
stageId: StageId,
|
||||||
|
requiredCapabilities: Set<ModelCapability>,
|
||||||
|
): InferenceProvider =
|
||||||
mockProvider(responseText)
|
mockProvider(responseText)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -488,8 +522,10 @@ class RouterFacadeTest {
|
|||||||
tokensUsed = TokenUsage(promptTokens = 10, completionTokens = 5),
|
tokensUsed = TokenUsage(promptTokens = 10, completionTokens = 5),
|
||||||
latencyMs = 0,
|
latencyMs = 0,
|
||||||
)
|
)
|
||||||
|
|
||||||
override suspend fun healthCheck(): com.correx.core.inference.ProviderHealth =
|
override suspend fun healthCheck(): com.correx.core.inference.ProviderHealth =
|
||||||
com.correx.core.inference.ProviderHealth.Healthy
|
com.correx.core.inference.ProviderHealth.Healthy
|
||||||
|
|
||||||
override fun capabilities(): Set<com.correx.core.inference.CapabilityScore> =
|
override fun capabilities(): Set<com.correx.core.inference.CapabilityScore> =
|
||||||
setOf(com.correx.core.inference.CapabilityScore(ModelCapability.General, 1.0))
|
setOf(com.correx.core.inference.CapabilityScore(ModelCapability.General, 1.0))
|
||||||
}
|
}
|
||||||
@@ -512,8 +548,10 @@ class RouterFacadeTest {
|
|||||||
latencyMs = 0,
|
latencyMs = 0,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun healthCheck(): com.correx.core.inference.ProviderHealth =
|
override suspend fun healthCheck(): com.correx.core.inference.ProviderHealth =
|
||||||
com.correx.core.inference.ProviderHealth.Healthy
|
com.correx.core.inference.ProviderHealth.Healthy
|
||||||
|
|
||||||
override fun capabilities(): Set<com.correx.core.inference.CapabilityScore> = emptySet()
|
override fun capabilities(): Set<com.correx.core.inference.CapabilityScore> = emptySet()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -535,8 +573,10 @@ class RouterFacadeTest {
|
|||||||
latencyMs = 0,
|
latencyMs = 0,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun healthCheck(): com.correx.core.inference.ProviderHealth =
|
override suspend fun healthCheck(): com.correx.core.inference.ProviderHealth =
|
||||||
com.correx.core.inference.ProviderHealth.Healthy
|
com.correx.core.inference.ProviderHealth.Healthy
|
||||||
|
|
||||||
override fun capabilities(): Set<com.correx.core.inference.CapabilityScore> = emptySet()
|
override fun capabilities(): Set<com.correx.core.inference.CapabilityScore> = emptySet()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -568,16 +608,19 @@ class RouterFacadeTest {
|
|||||||
override suspend fun appendAll(events: List<NewEvent>): List<StoredEvent> =
|
override suspend fun appendAll(events: List<NewEvent>): List<StoredEvent> =
|
||||||
events.map { append(it) }
|
events.map { append(it) }
|
||||||
|
|
||||||
override fun read(sessionId: com.correx.core.events.types.SessionId): List<StoredEvent> =
|
override fun read(sessionId: SessionId): List<StoredEvent> =
|
||||||
storedEvents.values.filter { it.metadata.sessionId == sessionId }.toList()
|
storedEvents.values.filter { it.metadata.sessionId == sessionId }.toList()
|
||||||
|
|
||||||
override fun readFrom(sessionId: com.correx.core.events.types.SessionId, fromSequence: Long): List<StoredEvent> =
|
override fun readFrom(
|
||||||
|
sessionId: SessionId,
|
||||||
|
fromSequence: Long,
|
||||||
|
): List<StoredEvent> =
|
||||||
read(sessionId).filter { it.sequence >= fromSequence }
|
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 }
|
read(sessionId).maxOfOrNull { it.sequence }
|
||||||
|
|
||||||
override fun subscribe(sessionId: com.correx.core.events.types.SessionId): kotlinx.coroutines.flow.Flow<StoredEvent> =
|
override fun subscribe(sessionId: SessionId): kotlinx.coroutines.flow.Flow<StoredEvent> =
|
||||||
throw UnsupportedOperationException("subscribe not implemented for mock")
|
throw UnsupportedOperationException("subscribe not implemented for mock")
|
||||||
|
|
||||||
override fun allEvents(): Sequence<StoredEvent> =
|
override fun allEvents(): Sequence<StoredEvent> =
|
||||||
|
|||||||
@@ -7,13 +7,13 @@ import com.correx.core.events.events.ToolInvokedEvent
|
|||||||
import com.correx.core.events.events.WorkflowCompletedEvent
|
import com.correx.core.events.events.WorkflowCompletedEvent
|
||||||
import com.correx.core.events.events.WorkflowFailedEvent
|
import com.correx.core.events.events.WorkflowFailedEvent
|
||||||
import com.correx.core.events.events.WorkflowStartedEvent
|
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.DefaultRouterReducer
|
||||||
import com.correx.core.router.RouterProjector
|
import com.correx.core.router.RouterProjector
|
||||||
import com.correx.core.router.model.RouterState
|
import com.correx.core.router.model.RouterState
|
||||||
import com.correx.core.router.model.StageOutcomeKind
|
import com.correx.core.router.model.StageOutcomeKind
|
||||||
import com.correx.core.router.model.WorkflowStatus
|
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 com.correx.testing.fixtures.EventFixtures.stored
|
||||||
import org.junit.jupiter.api.Assertions.assertEquals
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
import org.junit.jupiter.api.Assertions.assertNull
|
import org.junit.jupiter.api.Assertions.assertNull
|
||||||
@@ -26,6 +26,7 @@ class RouterProjectorTest {
|
|||||||
private val projector = RouterProjector(reducer)
|
private val projector = RouterProjector(reducer)
|
||||||
private val sessionId = SessionId("s1")
|
private val sessionId = SessionId("s1")
|
||||||
private val stageId = StageId("stage-1")
|
private val stageId = StageId("stage-1")
|
||||||
|
private val workflowId = "workflow-test"
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `initial() returns RouterState with IDLE status and empty collections`() {
|
fun `initial() returns RouterState with IDLE status and empty collections`() {
|
||||||
@@ -49,7 +50,7 @@ class RouterProjectorTest {
|
|||||||
val state = projector.initial()
|
val state = projector.initial()
|
||||||
val updated = projector.apply(
|
val updated = projector.apply(
|
||||||
state,
|
state,
|
||||||
stored(payload = WorkflowStartedEvent(sessionId, stageId)),
|
stored(payload = WorkflowStartedEvent(sessionId, workflowId, stageId)),
|
||||||
)
|
)
|
||||||
assertEquals(sessionId, updated.sessionId)
|
assertEquals(sessionId, updated.sessionId)
|
||||||
assertEquals(WorkflowStatus.RUNNING, updated.workflowStatus)
|
assertEquals(WorkflowStatus.RUNNING, updated.workflowStatus)
|
||||||
@@ -60,11 +61,11 @@ class RouterProjectorTest {
|
|||||||
fun `apply(WorkflowCompletedEvent) delegates to reducer and sets COMPLETED`() {
|
fun `apply(WorkflowCompletedEvent) delegates to reducer and sets COMPLETED`() {
|
||||||
val started = projector.apply(
|
val started = projector.apply(
|
||||||
projector.initial(),
|
projector.initial(),
|
||||||
stored(payload = WorkflowStartedEvent(sessionId, stageId)),
|
stored(payload = WorkflowStartedEvent(sessionId, workflowId, stageId)),
|
||||||
)
|
)
|
||||||
val updated = projector.apply(
|
val updated = projector.apply(
|
||||||
started,
|
started,
|
||||||
stored(payload = WorkflowCompletedEvent(sessionId, stageId, 3)),
|
stored(payload = WorkflowCompletedEvent(sessionId, stageId, 3, workflowId)),
|
||||||
)
|
)
|
||||||
assertEquals(WorkflowStatus.COMPLETED, updated.workflowStatus)
|
assertEquals(WorkflowStatus.COMPLETED, updated.workflowStatus)
|
||||||
assertNull(updated.currentStageId)
|
assertNull(updated.currentStageId)
|
||||||
@@ -75,7 +76,7 @@ class RouterProjectorTest {
|
|||||||
fun `apply(WorkflowFailedEvent) delegates to reducer and sets FAILED`() {
|
fun `apply(WorkflowFailedEvent) delegates to reducer and sets FAILED`() {
|
||||||
val started = projector.apply(
|
val started = projector.apply(
|
||||||
projector.initial(),
|
projector.initial(),
|
||||||
stored(payload = WorkflowStartedEvent(sessionId, stageId)),
|
stored(payload = WorkflowStartedEvent(sessionId, workflowId, stageId)),
|
||||||
)
|
)
|
||||||
val updated = projector.apply(
|
val updated = projector.apply(
|
||||||
started,
|
started,
|
||||||
@@ -89,7 +90,7 @@ class RouterProjectorTest {
|
|||||||
fun `apply(OrchestrationPausedEvent) delegates to reducer and sets PAUSED`() {
|
fun `apply(OrchestrationPausedEvent) delegates to reducer and sets PAUSED`() {
|
||||||
val started = projector.apply(
|
val started = projector.apply(
|
||||||
projector.initial(),
|
projector.initial(),
|
||||||
stored(payload = WorkflowStartedEvent(sessionId, stageId)),
|
stored(payload = WorkflowStartedEvent(sessionId, workflowId, stageId)),
|
||||||
)
|
)
|
||||||
val updated = projector.apply(
|
val updated = projector.apply(
|
||||||
started,
|
started,
|
||||||
@@ -103,7 +104,7 @@ class RouterProjectorTest {
|
|||||||
fun `apply(OrchestrationResumedEvent) delegates to reducer and sets RUNNING`() {
|
fun `apply(OrchestrationResumedEvent) delegates to reducer and sets RUNNING`() {
|
||||||
val started = projector.apply(
|
val started = projector.apply(
|
||||||
projector.initial(),
|
projector.initial(),
|
||||||
stored(payload = WorkflowStartedEvent(sessionId, stageId)),
|
stored(payload = WorkflowStartedEvent(sessionId, workflowId, stageId)),
|
||||||
)
|
)
|
||||||
val paused = projector.apply(
|
val paused = projector.apply(
|
||||||
started,
|
started,
|
||||||
@@ -133,7 +134,7 @@ class RouterProjectorTest {
|
|||||||
fun `apply(StageFailedEvent) delegates to reducer and appends FAILURE entry`() {
|
fun `apply(StageFailedEvent) delegates to reducer and appends FAILURE entry`() {
|
||||||
val state = projector.apply(
|
val state = projector.apply(
|
||||||
projector.initial(),
|
projector.initial(),
|
||||||
stored(payload = WorkflowStartedEvent(sessionId, stageId)),
|
stored(payload = WorkflowStartedEvent(sessionId, workflowId, stageId)),
|
||||||
)
|
)
|
||||||
val updated = projector.apply(
|
val updated = projector.apply(
|
||||||
state,
|
state,
|
||||||
@@ -169,13 +170,13 @@ class RouterProjectorTest {
|
|||||||
fun `apply produces same result as reducer reduce for every handled event`() {
|
fun `apply produces same result as reducer reduce for every handled event`() {
|
||||||
val state = projector.initial()
|
val state = projector.initial()
|
||||||
val events = listOf(
|
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 = StageCompletedEvent(sessionId, StageId("a"), StageId("t1"))),
|
||||||
stored(payload = OrchestrationPausedEvent(sessionId, StageId("a"), "hold")),
|
stored(payload = OrchestrationPausedEvent(sessionId, StageId("a"), "hold")),
|
||||||
stored(payload = OrchestrationResumedEvent(sessionId, StageId("a"))),
|
stored(payload = OrchestrationResumedEvent(sessionId, StageId("a"))),
|
||||||
stored(payload = StageCompletedEvent(sessionId, StageId("b"), StageId("t2"))),
|
stored(payload = StageCompletedEvent(sessionId, StageId("b"), StageId("t2"))),
|
||||||
stored(payload = StageFailedEvent(sessionId, StageId("b"), StageId("t3"), "error")),
|
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
|
var projected = state
|
||||||
@@ -193,7 +194,7 @@ class RouterProjectorTest {
|
|||||||
|
|
||||||
state = projector.apply(
|
state = projector.apply(
|
||||||
state,
|
state,
|
||||||
stored(payload = WorkflowStartedEvent(sessionId, StageId("stage-A"))),
|
stored(payload = WorkflowStartedEvent(sessionId, workflowId, StageId("stage-A"))),
|
||||||
)
|
)
|
||||||
assertEquals(WorkflowStatus.RUNNING, state.workflowStatus)
|
assertEquals(WorkflowStatus.RUNNING, state.workflowStatus)
|
||||||
|
|
||||||
@@ -205,7 +206,7 @@ class RouterProjectorTest {
|
|||||||
|
|
||||||
state = projector.apply(
|
state = projector.apply(
|
||||||
state,
|
state,
|
||||||
stored(payload = WorkflowCompletedEvent(sessionId, StageId("stage-A"), 1)),
|
stored(payload = WorkflowCompletedEvent(sessionId, StageId("stage-A"), 1, workflowId)),
|
||||||
)
|
)
|
||||||
assertEquals(WorkflowStatus.COMPLETED, state.workflowStatus)
|
assertEquals(WorkflowStatus.COMPLETED, state.workflowStatus)
|
||||||
assertNull(state.currentStageId)
|
assertNull(state.currentStageId)
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
import com.correx.core.events.events.OrchestrationPausedEvent
|
import com.correx.core.events.events.OrchestrationPausedEvent
|
||||||
import com.correx.core.events.events.OrchestrationResumedEvent
|
import com.correx.core.events.events.OrchestrationResumedEvent
|
||||||
import com.correx.core.events.events.SteeringNoteAddedEvent
|
|
||||||
import com.correx.core.events.events.StageCompletedEvent
|
import com.correx.core.events.events.StageCompletedEvent
|
||||||
import com.correx.core.events.events.StageFailedEvent
|
import com.correx.core.events.events.StageFailedEvent
|
||||||
|
import com.correx.core.events.events.SteeringNoteAddedEvent
|
||||||
import com.correx.core.events.events.ToolInvokedEvent
|
import com.correx.core.events.events.ToolInvokedEvent
|
||||||
import com.correx.core.events.events.WorkflowCompletedEvent
|
import com.correx.core.events.events.WorkflowCompletedEvent
|
||||||
import com.correx.core.events.events.WorkflowFailedEvent
|
import com.correx.core.events.events.WorkflowFailedEvent
|
||||||
import com.correx.core.events.events.WorkflowStartedEvent
|
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.DefaultRouterReducer
|
||||||
import com.correx.core.router.model.StageOutcomeKind
|
import com.correx.core.router.model.StageOutcomeKind
|
||||||
import com.correx.core.router.model.WorkflowStatus
|
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 com.correx.testing.fixtures.EventFixtures.stored
|
||||||
import kotlinx.datetime.Instant
|
import kotlinx.datetime.Instant
|
||||||
import org.junit.jupiter.api.Assertions.assertEquals
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
@@ -25,6 +25,7 @@ class RouterReducerTest {
|
|||||||
private val reducer = DefaultRouterReducer()
|
private val reducer = DefaultRouterReducer()
|
||||||
private val sessionId = SessionId("s1")
|
private val sessionId = SessionId("s1")
|
||||||
private val stageId = StageId("stage-1")
|
private val stageId = StageId("stage-1")
|
||||||
|
private val workflowId = "workflow-test"
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `initial state has IDLE status and empty collections`() {
|
fun `initial state has IDLE status and empty collections`() {
|
||||||
@@ -41,7 +42,7 @@ class RouterReducerTest {
|
|||||||
val state = reducer.initial
|
val state = reducer.initial
|
||||||
val updated = reducer.reduce(
|
val updated = reducer.reduce(
|
||||||
state,
|
state,
|
||||||
stored(payload = WorkflowStartedEvent(sessionId, stageId)),
|
stored(payload = WorkflowStartedEvent(sessionId, workflowId, stageId)),
|
||||||
)
|
)
|
||||||
assertEquals(sessionId, updated.sessionId)
|
assertEquals(sessionId, updated.sessionId)
|
||||||
assertEquals(WorkflowStatus.RUNNING, updated.workflowStatus)
|
assertEquals(WorkflowStatus.RUNNING, updated.workflowStatus)
|
||||||
@@ -53,11 +54,11 @@ class RouterReducerTest {
|
|||||||
val initial = reducer.initial
|
val initial = reducer.initial
|
||||||
val started = reducer.reduce(
|
val started = reducer.reduce(
|
||||||
initial,
|
initial,
|
||||||
stored(payload = WorkflowStartedEvent(sessionId, stageId)),
|
stored(payload = WorkflowStartedEvent(sessionId, workflowId, stageId)),
|
||||||
)
|
)
|
||||||
val completed = reducer.reduce(
|
val completed = reducer.reduce(
|
||||||
started,
|
started,
|
||||||
stored(payload = WorkflowCompletedEvent(sessionId, stageId, 3)),
|
stored(payload = WorkflowCompletedEvent(sessionId, stageId, 3, workflowId)),
|
||||||
)
|
)
|
||||||
assertEquals(WorkflowStatus.COMPLETED, completed.workflowStatus)
|
assertEquals(WorkflowStatus.COMPLETED, completed.workflowStatus)
|
||||||
assertNull(completed.currentStageId)
|
assertNull(completed.currentStageId)
|
||||||
@@ -69,7 +70,7 @@ class RouterReducerTest {
|
|||||||
val initial = reducer.initial
|
val initial = reducer.initial
|
||||||
val started = reducer.reduce(
|
val started = reducer.reduce(
|
||||||
initial,
|
initial,
|
||||||
stored(payload = WorkflowStartedEvent(sessionId, stageId)),
|
stored(payload = WorkflowStartedEvent(sessionId, workflowId, stageId)),
|
||||||
)
|
)
|
||||||
val failed = reducer.reduce(
|
val failed = reducer.reduce(
|
||||||
started,
|
started,
|
||||||
@@ -84,7 +85,7 @@ class RouterReducerTest {
|
|||||||
val initial = reducer.initial
|
val initial = reducer.initial
|
||||||
val started = reducer.reduce(
|
val started = reducer.reduce(
|
||||||
initial,
|
initial,
|
||||||
stored(payload = WorkflowStartedEvent(sessionId, stageId)),
|
stored(payload = WorkflowStartedEvent(sessionId, workflowId, stageId)),
|
||||||
)
|
)
|
||||||
val paused = reducer.reduce(
|
val paused = reducer.reduce(
|
||||||
started,
|
started,
|
||||||
@@ -99,7 +100,7 @@ class RouterReducerTest {
|
|||||||
val initial = reducer.initial
|
val initial = reducer.initial
|
||||||
val started = reducer.reduce(
|
val started = reducer.reduce(
|
||||||
initial,
|
initial,
|
||||||
stored(payload = WorkflowStartedEvent(sessionId, stageId)),
|
stored(payload = WorkflowStartedEvent(sessionId, workflowId, stageId)),
|
||||||
)
|
)
|
||||||
val paused = reducer.reduce(
|
val paused = reducer.reduce(
|
||||||
started,
|
started,
|
||||||
@@ -130,7 +131,7 @@ class RouterReducerTest {
|
|||||||
val initial = reducer.initial
|
val initial = reducer.initial
|
||||||
val started = reducer.reduce(
|
val started = reducer.reduce(
|
||||||
initial,
|
initial,
|
||||||
stored(payload = WorkflowStartedEvent(sessionId, stageId)),
|
stored(payload = WorkflowStartedEvent(sessionId, workflowId, stageId)),
|
||||||
)
|
)
|
||||||
val updated = reducer.reduce(
|
val updated = reducer.reduce(
|
||||||
started,
|
started,
|
||||||
@@ -146,7 +147,7 @@ class RouterReducerTest {
|
|||||||
val initial = reducer.initial
|
val initial = reducer.initial
|
||||||
val started = reducer.reduce(
|
val started = reducer.reduce(
|
||||||
initial,
|
initial,
|
||||||
stored(payload = WorkflowStartedEvent(sessionId, stageId)),
|
stored(payload = WorkflowStartedEvent(sessionId, workflowId, stageId)),
|
||||||
)
|
)
|
||||||
val storedEvent = stored(payload = StageFailedEvent(sessionId, stageId, StageId("t1"), "timeout"))
|
val storedEvent = stored(payload = StageFailedEvent(sessionId, stageId, StageId("t1"), "timeout"))
|
||||||
val updated = reducer.reduce(started, storedEvent)
|
val updated = reducer.reduce(started, storedEvent)
|
||||||
@@ -189,7 +190,7 @@ class RouterReducerTest {
|
|||||||
val initial = reducer.initial
|
val initial = reducer.initial
|
||||||
val started = reducer.reduce(
|
val started = reducer.reduce(
|
||||||
initial,
|
initial,
|
||||||
stored(payload = WorkflowStartedEvent(sessionId, stageId)),
|
stored(payload = WorkflowStartedEvent(sessionId, workflowId, stageId)),
|
||||||
)
|
)
|
||||||
val withNote = reducer.reduce(
|
val withNote = reducer.reduce(
|
||||||
started,
|
started,
|
||||||
@@ -223,7 +224,7 @@ class RouterReducerTest {
|
|||||||
val s0 = reducer.initial
|
val s0 = reducer.initial
|
||||||
assertEquals(WorkflowStatus.IDLE, s0.workflowStatus)
|
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(WorkflowStatus.RUNNING, s1.workflowStatus)
|
||||||
assertEquals(StageId("stage-A"), s1.currentStageId)
|
assertEquals(StageId("stage-A"), s1.currentStageId)
|
||||||
|
|
||||||
@@ -231,11 +232,15 @@ class RouterReducerTest {
|
|||||||
assertEquals(1, s2.l2Memory.size)
|
assertEquals(1, s2.l2Memory.size)
|
||||||
assertEquals(StageOutcomeKind.SUCCESS, s2.l2Memory[0].outcome)
|
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)
|
assertEquals(WorkflowStatus.COMPLETED, s3.workflowStatus)
|
||||||
assertNull(s3.currentStageId)
|
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)
|
assertEquals(WorkflowStatus.PAUSED, s4.workflowStatus)
|
||||||
|
|
||||||
val s5 = reducer.reduce(s4, stored(payload = OrchestrationResumedEvent(sessionId, StageId("stage-A"))))
|
val s5 = reducer.reduce(s4, stored(payload = OrchestrationResumedEvent(sessionId, StageId("stage-A"))))
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import com.correx.core.events.types.InferenceRequestId
|
|||||||
import com.correx.core.events.types.ProviderId
|
import com.correx.core.events.types.ProviderId
|
||||||
import com.correx.core.events.types.SessionId
|
import com.correx.core.events.types.SessionId
|
||||||
import com.correx.core.events.types.StageId
|
import com.correx.core.events.types.StageId
|
||||||
import com.correx.core.utils.TypeId
|
|
||||||
import com.correx.core.inference.FinishReason
|
import com.correx.core.inference.FinishReason
|
||||||
import com.correx.core.inference.GenerationConfig
|
import com.correx.core.inference.GenerationConfig
|
||||||
import com.correx.core.inference.InferenceProvider
|
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.InferenceRouter
|
||||||
import com.correx.core.inference.ModelCapability
|
import com.correx.core.inference.ModelCapability
|
||||||
import com.correx.core.inference.TokenUsage
|
import com.correx.core.inference.TokenUsage
|
||||||
|
import com.correx.core.utils.TypeId
|
||||||
import com.correx.testing.fixtures.inference.MockInferenceProvider
|
import com.correx.testing.fixtures.inference.MockInferenceProvider
|
||||||
import kotlinx.datetime.Clock
|
import kotlinx.datetime.Clock
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ object WorkflowFixtures {
|
|||||||
)
|
)
|
||||||
|
|
||||||
return WorkflowGraph(
|
return WorkflowGraph(
|
||||||
|
id = "workflow-test",
|
||||||
stages = mapOf(a to StageConfig(), b to StageConfig()),
|
stages = mapOf(a to StageConfig(), b to StageConfig()),
|
||||||
transitions = setOf(t1),
|
transitions = setOf(t1),
|
||||||
start = a
|
start = a
|
||||||
|
|||||||
+2
-1
@@ -32,12 +32,13 @@ object TransitionFixtures {
|
|||||||
val c = StageId("C")
|
val c = StageId("C")
|
||||||
|
|
||||||
return WorkflowGraph(
|
return WorkflowGraph(
|
||||||
|
id = "workflow-test",
|
||||||
stages = mapOf(a to StageConfig(), b to StageConfig(), c to StageConfig()),
|
stages = mapOf(a to StageConfig(), b to StageConfig(), c to StageConfig()),
|
||||||
transitions = setOf(
|
transitions = setOf(
|
||||||
TransitionEdge(id = TransitionId("t1"), from = a, to = b, condition = { true }),
|
TransitionEdge(id = TransitionId("t1"), from = a, to = b, condition = { true }),
|
||||||
TransitionEdge(id = TransitionId("t2"), from = b, to = c, condition = { true }),
|
TransitionEdge(id = TransitionId("t2"), from = b, to = c, condition = { true }),
|
||||||
),
|
),
|
||||||
start = a
|
start = a,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,13 +5,18 @@ import com.correx.core.approvals.domain.DefaultApprovalEngine
|
|||||||
import com.correx.core.approvals.model.ApprovalContext
|
import com.correx.core.approvals.model.ApprovalContext
|
||||||
import com.correx.core.approvals.model.ApprovalDecision
|
import com.correx.core.approvals.model.ApprovalDecision
|
||||||
import com.correx.core.approvals.model.ApprovalScopeIdentity
|
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.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.OrchestrationPausedEvent
|
||||||
import com.correx.core.events.events.OrchestrationResumedEvent
|
import com.correx.core.events.events.OrchestrationResumedEvent
|
||||||
import com.correx.core.events.events.WorkflowCompletedEvent
|
import com.correx.core.events.events.WorkflowCompletedEvent
|
||||||
import com.correx.core.events.events.WorkflowFailedEvent
|
import com.correx.core.events.events.WorkflowFailedEvent
|
||||||
import com.correx.core.events.events.WorkflowStartedEvent
|
import com.correx.core.events.events.WorkflowStartedEvent
|
||||||
import com.correx.core.events.execution.RetryPolicy
|
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.SessionId
|
||||||
import com.correx.core.events.types.StageId
|
import com.correx.core.events.types.StageId
|
||||||
import com.correx.core.inference.InferenceRepository
|
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.DefaultEventReplayer
|
||||||
import com.correx.core.sessions.projections.replay.EventReplayer
|
import com.correx.core.sessions.projections.replay.EventReplayer
|
||||||
import com.correx.core.transitions.graph.StageConfig
|
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.transitions.graph.WorkflowGraph
|
||||||
|
import com.correx.core.utils.TypeId
|
||||||
import com.correx.core.validation.approval.ApprovalTrigger
|
import com.correx.core.validation.approval.ApprovalTrigger
|
||||||
import com.correx.core.validation.pipeline.ValidationPipeline
|
import com.correx.core.validation.pipeline.ValidationPipeline
|
||||||
import com.correx.core.artifacts.DefaultArtifactReducer
|
|
||||||
import com.correx.infrastructure.persistence.InMemoryEventStore
|
import com.correx.infrastructure.persistence.InMemoryEventStore
|
||||||
import com.correx.infrastructure.persistence.artifact.LiveArtifactRepository
|
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.InferenceFixtures
|
||||||
import com.correx.testing.fixtures.context.ContextFixtures
|
import com.correx.testing.fixtures.context.ContextFixtures
|
||||||
import com.correx.testing.fixtures.cyclePolicyMissingValidator
|
import com.correx.testing.fixtures.cyclePolicyMissingValidator
|
||||||
import com.correx.testing.fixtures.inference.MockInferenceProvider
|
import com.correx.testing.fixtures.inference.MockInferenceProvider
|
||||||
import com.correx.testing.fixtures.transitions.TransitionFixtures
|
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.fixtures.transitions.TransitionFixtures.threeStageGraph
|
||||||
import com.correx.testing.kernel.MockSessionEventReplayer
|
import com.correx.testing.kernel.MockSessionEventReplayer
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -209,9 +210,10 @@ class SessionOrchestratorIntegrationTest {
|
|||||||
// Single-stage graph: enterStage(A) triggers exactly one approval; after resume,
|
// Single-stage graph: enterStage(A) triggers exactly one approval; after resume,
|
||||||
// the always-true transition moves to the terminal "done" and the workflow completes.
|
// the always-true transition moves to the terminal "done" and the workflow completes.
|
||||||
val graph = WorkflowGraph(
|
val graph = WorkflowGraph(
|
||||||
|
id = "workflow-test",
|
||||||
stages = mapOf(StageId("A") to StageConfig()),
|
stages = mapOf(StageId("A") to StageConfig()),
|
||||||
transitions = setOf(
|
transitions = setOf(
|
||||||
com.correx.core.transitions.graph.TransitionEdge(
|
TransitionEdge(
|
||||||
id = com.correx.core.events.types.TransitionId("t1"),
|
id = com.correx.core.events.types.TransitionId("t1"),
|
||||||
from = StageId("A"),
|
from = StageId("A"),
|
||||||
to = StageId("done"),
|
to = StageId("done"),
|
||||||
@@ -268,6 +270,7 @@ class SessionOrchestratorIntegrationTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `workflow with empty graph fails immediately`(): Unit = runBlocking {
|
fun `workflow with empty graph fails immediately`(): Unit = runBlocking {
|
||||||
val emptyGraph = WorkflowGraph(
|
val emptyGraph = WorkflowGraph(
|
||||||
|
id = "workflow-test",
|
||||||
stages = mapOf(StageId("start") to StageConfig()),
|
stages = mapOf(StageId("start") to StageConfig()),
|
||||||
transitions = emptySet(),
|
transitions = emptySet(),
|
||||||
start = StageId("start"),
|
start = StageId("start"),
|
||||||
@@ -310,8 +313,8 @@ class SessionOrchestratorIntegrationTest {
|
|||||||
assertTrue(recordingStore.puts.size >= 2, "Expected at least 2 puts, got ${recordingStore.puts.size}")
|
assertTrue(recordingStore.puts.size >= 2, "Expected at least 2 puts, got ${recordingStore.puts.size}")
|
||||||
|
|
||||||
val events = eventStore.read(sessionId)
|
val events = eventStore.read(sessionId)
|
||||||
val started = events.mapNotNull { it.payload as? InferenceStartedEvent }.first()
|
val started = events.firstNotNullOf { it.payload as? InferenceStartedEvent }
|
||||||
val completed = events.mapNotNull { it.payload as? InferenceCompletedEvent }.first()
|
val completed = events.firstNotNullOf { it.payload as? InferenceCompletedEvent }
|
||||||
|
|
||||||
assertEquals(recordingStore.idForIndex(0), started.promptArtifactId)
|
assertEquals(recordingStore.idForIndex(0), started.promptArtifactId)
|
||||||
assertEquals(recordingStore.idForIndex(1), completed.responseArtifactId)
|
assertEquals(recordingStore.idForIndex(1), completed.responseArtifactId)
|
||||||
@@ -325,17 +328,18 @@ class SessionOrchestratorIntegrationTest {
|
|||||||
val z = StageId("Z")
|
val z = StageId("Z")
|
||||||
// ghost is missing from stages map but has an outgoing transition so it is non-terminal
|
// ghost is missing from stages map but has an outgoing transition so it is non-terminal
|
||||||
val brokenGraph = WorkflowGraph(
|
val brokenGraph = WorkflowGraph(
|
||||||
|
id = "workflow-test",
|
||||||
stages = mapOf(a to StageConfig(), b to StageConfig()),
|
stages = mapOf(a to StageConfig(), b to StageConfig()),
|
||||||
transitions = setOf(
|
transitions = setOf(
|
||||||
com.correx.core.transitions.graph.TransitionEdge(
|
TransitionEdge(
|
||||||
id = com.correx.core.events.types.TransitionId("t1"), from = a, to = b,
|
id = com.correx.core.events.types.TransitionId("t1"), from = a, to = b,
|
||||||
condition = { true },
|
condition = { true },
|
||||||
),
|
),
|
||||||
com.correx.core.transitions.graph.TransitionEdge(
|
TransitionEdge(
|
||||||
id = com.correx.core.events.types.TransitionId("t2"), from = b, to = ghost,
|
id = com.correx.core.events.types.TransitionId("t2"), from = b, to = ghost,
|
||||||
condition = { true },
|
condition = { true },
|
||||||
),
|
),
|
||||||
com.correx.core.transitions.graph.TransitionEdge(
|
TransitionEdge(
|
||||||
id = com.correx.core.events.types.TransitionId("t3"), from = ghost, to = z,
|
id = com.correx.core.events.types.TransitionId("t3"), from = ghost, to = z,
|
||||||
condition = { true },
|
condition = { true },
|
||||||
),
|
),
|
||||||
@@ -391,5 +395,7 @@ private class RecordingArtifactStore : ArtifactStore {
|
|||||||
|
|
||||||
override suspend fun get(id: ArtifactId): ByteArray? = byId[id]
|
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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ class ValidationPipelineIntegrationTest {
|
|||||||
val a = StageId("A")
|
val a = StageId("A")
|
||||||
val ghost = StageId("GHOST")
|
val ghost = StageId("GHOST")
|
||||||
val graphWithDanglingTransition = WorkflowGraph(
|
val graphWithDanglingTransition = WorkflowGraph(
|
||||||
|
id = "workflow-test",
|
||||||
stages = mapOf(a to StageConfig()),
|
stages = mapOf(a to StageConfig()),
|
||||||
transitions = setOf(TransitionEdge(TransitionId("t1"), from = a, to = ghost) { true }),
|
transitions = setOf(TransitionEdge(TransitionId("t1"), from = a, to = ghost) { true }),
|
||||||
start = a,
|
start = a,
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ class OrchestrationProjectorTest {
|
|||||||
private val projector = OrchestrationProjector(reducer)
|
private val projector = OrchestrationProjector(reducer)
|
||||||
private val sessionId = SessionId("s1")
|
private val sessionId = SessionId("s1")
|
||||||
private val stageId = StageId("stage-1")
|
private val stageId = StageId("stage-1")
|
||||||
|
private val workflowId = "workflow-test"
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `should initialize orchestration with IDLE status`() {
|
fun `should initialize orchestration with IDLE status`() {
|
||||||
@@ -32,7 +33,7 @@ class OrchestrationProjectorTest {
|
|||||||
fun `should set RUNNING on WorkflowStartedEvent`() {
|
fun `should set RUNNING on WorkflowStartedEvent`() {
|
||||||
val initialState = projector.initial()
|
val initialState = projector.initial()
|
||||||
val started =
|
val started =
|
||||||
projector.apply(initialState, stored(payload = WorkflowStartedEvent(sessionId, stageId)))
|
projector.apply(initialState, stored(payload = WorkflowStartedEvent(sessionId, workflowId, stageId)))
|
||||||
assertEquals(OrchestrationStatus.RUNNING, started.status)
|
assertEquals(OrchestrationStatus.RUNNING, started.status)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,7 +41,7 @@ class OrchestrationProjectorTest {
|
|||||||
fun `should set RUNNING after resume on pause`() {
|
fun `should set RUNNING after resume on pause`() {
|
||||||
val initialState = projector.initial()
|
val initialState = projector.initial()
|
||||||
val started =
|
val started =
|
||||||
projector.apply(initialState, stored(payload = WorkflowStartedEvent(sessionId, stageId)))
|
projector.apply(initialState, stored(payload = WorkflowStartedEvent(sessionId, workflowId, stageId)))
|
||||||
assertEquals(OrchestrationStatus.RUNNING, started.status)
|
assertEquals(OrchestrationStatus.RUNNING, started.status)
|
||||||
val paused =
|
val paused =
|
||||||
projector.apply(started, stored(payload = OrchestrationPausedEvent(sessionId, stageId, "Tool approval")))
|
projector.apply(started, stored(payload = OrchestrationPausedEvent(sessionId, stageId, "Tool approval")))
|
||||||
@@ -56,7 +57,7 @@ class OrchestrationProjectorTest {
|
|||||||
fun `should set FAILED on WorkflowFailedEvent`() {
|
fun `should set FAILED on WorkflowFailedEvent`() {
|
||||||
val initialState = projector.initial()
|
val initialState = projector.initial()
|
||||||
val started =
|
val started =
|
||||||
projector.apply(initialState, stored(payload = WorkflowStartedEvent(sessionId, stageId)))
|
projector.apply(initialState, stored(payload = WorkflowStartedEvent(sessionId, workflowId, stageId)))
|
||||||
val failed =
|
val failed =
|
||||||
projector.apply(
|
projector.apply(
|
||||||
started,
|
started,
|
||||||
@@ -69,7 +70,7 @@ class OrchestrationProjectorTest {
|
|||||||
fun `same events produce same result`() {
|
fun `same events produce same result`() {
|
||||||
val initialState = projector.initial()
|
val initialState = projector.initial()
|
||||||
val events = listOf(
|
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 = WorkflowFailedEvent(sessionId, stageId, "Something went wrong", false)),
|
||||||
stored(
|
stored(
|
||||||
payload = RetryAttemptedEvent(
|
payload = RetryAttemptedEvent(
|
||||||
@@ -80,7 +81,7 @@ class OrchestrationProjectorTest {
|
|||||||
failureReason = "Something went wrong",
|
failureReason = "Something went wrong",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
stored(payload = WorkflowCompletedEvent(sessionId, stageId, 1)),
|
stored(payload = WorkflowCompletedEvent(sessionId, stageId, 1, workflowId)),
|
||||||
)
|
)
|
||||||
|
|
||||||
val result1 = events.fold(initialState) { state, event ->
|
val result1 = events.fold(initialState) { state, event ->
|
||||||
@@ -96,11 +97,11 @@ class OrchestrationProjectorTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `full pause-resume lifecycle`() {
|
fun `full pause-resume lifecycle`() {
|
||||||
val s0 = projector.initial()
|
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 =
|
val s2 =
|
||||||
projector.apply(s1, stored(payload = OrchestrationPausedEvent(sessionId, stageId, "approval required")))
|
projector.apply(s1, stored(payload = OrchestrationPausedEvent(sessionId, stageId, "approval required")))
|
||||||
val s3 = projector.apply(s2, stored(payload = OrchestrationResumedEvent(sessionId, stageId)))
|
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.RUNNING, s1.status)
|
||||||
assertEquals(OrchestrationStatus.PAUSED, s2.status)
|
assertEquals(OrchestrationStatus.PAUSED, s2.status)
|
||||||
|
|||||||
@@ -22,12 +22,13 @@ class OrchestrationReducerTest {
|
|||||||
private val sessionId = SessionId("s1")
|
private val sessionId = SessionId("s1")
|
||||||
private val stageId = StageId("stage-1")
|
private val stageId = StageId("stage-1")
|
||||||
private val state = OrchestrationState(stageId)
|
private val state = OrchestrationState(stageId)
|
||||||
|
private val workflowId = "workflow-test"
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `WorkflowStartedEvent sets status to RUNNING`() {
|
fun `WorkflowStartedEvent sets status to RUNNING`() {
|
||||||
val state = reducer.reduce(
|
val state = reducer.reduce(
|
||||||
state,
|
state,
|
||||||
stored(payload = WorkflowStartedEvent(sessionId, stageId)),
|
stored(payload = WorkflowStartedEvent(sessionId, workflowId, stageId)),
|
||||||
)
|
)
|
||||||
assertEquals(OrchestrationStatus.RUNNING, state.status)
|
assertEquals(OrchestrationStatus.RUNNING, state.status)
|
||||||
assertEquals(stageId, state.currentStageId)
|
assertEquals(stageId, state.currentStageId)
|
||||||
@@ -37,7 +38,7 @@ class OrchestrationReducerTest {
|
|||||||
fun `WorkflowFailedEvent sets status to FAILED and failure reason`() {
|
fun `WorkflowFailedEvent sets status to FAILED and failure reason`() {
|
||||||
val started = reducer.reduce(
|
val started = reducer.reduce(
|
||||||
state,
|
state,
|
||||||
stored(payload = WorkflowStartedEvent(sessionId, stageId)),
|
stored(payload = WorkflowStartedEvent(sessionId, workflowId, stageId)),
|
||||||
)
|
)
|
||||||
|
|
||||||
val failed = reducer.reduce(
|
val failed = reducer.reduce(
|
||||||
@@ -52,12 +53,12 @@ class OrchestrationReducerTest {
|
|||||||
fun `WorkflowCompletedEvent sets status to COMPLETED`() {
|
fun `WorkflowCompletedEvent sets status to COMPLETED`() {
|
||||||
val started = reducer.reduce(
|
val started = reducer.reduce(
|
||||||
state,
|
state,
|
||||||
stored(payload = WorkflowStartedEvent(sessionId, stageId)),
|
stored(payload = WorkflowStartedEvent(sessionId, workflowId, stageId)),
|
||||||
)
|
)
|
||||||
|
|
||||||
val completed = reducer.reduce(
|
val completed = reducer.reduce(
|
||||||
started,
|
started,
|
||||||
stored(payload = WorkflowCompletedEvent(sessionId, stageId, 1)),
|
stored(payload = WorkflowCompletedEvent(sessionId, stageId, 1, workflowId)),
|
||||||
)
|
)
|
||||||
assertTrue(completed.failureReason.isNullOrBlank())
|
assertTrue(completed.failureReason.isNullOrBlank())
|
||||||
assertEquals(OrchestrationStatus.COMPLETED, completed.status)
|
assertEquals(OrchestrationStatus.COMPLETED, completed.status)
|
||||||
@@ -68,7 +69,7 @@ class OrchestrationReducerTest {
|
|||||||
fun `OrchestrationPausedEvent sets status to PAUSED with reason and pending approval`() {
|
fun `OrchestrationPausedEvent sets status to PAUSED with reason and pending approval`() {
|
||||||
val started = reducer.reduce(
|
val started = reducer.reduce(
|
||||||
state,
|
state,
|
||||||
stored(payload = WorkflowStartedEvent(sessionId, stageId)),
|
stored(payload = WorkflowStartedEvent(sessionId, workflowId, stageId)),
|
||||||
)
|
)
|
||||||
|
|
||||||
val paused = reducer.reduce(
|
val paused = reducer.reduce(
|
||||||
@@ -84,7 +85,7 @@ class OrchestrationReducerTest {
|
|||||||
fun `OrchestrationResumedEvent sets status to RUNNING with null reason and no pending approval`() {
|
fun `OrchestrationResumedEvent sets status to RUNNING with null reason and no pending approval`() {
|
||||||
val started = reducer.reduce(
|
val started = reducer.reduce(
|
||||||
state,
|
state,
|
||||||
stored(payload = WorkflowStartedEvent(sessionId, stageId)),
|
stored(payload = WorkflowStartedEvent(sessionId, workflowId, stageId)),
|
||||||
)
|
)
|
||||||
|
|
||||||
val paused = reducer.reduce(
|
val paused = reducer.reduce(
|
||||||
@@ -105,7 +106,7 @@ class OrchestrationReducerTest {
|
|||||||
fun `RetryAttemptedEvent sets status to RUNNING with null reason and no pending approval`() {
|
fun `RetryAttemptedEvent sets status to RUNNING with null reason and no pending approval`() {
|
||||||
val started = reducer.reduce(
|
val started = reducer.reduce(
|
||||||
state,
|
state,
|
||||||
stored(payload = WorkflowStartedEvent(sessionId, stageId)),
|
stored(payload = WorkflowStartedEvent(sessionId, workflowId, stageId)),
|
||||||
)
|
)
|
||||||
|
|
||||||
val failed = reducer.reduce(
|
val failed = reducer.reduce(
|
||||||
@@ -137,7 +138,7 @@ class OrchestrationReducerTest {
|
|||||||
fun `unrelated event does nothing`() {
|
fun `unrelated event does nothing`() {
|
||||||
val started = reducer.reduce(
|
val started = reducer.reduce(
|
||||||
state,
|
state,
|
||||||
stored(payload = WorkflowStartedEvent(sessionId, stageId)),
|
stored(payload = WorkflowStartedEvent(sessionId, workflowId, stageId)),
|
||||||
)
|
)
|
||||||
|
|
||||||
val unrelated = reducer.reduce(started, stored())
|
val unrelated = reducer.reduce(started, stored())
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
import com.correx.core.events.events.OrchestrationPausedEvent
|
import com.correx.core.events.events.OrchestrationPausedEvent
|
||||||
import com.correx.core.events.events.OrchestrationResumedEvent
|
import com.correx.core.events.events.OrchestrationResumedEvent
|
||||||
import com.correx.core.events.events.SteeringNoteAddedEvent
|
|
||||||
import com.correx.core.events.events.StageCompletedEvent
|
import com.correx.core.events.events.StageCompletedEvent
|
||||||
import com.correx.core.events.events.StageFailedEvent
|
import com.correx.core.events.events.StageFailedEvent
|
||||||
|
import com.correx.core.events.events.SteeringNoteAddedEvent
|
||||||
import com.correx.core.events.events.WorkflowCompletedEvent
|
import com.correx.core.events.events.WorkflowCompletedEvent
|
||||||
import com.correx.core.events.events.WorkflowFailedEvent
|
import com.correx.core.events.events.WorkflowFailedEvent
|
||||||
import com.correx.core.events.events.WorkflowStartedEvent
|
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.DefaultRouterReducer
|
||||||
import com.correx.core.router.RouterProjector
|
import com.correx.core.router.RouterProjector
|
||||||
import com.correx.core.router.model.StageOutcomeKind
|
import com.correx.core.router.model.StageOutcomeKind
|
||||||
import com.correx.core.router.model.WorkflowStatus
|
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 com.correx.testing.fixtures.EventFixtures.stored
|
||||||
import org.junit.jupiter.api.Assertions.assertEquals
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
import org.junit.jupiter.api.Assertions.assertNull
|
import org.junit.jupiter.api.Assertions.assertNull
|
||||||
@@ -23,6 +23,7 @@ class RouterProjectorTest {
|
|||||||
private val projector = RouterProjector(reducer)
|
private val projector = RouterProjector(reducer)
|
||||||
private val sessionId = SessionId("s1")
|
private val sessionId = SessionId("s1")
|
||||||
private val stageId = StageId("stage-1")
|
private val stageId = StageId("stage-1")
|
||||||
|
private val workflowId = "workflow-test"
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `initial state has IDLE status and empty collections`() {
|
fun `initial state has IDLE status and empty collections`() {
|
||||||
@@ -37,7 +38,7 @@ class RouterProjectorTest {
|
|||||||
fun `should set RUNNING on WorkflowStartedEvent`() {
|
fun `should set RUNNING on WorkflowStartedEvent`() {
|
||||||
val initialState = projector.initial()
|
val initialState = projector.initial()
|
||||||
val started =
|
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(WorkflowStatus.RUNNING, started.workflowStatus)
|
||||||
assertEquals(stageId, started.currentStageId)
|
assertEquals(stageId, started.currentStageId)
|
||||||
}
|
}
|
||||||
@@ -46,9 +47,9 @@ class RouterProjectorTest {
|
|||||||
fun `should set COMPLETED on WorkflowCompletedEvent`() {
|
fun `should set COMPLETED on WorkflowCompletedEvent`() {
|
||||||
val initialState = projector.initial()
|
val initialState = projector.initial()
|
||||||
val started =
|
val started =
|
||||||
projector.apply(initialState, stored(payload = WorkflowStartedEvent(sessionId, stageId)))
|
projector.apply(initialState, stored(payload = WorkflowStartedEvent(sessionId, workflowId, stageId)))
|
||||||
val completed =
|
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)
|
assertEquals(WorkflowStatus.COMPLETED, completed.workflowStatus)
|
||||||
assertNull(completed.currentStageId)
|
assertNull(completed.currentStageId)
|
||||||
}
|
}
|
||||||
@@ -57,7 +58,7 @@ class RouterProjectorTest {
|
|||||||
fun `should set FAILED on WorkflowFailedEvent`() {
|
fun `should set FAILED on WorkflowFailedEvent`() {
|
||||||
val initialState = projector.initial()
|
val initialState = projector.initial()
|
||||||
val started =
|
val started =
|
||||||
projector.apply(initialState, stored(payload = WorkflowStartedEvent(sessionId, stageId)))
|
projector.apply(initialState, stored(payload = WorkflowStartedEvent(sessionId, workflowId, stageId)))
|
||||||
val failed =
|
val failed =
|
||||||
projector.apply(
|
projector.apply(
|
||||||
started,
|
started,
|
||||||
@@ -71,7 +72,7 @@ class RouterProjectorTest {
|
|||||||
fun `should set PAUSED on OrchestrationPausedEvent`() {
|
fun `should set PAUSED on OrchestrationPausedEvent`() {
|
||||||
val initialState = projector.initial()
|
val initialState = projector.initial()
|
||||||
val started =
|
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(WorkflowStatus.RUNNING, started.workflowStatus)
|
||||||
val paused =
|
val paused =
|
||||||
projector.apply(started, stored(payload = OrchestrationPausedEvent(sessionId, stageId, "Tool approval")))
|
projector.apply(started, stored(payload = OrchestrationPausedEvent(sessionId, stageId, "Tool approval")))
|
||||||
@@ -82,7 +83,7 @@ class RouterProjectorTest {
|
|||||||
fun `should set RUNNING after resume on pause`() {
|
fun `should set RUNNING after resume on pause`() {
|
||||||
val initialState = projector.initial()
|
val initialState = projector.initial()
|
||||||
val started =
|
val started =
|
||||||
projector.apply(initialState, stored(payload = WorkflowStartedEvent(sessionId, stageId)))
|
projector.apply(initialState, stored(payload = WorkflowStartedEvent(sessionId, workflowId, stageId)))
|
||||||
val paused =
|
val paused =
|
||||||
projector.apply(started, stored(payload = OrchestrationPausedEvent(sessionId, stageId, "Tool approval")))
|
projector.apply(started, stored(payload = OrchestrationPausedEvent(sessionId, stageId, "Tool approval")))
|
||||||
assertEquals(WorkflowStatus.PAUSED, paused.workflowStatus)
|
assertEquals(WorkflowStatus.PAUSED, paused.workflowStatus)
|
||||||
@@ -105,7 +106,7 @@ class RouterProjectorTest {
|
|||||||
fun `should append l2Memory entry and clear currentStageId on StageFailedEvent`() {
|
fun `should append l2Memory entry and clear currentStageId on StageFailedEvent`() {
|
||||||
val initialState = projector.initial()
|
val initialState = projector.initial()
|
||||||
val started =
|
val started =
|
||||||
projector.apply(initialState, stored(payload = WorkflowStartedEvent(sessionId, stageId)))
|
projector.apply(initialState, stored(payload = WorkflowStartedEvent(sessionId, workflowId, stageId)))
|
||||||
assertEquals(stageId, started.currentStageId)
|
assertEquals(stageId, started.currentStageId)
|
||||||
val failed =
|
val failed =
|
||||||
projector.apply(started, stored(payload = StageFailedEvent(sessionId, stageId, StageId("t1"), "timeout")))
|
projector.apply(started, stored(payload = StageFailedEvent(sessionId, stageId, StageId("t1"), "timeout")))
|
||||||
@@ -129,11 +130,11 @@ class RouterProjectorTest {
|
|||||||
fun `same events produce same result (determinism)`() {
|
fun `same events produce same result (determinism)`() {
|
||||||
val initialState = projector.initial()
|
val initialState = projector.initial()
|
||||||
val events = listOf(
|
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 = StageCompletedEvent(sessionId, StageId("stage-2"), StageId("t1"))),
|
||||||
stored(payload = SteeringNoteAddedEvent(sessionId, "note")),
|
stored(payload = SteeringNoteAddedEvent(sessionId, "note")),
|
||||||
stored(payload = StageFailedEvent(sessionId, stageId, StageId("t2"), "timeout")),
|
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 ->
|
val result1 = events.fold(initialState) { state, event ->
|
||||||
@@ -149,11 +150,12 @@ class RouterProjectorTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `full lifecycle stage complete then stage fail then resume then complete`() {
|
fun `full lifecycle stage complete then stage fail then resume then complete`() {
|
||||||
val s0 = projector.initial()
|
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 = StageCompletedEvent(sessionId, StageId("stage-2"), StageId("t1"))))
|
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 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 s4 = projector.apply(s3, stored(payload = WorkflowStartedEvent(sessionId, workflowId, StageId("stage-3"))))
|
||||||
val s5 = projector.apply(s4, stored(payload = WorkflowCompletedEvent(sessionId, stageId, 1)))
|
val s5 = projector.apply(s4, stored(payload = WorkflowCompletedEvent(sessionId, stageId, 1, workflowId)))
|
||||||
|
|
||||||
assertEquals(WorkflowStatus.RUNNING, s1.workflowStatus)
|
assertEquals(WorkflowStatus.RUNNING, s1.workflowStatus)
|
||||||
assertEquals(StageId("stage-2"), s2.l2Memory[0].stageId)
|
assertEquals(StageId("stage-2"), s2.l2Memory[0].stageId)
|
||||||
@@ -169,7 +171,8 @@ class RouterProjectorTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `unknown event type returns unchanged state`() {
|
fun `unknown event type returns unchanged state`() {
|
||||||
val initialState = projector.initial()
|
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)
|
assertEquals(initialState, updated)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ class DefaultTransitionResolverTest {
|
|||||||
private val evaluator = object : TransitionConditionEvaluator {
|
private val evaluator = object : TransitionConditionEvaluator {
|
||||||
override fun evaluate(
|
override fun evaluate(
|
||||||
condition: TransitionCondition,
|
condition: TransitionCondition,
|
||||||
context: EvaluationContext
|
context: EvaluationContext,
|
||||||
): Boolean {
|
): Boolean {
|
||||||
return condition.evaluate(context)
|
return condition.evaluate(context)
|
||||||
}
|
}
|
||||||
@@ -36,20 +36,20 @@ class DefaultTransitionResolverTest {
|
|||||||
id = "t1",
|
id = "t1",
|
||||||
from = "stage-a",
|
from = "stage-a",
|
||||||
to = "stage-b",
|
to = "stage-b",
|
||||||
condition = alwaysTrue()
|
condition = alwaysTrue(),
|
||||||
)
|
|
||||||
),
|
),
|
||||||
stages = setOf("stage-a", "stage-b")
|
),
|
||||||
|
stages = setOf("stage-a", "stage-b"),
|
||||||
)
|
)
|
||||||
|
|
||||||
val result = resolver.resolve(
|
val result = resolver.resolve(
|
||||||
graph = graph,
|
graph = graph,
|
||||||
context = context(currentStage = "stage-b")
|
context = context(currentStage = "stage-b"),
|
||||||
)
|
)
|
||||||
|
|
||||||
assertEquals(
|
assertEquals(
|
||||||
TransitionDecision.NoMatch,
|
TransitionDecision.NoMatch,
|
||||||
result
|
result,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,19 +62,19 @@ class DefaultTransitionResolverTest {
|
|||||||
id = "t1",
|
id = "t1",
|
||||||
from = "stage-a",
|
from = "stage-a",
|
||||||
to = "stage-b",
|
to = "stage-b",
|
||||||
condition = alwaysFalse()
|
condition = alwaysFalse(),
|
||||||
)
|
),
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
val result = resolver.resolve(
|
val result = resolver.resolve(
|
||||||
graph = graph,
|
graph = graph,
|
||||||
context = context(currentStage = "stage-a")
|
context = context(currentStage = "stage-a"),
|
||||||
)
|
)
|
||||||
|
|
||||||
assertEquals(
|
assertEquals(
|
||||||
TransitionDecision.Stay,
|
TransitionDecision.Stay,
|
||||||
result
|
result,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,26 +87,26 @@ class DefaultTransitionResolverTest {
|
|||||||
id = "t1",
|
id = "t1",
|
||||||
from = "stage-a",
|
from = "stage-a",
|
||||||
to = "stage-b",
|
to = "stage-b",
|
||||||
condition = alwaysTrue()
|
condition = alwaysTrue(),
|
||||||
)
|
),
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
val result = resolver.resolve(
|
val result = resolver.resolve(
|
||||||
graph = graph,
|
graph = graph,
|
||||||
context = context(currentStage = "stage-a")
|
context = context(currentStage = "stage-a"),
|
||||||
)
|
)
|
||||||
|
|
||||||
val move = assertInstanceOf<TransitionDecision.Move>(result)
|
val move = assertInstanceOf<TransitionDecision.Move>(result)
|
||||||
|
|
||||||
assertEquals(
|
assertEquals(
|
||||||
TransitionId("t1"),
|
TransitionId("t1"),
|
||||||
move.transitionId
|
move.transitionId,
|
||||||
)
|
)
|
||||||
|
|
||||||
assertEquals(
|
assertEquals(
|
||||||
StageId("stage-b"),
|
StageId("stage-b"),
|
||||||
move.to
|
move.to,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,32 +119,32 @@ class DefaultTransitionResolverTest {
|
|||||||
id = "t2",
|
id = "t2",
|
||||||
from = "stage-a",
|
from = "stage-a",
|
||||||
to = "stage-c",
|
to = "stage-c",
|
||||||
condition = alwaysTrue()
|
condition = alwaysTrue(),
|
||||||
),
|
),
|
||||||
edge(
|
edge(
|
||||||
id = "t1",
|
id = "t1",
|
||||||
from = "stage-a",
|
from = "stage-a",
|
||||||
to = "stage-b",
|
to = "stage-b",
|
||||||
condition = alwaysTrue()
|
condition = alwaysTrue(),
|
||||||
)
|
),
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
val result = resolver.resolve(
|
val result = resolver.resolve(
|
||||||
graph = graph,
|
graph = graph,
|
||||||
context = context(currentStage = "stage-a")
|
context = context(currentStage = "stage-a"),
|
||||||
)
|
)
|
||||||
|
|
||||||
val move = assertInstanceOf<TransitionDecision.Move>(result)
|
val move = assertInstanceOf<TransitionDecision.Move>(result)
|
||||||
|
|
||||||
assertEquals(
|
assertEquals(
|
||||||
TransitionId("t1"),
|
TransitionId("t1"),
|
||||||
move.transitionId
|
move.transitionId,
|
||||||
)
|
)
|
||||||
|
|
||||||
assertEquals(
|
assertEquals(
|
||||||
StageId("stage-b"),
|
StageId("stage-b"),
|
||||||
move.to
|
move.to,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,27 +157,27 @@ class DefaultTransitionResolverTest {
|
|||||||
id = "t1",
|
id = "t1",
|
||||||
from = "stage-x",
|
from = "stage-x",
|
||||||
to = "stage-y",
|
to = "stage-y",
|
||||||
condition = alwaysTrue()
|
condition = alwaysTrue(),
|
||||||
),
|
),
|
||||||
edge(
|
edge(
|
||||||
id = "t2",
|
id = "t2",
|
||||||
from = "stage-a",
|
from = "stage-a",
|
||||||
to = "stage-b",
|
to = "stage-b",
|
||||||
condition = alwaysTrue()
|
condition = alwaysTrue(),
|
||||||
)
|
),
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
val result = resolver.resolve(
|
val result = resolver.resolve(
|
||||||
graph = graph,
|
graph = graph,
|
||||||
context = context(currentStage = "stage-a")
|
context = context(currentStage = "stage-a"),
|
||||||
)
|
)
|
||||||
|
|
||||||
val move = assertInstanceOf<TransitionDecision.Move>(result)
|
val move = assertInstanceOf<TransitionDecision.Move>(result)
|
||||||
|
|
||||||
assertEquals(
|
assertEquals(
|
||||||
TransitionId("t2"),
|
TransitionId("t2"),
|
||||||
move.transitionId
|
move.transitionId,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,24 +185,25 @@ class DefaultTransitionResolverTest {
|
|||||||
id: String,
|
id: String,
|
||||||
from: String,
|
from: String,
|
||||||
to: String,
|
to: String,
|
||||||
condition: TransitionCondition
|
condition: TransitionCondition,
|
||||||
): TransitionEdge {
|
): TransitionEdge {
|
||||||
return TransitionEdge(
|
return TransitionEdge(
|
||||||
id = TransitionId(id),
|
id = TransitionId(id),
|
||||||
from = StageId(from),
|
from = StageId(from),
|
||||||
to = StageId(to),
|
to = StageId(to),
|
||||||
condition = condition
|
condition = condition,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun graph(
|
private fun graph(
|
||||||
start: String,
|
start: String,
|
||||||
transitions: Set<TransitionEdge>,
|
transitions: Set<TransitionEdge>,
|
||||||
stages: Set<String> = transitions.flatMap { listOf(it.from.value, it.to.value) }.toSet()
|
stages: Set<String> = transitions.flatMap { listOf(it.from.value, it.to.value) }.toSet(),
|
||||||
) = WorkflowGraph(
|
) = WorkflowGraph(
|
||||||
|
id = "workflow-test",
|
||||||
start = StageId(start),
|
start = StageId(start),
|
||||||
stages = stages.associate { StageId(it) to StageConfig() },
|
stages = stages.associate { StageId(it) to StageConfig() },
|
||||||
transitions = transitions
|
transitions = transitions,
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun context(
|
private fun context(
|
||||||
|
|||||||
Reference in New Issue
Block a user