import com.correx.core.approvals.ApprovalOutcome import com.correx.core.approvals.ApprovalProjector import com.correx.core.approvals.ApprovalStatus import com.correx.core.approvals.DefaultApprovalReducer import com.correx.core.approvals.GrantScope import com.correx.core.approvals.Tier import com.correx.core.events.events.ApprovalDecisionResolvedEvent import com.correx.core.events.events.ApprovalGrantCreatedEvent import com.correx.core.events.events.ApprovalGrantExpiredEvent import com.correx.core.events.events.ApprovalRequestedEvent import com.correx.core.events.events.EventMetadata import com.correx.core.events.events.EventPayload import com.correx.core.events.events.StoredEvent import com.correx.core.events.types.ApprovalDecisionId import com.correx.core.events.types.ApprovalRequestId import com.correx.core.events.types.EventId import com.correx.core.events.types.GrantId import com.correx.core.events.types.SessionId import com.correx.core.events.types.ValidationReportId import kotlinx.datetime.Instant import org.junit.jupiter.api.Test import org.junit.jupiter.api.fail class ApprovalFuzzTest { private companion object { const val FUZZ_ITERATIONS = 500 const val MAX_STEPS = 100 val SEED: Long = System.getProperty("fuzz.seed")?.toLong() ?: System.currentTimeMillis() val BASE_TIMESTAMP: Instant = Instant.parse("2026-01-01T00:00:00Z") val SESSION_ID: SessionId = SessionId("fuzz-session") } private val reducer = DefaultApprovalReducer() private val projector = ApprovalProjector(reducer) @Test fun `reducer is total and policy-denial is terminal`() { val rng = kotlin.random.Random(SEED) repeat(FUZZ_ITERATIONS) { iteration -> val iterSeed = rng.nextLong() val sequence1 = randomApprovalSequence(kotlin.random.Random(iterSeed)) val sequence2 = randomApprovalSequence(kotlin.random.Random(iterSeed)) val state1 = runCatching { applySequence(sequence1) }.getOrElse { ex -> fail( "Totality violated: reducer threw on a random ordering. " + "error=${ex.message} seed=$SEED iterSeed=$iterSeed iteration=$iteration" ) } val state2 = runCatching { applySequence(sequence2) }.getOrElse { ex -> fail( "Totality violated: reducer threw on the replayed ordering. " + "error=${ex.message} seed=$SEED iterSeed=$iterSeed iteration=$iteration" ) } if (state1 != state2) { fail( "Determinism violated: same seed produced different final states. " + "seed=$SEED iterSeed=$iterSeed iteration=$iteration" ) } assertPolicyDenialTerminal(state1, sequence1, SEED, iterSeed, iteration) } } private fun applySequence(events: List): com.correx.core.approvals.model.ApprovalState { var state = projector.initial() events.forEach { state = projector.apply(state, it) } return state } // Reducer-level terminality: for each decisionId, the *last* event wins (map put-semantics). // If the last event for a decisionId carries REJECTED, the final state must show REJECTED — // not APPROVED. This is the reducer guarantee; cross-requestId sequencing is an engine concern // (invariant #4) enforced above the reducer layer. private fun assertPolicyDenialTerminal( state: com.correx.core.approvals.model.ApprovalState, events: List, seed: Long, iterSeed: Long, iteration: Int, ) { val lastOutcomeByDecisionId = events .map { it.payload } .filterIsInstance() .groupBy { it.decisionId } .mapValues { (_, decisions) -> decisions.last().outcome } lastOutcomeByDecisionId.forEach { (decisionId, lastOutcome) -> if (lastOutcome == ApprovalOutcome.REJECTED) { val finalDecision = state.decisions[decisionId] if (finalDecision != null && finalDecision.isApproved) { fail( "Denial terminality violated: decision $decisionId last seen REJECTED " + "but final state is approved. " + "seed=$seed iterSeed=$iterSeed iteration=$iteration" ) } } } } private fun randomApprovalSequence(rng: kotlin.random.Random): List { val requestCount = rng.nextInt(1, 5) val requestIds = (0 until requestCount).map { ApprovalRequestId("req-$it-${rng.nextInt(1000)}") } val payloads = mutableListOf() requestIds.forEach { requestId -> payloads.add(randomRequestedEvent(requestId, rng)) } val extraPayloadCount = rng.nextInt(0, requestCount * 2 + 1) repeat(extraPayloadCount) { payloads.add(randomExtraPayload(requestIds, rng)) } payloads.shuffle(java.util.Random(rng.nextLong())) return payloads.take(MAX_STEPS).mapIndexed { idx, payload -> storedEvent(payload, idx + 1L) } } private fun randomRequestedEvent(requestId: ApprovalRequestId, rng: kotlin.random.Random): ApprovalRequestedEvent { val tiers = Tier.entries.toList() return ApprovalRequestedEvent( requestId = requestId, tier = tiers[rng.nextInt(tiers.size)], validationReportId = ValidationReportId("vr-${rng.nextInt(1000)}"), riskSummaryId = null, sessionId = SESSION_ID, stageId = null, projectId = null, ) } private fun randomExtraPayload(requestIds: List, rng: kotlin.random.Random): EventPayload { val requestId = requestIds[rng.nextInt(requestIds.size)] return when (rng.nextInt(4)) { 0 -> rejectedDecisionEvent(requestId, rng) 1 -> approvedDecisionEvent(requestId, rng) 2 -> createdGrantEvent(rng) else -> expiredGrantEvent(rng) } } private fun rejectedDecisionEvent(requestId: ApprovalRequestId, rng: kotlin.random.Random): ApprovalDecisionResolvedEvent = ApprovalDecisionResolvedEvent( decisionId = ApprovalDecisionId("dec-rejected-${rng.nextInt(10000)}"), requestId = requestId, outcome = ApprovalOutcome.REJECTED, status = ApprovalStatus.COMPLETED, tier = Tier.T2, resolutionTimestamp = BASE_TIMESTAMP, reason = "policy denial", ) private fun approvedDecisionEvent(requestId: ApprovalRequestId, rng: kotlin.random.Random): ApprovalDecisionResolvedEvent = ApprovalDecisionResolvedEvent( decisionId = ApprovalDecisionId("dec-approved-${rng.nextInt(10000)}"), requestId = requestId, outcome = ApprovalOutcome.APPROVED, status = ApprovalStatus.COMPLETED, tier = Tier.T2, resolutionTimestamp = BASE_TIMESTAMP, reason = null, ) private fun createdGrantEvent(rng: kotlin.random.Random): ApprovalGrantCreatedEvent = ApprovalGrantCreatedEvent( grantId = GrantId("grant-${rng.nextInt(10000)}"), scope = GrantScope.SESSION(toolName = "shell"), permittedTiers = setOf(Tier.entries[rng.nextInt(Tier.entries.size)]), reason = "fuzz grant", expiresAt = null, sessionId = SESSION_ID, stageId = null, projectId = null, toolName = "shell", ) private fun expiredGrantEvent(rng: kotlin.random.Random): ApprovalGrantExpiredEvent = ApprovalGrantExpiredEvent(grantId = GrantId("grant-${rng.nextInt(10000)}")) private fun storedEvent(payload: EventPayload, sequence: Long): StoredEvent = StoredEvent( metadata = EventMetadata( eventId = EventId("event-$sequence"), sessionId = SESSION_ID, timestamp = BASE_TIMESTAMP, schemaVersion = 1, causationId = null, correlationId = null, ), sequence = sequence, sessionSequence = sequence, payload = payload, ) }