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 org.junit.jupiter.api.Test import org.junit.jupiter.api.fail class TransitionFuzzTest { private companion object { const val FUZZ_ITERATIONS = 500 const val MAX_STEPS = 100 val SEED: Long = System.getProperty("fuzz.seed")?.toLong() ?: System.currentTimeMillis() } private val evaluator = TransitionConditionEvaluator { condition, context -> condition.evaluate(context) } private val resolver = DefaultTransitionResolver(evaluator) @Test fun `graph-walk stays within declared edges and terminates`() { val rng = kotlin.random.Random(SEED) repeat(FUZZ_ITERATIONS) { iteration -> val iterSeed = rng.nextLong() val graph = randomGraph(kotlin.random.Random(iterSeed)) val path1 = walk(graph, kotlin.random.Random(iterSeed)) val path2 = walk(graph, kotlin.random.Random(iterSeed)) val declaredEdgeIds = graph.transitions.map { it.id }.toSet() path1.forEach { transitionId -> if (transitionId !in declaredEdgeIds) { fail( "Invariant 1 violated: transition $transitionId not in declared graph. " + "seed=$SEED iterSeed=$iterSeed iteration=$iteration" ) } } if (path1.size >= MAX_STEPS) { fail( "Invariant 2 violated: walk did not terminate within $MAX_STEPS steps. " + "seed=$SEED iterSeed=$iterSeed iteration=$iteration" ) } if (path1 != path2) { fail( "Invariant 3 violated: same seed produced different paths. " + "path1=$path1 path2=$path2 seed=$SEED iterSeed=$iterSeed iteration=$iteration" ) } } } private fun walk(graph: WorkflowGraph, rng: kotlin.random.Random): List { val path = mutableListOf() var currentStage = graph.start repeat(MAX_STEPS) { val context = randomContext(currentStage, rng) val graphWithConditions = randomizeConditions(graph, rng) val decision = resolver.resolve(graphWithConditions, context) if (decision is TransitionDecision.Move) { path.add(decision.transitionId) currentStage = decision.to } else { return path } } return path } private fun randomGraph(rng: kotlin.random.Random): WorkflowGraph { val stageCount = rng.nextInt(3, 9) val stageIds = (0 until stageCount).map { StageId("stage-$it") } val stages = stageIds.associateWith { StageConfig() } val start = stageIds.first() val edgeCount = rng.nextInt(stageCount, stageCount * 2 + 1) val edges = mutableSetOf() repeat(edgeCount) { idx -> val from = stageIds[rng.nextInt(stageIds.size)] val to = stageIds[rng.nextInt(stageIds.size)] edges.add( TransitionEdge( id = TransitionId("t-$idx"), from = from, to = to, condition = TransitionCondition { true }, ) ) } return WorkflowGraph( id = "fuzz-graph", start = start, stages = stages, transitions = edges, ) } private fun randomizeConditions(graph: WorkflowGraph, rng: kotlin.random.Random): WorkflowGraph { val newEdges = graph.transitions.map { edge -> val outcome = rng.nextBoolean() edge.copy(condition = TransitionCondition { outcome }) }.toSet() return graph.copy(transitions = newEdges) } private fun randomContext(currentStage: StageId, rng: kotlin.random.Random): EvaluationContext { val varCount = rng.nextInt(0, 4) val variables = (0 until varCount).associate { "var$it" to rng.nextInt(100).toString() } return EvaluationContext( sessionId = SessionId("fuzz-session"), currentStage = currentStage, variables = variables, ) } }