1b9896d2fa
Extract a SubagentRunner fun-interface (SubagentRunRequest/SubagentRunResult) and an InSessionSubagentRunner default that delegates to the existing executeStage path, with per-run effectives captured in the lambda closure so the seam stays free of the internal RunEffectives type. DefaultSessionOrchestrator.enterStage now dispatches through subagentRunner.run instead of calling executeStage inline; effectives threading is dropped from run/step/executeMove/enterStage/resume. ReplayOrchestrator supplies its own runner. Behaviour is identical — RefinementLoopTest and full check stay green.
141 lines
6.8 KiB
Kotlin
141 lines
6.8 KiB
Kotlin
import com.correx.core.approvals.ApprovalProjector
|
|
import com.correx.core.approvals.DefaultApprovalReducer
|
|
import com.correx.core.approvals.DefaultApprovalRepository
|
|
import com.correx.core.approvals.domain.DefaultApprovalEngine
|
|
import com.correx.core.artifacts.DefaultArtifactReducer
|
|
import com.correx.testing.fixtures.context.ContextFixtures
|
|
import com.correx.core.events.events.TransitionExecutedEvent
|
|
import com.correx.core.events.events.WorkflowCompletedEvent
|
|
import com.correx.core.events.execution.RetryPolicy
|
|
import com.correx.core.events.types.SessionId
|
|
import com.correx.core.events.types.StageId
|
|
import com.correx.core.events.types.TransitionId
|
|
import com.correx.core.inference.InferenceRepository
|
|
import com.correx.core.inference.InferenceState
|
|
import com.correx.core.journal.DecisionJournalProjector
|
|
import com.correx.core.journal.DefaultDecisionJournalReducer
|
|
import com.correx.core.journal.DefaultDecisionJournalRepository
|
|
import com.correx.core.kernel.orchestration.DefaultOrchestrationReducer
|
|
import com.correx.core.kernel.orchestration.DefaultSessionOrchestrator
|
|
import com.correx.core.kernel.orchestration.OrchestrationConfig
|
|
import com.correx.core.kernel.orchestration.OrchestrationProjector
|
|
import com.correx.core.kernel.orchestration.OrchestrationRepository
|
|
import com.correx.core.kernel.orchestration.OrchestratorEngines
|
|
import com.correx.core.kernel.orchestration.OrchestratorRepositories
|
|
import com.correx.core.kernel.retry.DefaultRetryCoordinator
|
|
import com.correx.core.risk.DefaultRiskAssessor
|
|
import com.correx.core.sessions.DefaultSessionRepository
|
|
import com.correx.core.sessions.projections.replay.DefaultEventReplayer
|
|
import com.correx.core.sessions.projections.replay.EventReplayer
|
|
import com.correx.core.transitions.graph.StageConfig
|
|
import com.correx.core.transitions.graph.TransitionEdge
|
|
import com.correx.core.transitions.graph.WorkflowGraph
|
|
import com.correx.core.validation.pipeline.ValidationPipeline
|
|
import com.correx.infrastructure.persistence.InMemoryEventStore
|
|
import com.correx.infrastructure.persistence.artifact.LiveArtifactRepository
|
|
import com.correx.testing.contracts.fixtures.artifactstore.NoopArtifactStore
|
|
import com.correx.testing.fixtures.InferenceFixtures
|
|
import com.correx.testing.fixtures.cyclePolicyMissingValidator
|
|
import com.correx.testing.fixtures.transitions.TransitionFixtures
|
|
import com.correx.testing.kernel.MockSessionEventReplayer
|
|
import kotlinx.coroutines.runBlocking
|
|
import org.junit.jupiter.api.Assertions.assertEquals
|
|
import org.junit.jupiter.api.Assertions.assertNotNull
|
|
import org.junit.jupiter.api.Test
|
|
|
|
/**
|
|
* Regression guard for the [com.correx.core.kernel.orchestration.subagent.SubagentRunner] seam:
|
|
* stage execution is now dispatched through the runner rather than calling executeStage inline.
|
|
* The default in-session runner must produce behaviour identical to the prior path — a workflow
|
|
* still runs every non-terminal stage to completion. (executeStage is module-internal, so the
|
|
* runner cannot be wrapped from this test module; we assert per-stage execution end-to-end.)
|
|
*/
|
|
class SubagentRunnerSeamTest {
|
|
|
|
private val eventStore = InMemoryEventStore()
|
|
private val sessionReplayer = MockSessionEventReplayer()
|
|
private val sessionRepository = DefaultSessionRepository(sessionReplayer)
|
|
private val orchestrationRepository = OrchestrationRepository(
|
|
DefaultEventReplayer(eventStore, OrchestrationProjector(DefaultOrchestrationReducer())),
|
|
)
|
|
private val inferenceRepository = InferenceRepository(
|
|
object : EventReplayer<InferenceState> {
|
|
override fun rebuild(sessionId: SessionId) = InferenceState()
|
|
},
|
|
)
|
|
private val approvalRepository = DefaultApprovalRepository(
|
|
DefaultEventReplayer(eventStore, ApprovalProjector(DefaultApprovalReducer())),
|
|
)
|
|
private val decisionJournalRepository = DefaultDecisionJournalRepository(
|
|
DefaultEventReplayer(eventStore, DecisionJournalProjector(DefaultDecisionJournalReducer())),
|
|
)
|
|
private val artifactStore = NoopArtifactStore()
|
|
private val retryCoordinator = DefaultRetryCoordinator(eventStore)
|
|
|
|
private val repositories = OrchestratorRepositories(
|
|
eventStore = eventStore,
|
|
inferenceRepository = inferenceRepository,
|
|
orchestrationRepository = orchestrationRepository,
|
|
sessionRepository = sessionRepository,
|
|
artifactRepository = LiveArtifactRepository(eventStore, DefaultArtifactReducer()),
|
|
approvalRepository = approvalRepository,
|
|
)
|
|
|
|
private val orchestrator = DefaultSessionOrchestrator(
|
|
repositories = repositories,
|
|
engines = OrchestratorEngines(
|
|
transitionResolver = TransitionFixtures.simpleResolver(),
|
|
contextPackBuilder = ContextFixtures.simpleBuilder(),
|
|
inferenceRouter = InferenceFixtures.fixedRouter(),
|
|
validationPipeline = ValidationPipeline(validators = listOf(cyclePolicyMissingValidator())),
|
|
approvalEngine = DefaultApprovalEngine(),
|
|
riskAssessor = DefaultRiskAssessor(),
|
|
),
|
|
retryCoordinator = retryCoordinator,
|
|
artifactStore = artifactStore,
|
|
decisionJournalRepository = decisionJournalRepository,
|
|
)
|
|
|
|
private val defaultConfig = OrchestrationConfig(
|
|
retryPolicy = RetryPolicy(maxAttempts = 1, backoffMs = 0),
|
|
)
|
|
|
|
@Test
|
|
fun `workflow runs every non-terminal stage through the runner and completes`(): Unit = runBlocking {
|
|
val sessionId = SessionId("seam-test-1")
|
|
val stageA = StageId("A")
|
|
val stageB = StageId("B")
|
|
val done = StageId("done")
|
|
|
|
val graph = WorkflowGraph(
|
|
id = "seam-test",
|
|
stages = mapOf(
|
|
stageA to StageConfig(allowedTools = emptySet()),
|
|
stageB to StageConfig(allowedTools = emptySet()),
|
|
done to StageConfig(),
|
|
),
|
|
transitions = setOf(
|
|
TransitionEdge(id = TransitionId("t1"), from = stageA, to = stageB, condition = { true }),
|
|
TransitionEdge(id = TransitionId("t2"), from = stageB, to = done, condition = { true }),
|
|
),
|
|
start = stageA,
|
|
)
|
|
|
|
orchestrator.run(sessionId, graph, defaultConfig)
|
|
|
|
val events = eventStore.read(sessionId)
|
|
assertNotNull(
|
|
events.find { it.payload is WorkflowCompletedEvent },
|
|
"workflow should complete through the subagent runner seam",
|
|
)
|
|
// Stage A is the start (entered, no transition-to event); B is entered via the runner
|
|
// after the A->B transition; `done` is a terminal sentinel (no TransitionExecuted).
|
|
val transitions = events.mapNotNull { it.payload as? TransitionExecutedEvent }
|
|
assertEquals(
|
|
listOf(stageB),
|
|
transitions.map { it.to },
|
|
"stage B should be entered via the runner after stage A completes",
|
|
)
|
|
}
|
|
}
|