epic-12: after epic audit and init commit

This commit is contained in:
2026-05-16 11:42:00 +04:00
commit c77277af0b
461 changed files with 28958 additions and 0 deletions
+17
View File
@@ -0,0 +1,17 @@
plugins {
id 'java-library'
id 'org.jetbrains.kotlin.jvm'
id 'org.jetbrains.kotlin.plugin.serialization'
}
dependencies {
testImplementation(project(":core:events"))
testImplementation(project(":core:sessions"))
testImplementation(project(":core:transitions"))
testImplementation(project(":core:validation"))
testImplementation(project(":core:approvals"))
testImplementation(project(":core:context"))
testImplementation(project(":infrastructure:persistence"))
testImplementation(project(":testing:fixtures"))
testImplementation(project(":core:inference"))
}
@@ -0,0 +1,3 @@
package com.correx.testing.deterministic
object Module
@@ -0,0 +1,45 @@
import com.correx.core.approvals.Tier
import com.correx.core.approvals.domain.DefaultApprovalEngine
import com.correx.core.approvals.model.ApprovalContext
import com.correx.core.approvals.model.ApprovalGrant
import com.correx.core.approvals.model.ApprovalScopeIdentity
import com.correx.core.events.types.SessionId
import com.correx.core.sessions.ApprovalMode
import com.correx.testing.fixtures.request
import kotlinx.datetime.Instant
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNotEquals
import org.junit.jupiter.api.Test
class ApprovalEngineDeterminismTest {
private val engine = DefaultApprovalEngine()
@Test
fun `identical inputs produce identical decisions`() {
val request = request("r1", Tier.T2)
val identity = ApprovalScopeIdentity(SessionId("s1"), null, null)
val context = ApprovalContext(identity, ApprovalMode.AUTO)
val grants = emptyList<ApprovalGrant>()
val now = Instant.parse("2026-01-01T00:00:01Z")
val d1 = engine.evaluate(request, context, grants, now)
val d2 = engine.evaluate(request, context, grants, now)
assertEquals(d1, d2)
}
@Test
fun `different requests produce different decision IDs`() {
val request1 = request("r1", Tier.T2)
val request2 = request("r2", Tier.T2)
val identity = ApprovalScopeIdentity(SessionId("s1"), null, null)
val context = ApprovalContext(identity, ApprovalMode.AUTO)
val now = Instant.parse("2026-01-01T00:00:01Z")
val d1 = engine.evaluate(request1, context, emptyList(), now)
val d2 = engine.evaluate(request2, context, emptyList(), now)
assertNotEquals(d1.id, d2.id)
}
}
@@ -0,0 +1,207 @@
import com.correx.core.context.compression.CompressionStrategy
import com.correx.core.context.compression.DefaultContextCompressor
import com.correx.core.context.model.ContextEntry
import com.correx.core.context.model.ContextLayer
import com.correx.core.context.model.TokenBudget
import com.correx.core.events.types.ContextEntryId
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.MethodSource
class ContextCompressionDeterminismTest {
private val compressor = DefaultContextCompressor()
private fun entry(
id: String,
layer: ContextLayer,
tokens: Int,
content: String = "content-$id",
sourceId: String = id
) = ContextEntry(
id = ContextEntryId(id),
layer = layer,
content = content,
sourceType = "test",
sourceId = sourceId,
tokenEstimate = tokens
)
@Test
fun `same input always produces same output`() {
val entries = listOf(
entry("a", ContextLayer.L2, 100),
entry("b", ContextLayer.L1, 50),
entry("c", ContextLayer.L0, 30)
)
val budget = TokenBudget(limit = 200)
val result1 = compressor.compress(entries, budget, CompressionStrategy.Conversation())
val result2 = compressor.compress(entries, budget, CompressionStrategy.Conversation())
assertEquals(result1, result2)
}
@Test
fun `L2 entries are dropped before L1 when over budget`() {
val entries = listOf(
entry("l2-1", ContextLayer.L2, 80),
entry("l1-1", ContextLayer.L1, 80),
entry("l0-1", ContextLayer.L0, 80)
)
val budget = TokenBudget(limit = 170)
val result = compressor.compress(entries, budget, CompressionStrategy.Conversation())
assertTrue(result.none { it.layer == ContextLayer.L2 }, "L2 must be dropped first")
assertTrue(result.any { it.layer == ContextLayer.L1 }, "L1 must be retained")
assertTrue(result.any { it.layer == ContextLayer.L0 }, "L0 must always be retained")
}
@Test
fun `SteeringNote entries are never dropped regardless of budget`() {
val entries = listOf(
entry("s1", ContextLayer.L1, 500),
entry("s2", ContextLayer.L2, 500)
)
val budget = TokenBudget(limit = 10)
val result = compressor.compress(entries, budget, CompressionStrategy.SteeringNote)
assertEquals(2, result.size)
}
@Test
fun `EventHistory entries are never dropped`() {
val entries = listOf(
entry("e1", ContextLayer.L2, 500),
entry("e2", ContextLayer.L2, 500)
)
val budget = TokenBudget(limit = 10)
val result = compressor.compress(entries, budget, CompressionStrategy.EventHistory)
assertEquals(2, result.size)
}
@Test
fun `Artifact strategy keeps latest entry per sourceId`() {
val entries = listOf(
entry("v1", ContextLayer.L1, 50, sourceId = "artifact-a"),
entry("v2", ContextLayer.L1, 50, sourceId = "artifact-a"),
entry("v3", ContextLayer.L1, 50, sourceId = "artifact-b")
)
val budget = TokenBudget(limit = 4000)
val result = compressor.compress(entries, budget, CompressionStrategy.Artifact)
assertEquals(2, result.size)
assertTrue(result.any { it.id == ContextEntryId("v2") }, "Latest artifact-a version must be kept")
assertTrue(result.any { it.id == ContextEntryId("v3") }, "artifact-b must be kept")
}
@Test
fun `ToolLog strategy deduplicates identical content`() {
val entries = listOf(
entry("t1", ContextLayer.L1, 30, content = "same output"),
entry("t2", ContextLayer.L1, 30, content = "same output"),
entry("t3", ContextLayer.L1, 30, content = "different output")
)
val budget = TokenBudget(limit = 4000)
val result = compressor.compress(entries, budget, CompressionStrategy.ToolLog)
assertEquals(2, result.size)
}
@Test
fun `entries fitting within budget are not dropped`() {
val entries = listOf(
entry("a", ContextLayer.L1, 30),
entry("b", ContextLayer.L2, 30)
)
val budget = TokenBudget(limit = 200)
val result = compressor.compress(entries, budget, CompressionStrategy.Conversation())
assertEquals(2, result.size)
}
@Test
fun `Conversation keepLast drops oldest entries beyond the limit`() {
val entries = (1..5).map { entry("e$it", ContextLayer.L1, 10) }
val budget = TokenBudget(limit = 4000)
val result = compressor.compress(entries, budget, CompressionStrategy.Conversation(keepLast = 3))
assertEquals(3, result.size)
assertEquals(listOf("e3", "e4", "e5"), result.map { it.id.value })
}
@Test
fun `Conversation L0 is never truncated when L1 overflows budget`() {
val entries = listOf(
entry("l0-1", ContextLayer.L0, 60),
entry("l1-1", ContextLayer.L1, 60),
entry("l1-2", ContextLayer.L1, 60)
)
val budget = TokenBudget(limit = 100)
val result = compressor.compress(entries, budget, CompressionStrategy.Conversation())
assertTrue(result.any { it.layer == ContextLayer.L0 }, "L0 must never be dropped")
assertTrue(result.none { it.id == ContextEntryId("l1-1") }, "Oldest L1 must be dropped first")
}
// Parameterized equivalence test: verifies the refactored running-total trimToFit
// produces the same output as the reference naive implementation for varied input shapes.
@ParameterizedTest
@MethodSource("trimToFitEquivalenceCases")
fun `trimToFit running-total matches reference naive implementation`(
entries: List<ContextEntry>,
budget: TokenBudget
) {
val expected = naiveTrimToFit(entries, budget)
val actual = compressor.compress(entries, budget, CompressionStrategy.Conversation())
assertEquals(expected, actual, "running-total trimToFit must match naive O(n²) reference")
}
// Reference O(n²) implementation kept as spec baseline — intentionally not optimized.
private fun naiveTrimToFit(entries: List<ContextEntry>, budget: TokenBudget): List<ContextEntry> {
val dropOrder = listOf(ContextLayer.L2, ContextLayer.L1)
if (entries.sumOf { it.tokenEstimate } <= budget.limit) return entries
val mutable = entries.toMutableList()
for (layer in dropOrder) {
if (mutable.sumOf { it.tokenEstimate } <= budget.limit) break
val layerEntries = mutable.filter { it.layer == layer }
for (e in layerEntries) {
if (mutable.sumOf { it.tokenEstimate } <= budget.limit) break
mutable.remove(e)
}
}
return mutable
}
companion object {
@JvmStatic
fun trimToFitEquivalenceCases(): List<Array<Any>> {
fun e(id: String, layer: ContextLayer, tokens: Int) =
ContextEntry(
id = ContextEntryId(id),
layer = layer,
content = "c-$id",
sourceType = "test",
sourceId = id,
tokenEstimate = tokens
)
return listOf(
// All fit — nothing dropped
arrayOf(
listOf(e("a", ContextLayer.L1, 10), e("b", ContextLayer.L2, 10)),
TokenBudget(limit = 100)
),
// L2 fully dropped, L1 retained
arrayOf(
listOf(e("x1", ContextLayer.L2, 80), e("x2", ContextLayer.L1, 80), e("x3", ContextLayer.L0, 40)),
TokenBudget(limit = 130)
),
// Both L2 and some L1 dropped
arrayOf(
(1..5).map { e("l2-$it", ContextLayer.L2, 30) } +
(1..5).map { e("l1-$it", ContextLayer.L1, 30) } +
listOf(e("l0-1", ContextLayer.L0, 20)),
TokenBudget(limit = 80)
),
// Empty list
arrayOf(emptyList<ContextEntry>(), TokenBudget(limit = 50)),
// Single entry exactly at budget
arrayOf(listOf(e("only", ContextLayer.L2, 50)), TokenBudget(limit = 50))
)
}
}
}
@@ -0,0 +1,97 @@
import com.correx.core.context.builder.DefaultContextPackBuilder
import com.correx.core.context.builder.DefaultDecisionPointBuilder
import com.correx.core.context.compression.DefaultContextCompressor
import com.correx.core.context.model.ContextEntry
import com.correx.core.context.model.ContextLayer
import com.correx.core.context.model.TokenBudget
import com.correx.core.events.types.ContextEntryId
import com.correx.core.events.types.ContextPackId
import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
class DefaultContextPackBuilderTest {
private val compressor = DefaultContextCompressor()
private val builder = DefaultContextPackBuilder(compressor)
private val decisionBuilder = DefaultDecisionPointBuilder()
private val sessionId = SessionId("s1")
private val stageId = StageId("stage-1")
private val packId = ContextPackId("pack-1")
private fun entry(id: String, layer: ContextLayer, tokens: Int) = ContextEntry(
id = ContextEntryId(id),
layer = layer,
content = "content-$id",
sourceType = "test",
sourceId = id,
tokenEstimate = tokens
)
@Test
fun `build groups entries by layer`() {
val entries = listOf(
entry("a", ContextLayer.L0, 50),
entry("b", ContextLayer.L1, 60),
entry("c", ContextLayer.L2, 40)
)
val pack = builder.build(packId, sessionId, stageId, entries, TokenBudget(limit = 4000))
assertEquals(1, pack.layers[ContextLayer.L0]?.size)
assertEquals(1, pack.layers[ContextLayer.L1]?.size)
assertEquals(1, pack.layers[ContextLayer.L2]?.size)
}
@Test
fun `budgetUsed reflects total token estimate of retained entries`() {
val entries = listOf(
entry("a", ContextLayer.L0, 100),
entry("b", ContextLayer.L1, 200)
)
val pack = builder.build(packId, sessionId, stageId, entries, TokenBudget(limit = 4000))
assertEquals(300, pack.budgetUsed)
}
@Test
fun `pack budgetUsed never exceeds budgetLimit`() {
val entries = (1..20).map { entry("e$it", ContextLayer.L2, 100) }
val pack = builder.build(packId, sessionId, stageId, entries, TokenBudget(limit = 500))
assertTrue(pack.budgetUsed <= 500)
}
@Test
fun `DecisionPointBuilder returns only L0 and L1 entries`() {
val entries = listOf(
entry("a", ContextLayer.L0, 50),
entry("b", ContextLayer.L1, 60),
entry("c", ContextLayer.L2, 40)
)
val pack = builder.build(packId, sessionId, stageId, entries, TokenBudget(limit = 4000))
val decisionEntries = decisionBuilder.build(pack)
assertTrue(decisionEntries.all { it.layer == ContextLayer.L0 || it.layer == ContextLayer.L1 })
assertEquals(2, decisionEntries.size)
}
@Test
fun `build handles empty entries list`() {
val pack = builder.build(packId, sessionId, stageId, emptyList(), TokenBudget(limit = 1000))
assertEquals(0, pack.budgetUsed)
assertEquals(0, pack.compressionMetadata.entriesDropped)
assertTrue(pack.layers.isEmpty())
}
@Test
fun `steeringNote and eventHistory entries are retained even when exceeding budget`() {
val entries = listOf(
entry("steering", ContextLayer.L0, 500).copy(sourceType = "steeringNote"),
entry("history", ContextLayer.L1, 600).copy(sourceType = "eventHistory"),
entry("normal", ContextLayer.L2, 200)
)
val pack = builder.build(packId, sessionId, stageId, entries, TokenBudget(limit = 800))
val allRetained = pack.layers.values.flatten()
assertTrue(allRetained.any { it.id == ContextEntryId("steering") }, "steeringNote must be retained")
assertTrue(allRetained.any { it.id == ContextEntryId("history") }, "eventHistory must be retained")
}
}
@@ -0,0 +1,120 @@
import com.correx.core.events.types.StageId
import com.correx.core.events.types.TransitionId
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.transitions.resolution.TransitionOrdering
import com.correx.core.validation.graph.GraphValidator
import com.correx.core.validation.model.ValidationContext
import com.correx.core.validation.transition.TransitionValidator
import com.correx.testing.fixtures.CycleFixtures
import com.correx.testing.fixtures.WorkflowFixtures
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
class GraphValidatorTest {
private val validator = GraphValidator()
@Test
fun `should detect dangling transition`() {
val graph = WorkflowFixtures.simpleGraph().copy(
stages = mapOf(StageId("A") to StageConfig()) // B removed → dangling
)
val result = validator.validate(
ValidationContext(
graph = graph,
detectedCycles = emptyList()
)
)
val issues = result.issues
assertTrue(
issues.any { it.code == "GRAPH_DANGLING_TRANSITION" }
)
}
@Test
fun `should expose cycles as informational only`() {
val graph = WorkflowFixtures.simpleGraph()
val cycles = listOf(CycleFixtures.simpleCycle())
val validator = GraphValidator()
val result = validator.validate(
ValidationContext(
graph = graph,
detectedCycles = cycles
)
)
val cycleIssues = result.issues
.filter { it.code == "GRAPH_CYCLE_DETECTED" }
assertTrue(cycleIssues.isNotEmpty())
}
class TransitionValidatorTest {
private val validator = TransitionValidator(
ordering = TransitionOrdering.comparator
)
@Test
fun `should be deterministic`() {
val graph = WorkflowFixtures.simpleGraph()
val result1 = validator.validate(
ValidationContext(graph, emptyList())
)
val result2 = validator.validate(
ValidationContext(graph, emptyList())
)
assertEquals(result1, result2)
}
@Test
fun `should flag non-deterministic order when two edges share same from and to`() {
val a = StageId("A")
val b = StageId("B")
val edge1 = TransitionEdge(
id = TransitionId("t1"),
from = a,
to = b,
condition = { true }
)
val edge2 = TransitionEdge(
id = TransitionId("t2"),
from = a,
to = b,
condition = { true }
)
val graph = WorkflowGraph(
stages = mapOf(a to StageConfig(), b to StageConfig()),
transitions = setOf(edge1, edge2),
start = a
)
val result = validator.validate(
ValidationContext(graph, emptyList())
)
assertTrue(
result.issues.any { it.code == "TRANSITION_NON_DETERMINISTIC_ORDER" },
"Expected TRANSITION_NON_DETERMINISTIC_ORDER but got: ${result.issues.map { it.code }}"
)
}
}
}
@@ -0,0 +1,57 @@
import com.correx.core.events.events.EventMetadata
import com.correx.core.events.events.EventPayload
import com.correx.core.events.events.NewEvent
import com.correx.core.events.events.SessionPausedEvent
import com.correx.core.events.events.SessionResumedEvent
import com.correx.core.events.events.SessionStartedEvent
import com.correx.core.events.stores.EventStore
import com.correx.core.events.types.EventId
import com.correx.core.events.types.SessionId
import com.correx.core.sessions.DefaultSessionReducer
import com.correx.core.sessions.SessionProjector
import com.correx.core.sessions.projections.replay.DefaultEventReplayer
import com.correx.infrastructure.persistence.InMemoryEventStore
import kotlinx.datetime.Clock
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
class SessionReplayDeterminismTest {
private fun build(store: EventStore) = DefaultEventReplayer(
store,
SessionProjector(DefaultSessionReducer())
)
@Test
fun `same events produce same state`() {
val sessionId = SessionId("s1")
val store1 = InMemoryEventStore()
val store2 = InMemoryEventStore()
val events = mapOf<EventMetadata, EventPayload>(
EventMetadata(EventId("start"), sessionId, Clock.System.now(), 1, null, null) to SessionStartedEvent(
sessionId
),
EventMetadata(EventId("paused"), sessionId, Clock.System.now(), 1, null, null) to SessionPausedEvent(
sessionId
),
EventMetadata(EventId("resumed"), sessionId, Clock.System.now(), 1, null, null) to SessionResumedEvent(
sessionId
)
)
events.forEach { (meta, payload) ->
store1.append(NewEvent(meta, payload))
store2.append(NewEvent(meta, payload))
}
val replayer1 = build(store1)
val replayer2 = build(store2)
val state1 = replayer1.rebuild(sessionId)
val state2 = replayer2.rebuild(sessionId)
assertEquals(state1, state2)
}
}
@@ -0,0 +1,42 @@
import com.correx.core.context.model.TokenBudget
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
class TokenBudgetEnforcementTest {
@Test
fun `remaining equals limit minus used`() {
val budget = TokenBudget(limit = 1000, used = 300)
assertEquals(700, budget.remaining)
}
@Test
fun `canFit returns true when tokens fit`() {
val budget = TokenBudget(limit = 500, used = 100)
assertTrue(budget.canFit(400))
}
@Test
fun `canFit returns false when over budget`() {
val budget = TokenBudget(limit = 500, used = 400)
assertFalse(budget.canFit(200))
}
@Test
fun `consume returns new instance with updated used`() {
val original = TokenBudget(limit = 1000, used = 0)
val after = original.consume(250)
assertEquals(0, original.used)
assertEquals(250, after.used)
assertEquals(750, after.remaining)
}
@Test
fun `fully exhausted budget cannot fit any tokens`() {
val budget = TokenBudget(limit = 100, used = 100)
assertFalse(budget.canFit(1))
assertEquals(0, budget.remaining)
}
}
@@ -0,0 +1,61 @@
import com.correx.core.validation.model.ValidationContext
import com.correx.core.validation.model.ValidationIssue
import com.correx.core.validation.model.ValidationSection
import com.correx.core.validation.model.ValidationSeverity
import com.correx.core.validation.pipeline.ValidationOutcome
import com.correx.core.validation.pipeline.ValidationPipeline
import com.correx.core.validation.pipeline.Validator
import com.correx.testing.fixtures.WorkflowFixtures
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertInstanceOf
import org.junit.jupiter.api.Test
class ValidationPipelineShortCircuitTest {
private val context = ValidationContext(WorkflowFixtures.simpleGraph())
private fun errorValidator(name: String) = Validator { _ ->
ValidationSection(
name = name,
issues = listOf(ValidationIssue("ERR", "failure", ValidationSeverity.ERROR))
)
}
@Test
fun `pipeline stops after first validator with errors and report contains only that section`() {
var secondCalled = false
val trackingValidator = Validator { _ ->
secondCalled = true
ValidationSection(name = "second")
}
val pipeline = ValidationPipeline(listOf(errorValidator("first"), trackingValidator))
val outcome = pipeline.validate(context) as ValidationOutcome.Rejected
assertFalse(secondCalled, "second validator must not run after a rejection")
assertEquals(1, outcome.report.sections.size)
assertEquals("first", outcome.report.sections[0].name)
}
@Test
fun `warnings do not short-circuit the pipeline`() {
var secondCalled = false
val warningValidator = Validator { _ ->
ValidationSection(
name = "first",
issues = listOf(ValidationIssue("WARN", "warning", ValidationSeverity.WARNING))
)
}
val trackingValidator = Validator { _ ->
secondCalled = true
ValidationSection(name = "second")
}
val pipeline = ValidationPipeline(listOf(warningValidator, trackingValidator))
pipeline.validate(context)
assert(secondCalled) { "warnings must not stop pipeline execution" }
}
}