125 lines
4.8 KiB
Kotlin
125 lines
4.8 KiB
Kotlin
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<TransitionId> {
|
|
val path = mutableListOf<TransitionId>()
|
|
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<TransitionEdge>()
|
|
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,
|
|
)
|
|
}
|
|
}
|