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.transitions.evaluation.EvaluationContext import com.correx.core.transitions.evaluation.TransitionConditionEvaluator import com.correx.core.transitions.graph.StageConfig import com.correx.core.transitions.graph.TransitionCondition import com.correx.core.transitions.graph.TransitionEdge import com.correx.core.transitions.graph.WorkflowGraph import com.correx.core.transitions.resolution.DefaultTransitionResolver import com.correx.core.transitions.resolution.TransitionDecision import com.correx.core.transitions.resolution.TransitionResolver import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertInstanceOf class DefaultTransitionResolverTest { private val evaluator = TransitionConditionEvaluator { condition, context -> condition.evaluate(context) } private val resolver: TransitionResolver = DefaultTransitionResolver(evaluator) @Test fun `returns NoMatch when no outgoing transitions exist for current stage`() { val graph = graph( start = "stage-a", transitions = setOf( edge( id = "t1", from = "stage-a", to = "stage-b", condition = alwaysTrue(), ), ), stages = setOf("stage-a", "stage-b"), ) val result = resolver.resolve( graph = graph, context = context(currentStage = "stage-b"), ) assertEquals( TransitionDecision.NoMatch, result, ) } @Test fun `returns Stay when all conditions evaluate to false`() { val graph = graph( start = "stage-a", transitions = setOf( edge( id = "t1", from = "stage-a", to = "stage-b", condition = alwaysFalse(), ), ), ) val result = resolver.resolve( graph = graph, context = context(currentStage = "stage-a"), ) assertEquals( TransitionDecision.Stay, result, ) } @Test fun `returns matching transition when condition evaluates to true`() { val graph = graph( start = "stage-a", transitions = setOf( edge( id = "t1", from = "stage-a", to = "stage-b", condition = alwaysTrue(), ), ), ) val result = resolver.resolve( graph = graph, context = context(currentStage = "stage-a"), ) val move = assertInstanceOf(result) assertEquals( TransitionId("t1"), move.transitionId, ) assertEquals( StageId("stage-b"), move.to, ) } @Test fun `first matching transition wins deterministically`() { val graph = graph( start = "stage-a", transitions = setOf( edge( id = "t2", from = "stage-a", to = "stage-c", condition = alwaysTrue(), ), edge( id = "t1", from = "stage-a", to = "stage-b", condition = alwaysTrue(), ), ), ) val result = resolver.resolve( graph = graph, context = context(currentStage = "stage-a"), ) val move = assertInstanceOf(result) assertEquals( TransitionId("t1"), move.transitionId, ) assertEquals( StageId("stage-b"), move.to, ) } @Test fun `only transitions from current stage are evaluated`() { val graph = graph( start = "stage-a", transitions = setOf( edge( id = "t1", from = "stage-x", to = "stage-y", condition = alwaysTrue(), ), edge( id = "t2", from = "stage-a", to = "stage-b", condition = alwaysTrue(), ), ), ) val result = resolver.resolve( graph = graph, context = context(currentStage = "stage-a"), ) val move = assertInstanceOf(result) assertEquals( TransitionId("t2"), move.transitionId, ) } private fun edge( id: String, from: String, to: String, condition: TransitionCondition, ): TransitionEdge { return TransitionEdge( id = TransitionId(id), from = StageId(from), to = StageId(to), condition = condition, ) } private fun graph( start: String, transitions: Set, stages: Set = transitions.flatMap { listOf(it.from.value, it.to.value) }.toSet(), ) = WorkflowGraph( id = "workflow-test", start = StageId(start), stages = stages.associate { StageId(it) to StageConfig() }, transitions = transitions, ) private fun context( currentSession: String = "s1", currentStage: String, ): EvaluationContext { return EvaluationContext( sessionId = SessionId(currentSession), currentStage = StageId(currentStage), variables = emptyMap(), ) } private fun alwaysTrue() = TransitionCondition { true } @Suppress("UnusedPrivateMember") private fun alwaysFalse() = TransitionCondition { false } }