epic-12: after epic audit and init commit
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
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(":infrastructure:persistence"))
|
||||
testImplementation(project(":testing:fixtures"))
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package com.correx.testing.approvals
|
||||
|
||||
object Module
|
||||
@@ -0,0 +1,88 @@
|
||||
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.approvals.ApprovalStatus
|
||||
import com.correx.core.approvals.GrantScope
|
||||
import com.correx.core.events.types.GrantId
|
||||
import com.correx.core.events.types.ProjectId
|
||||
import com.correx.core.events.types.SessionId
|
||||
import com.correx.core.events.types.StageId
|
||||
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.Test
|
||||
|
||||
class ApprovalEngineEdgeCasesTest {
|
||||
|
||||
private val engine = DefaultApprovalEngine()
|
||||
private val now = Instant.parse("2026-01-01T00:00:01Z")
|
||||
|
||||
private fun context(mode: ApprovalMode) = ApprovalContext(
|
||||
ApprovalScopeIdentity(SessionId("s1"), StageId("st1"), ProjectId("p1")),
|
||||
mode
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `grants from different scopes apply only to matching identity`() {
|
||||
val sessionGrant = ApprovalGrant(
|
||||
id = GrantId("g1"),
|
||||
scope = GrantScope.SESSION,
|
||||
permittedTiers = setOf(Tier.T3),
|
||||
reason = "",
|
||||
timestamp = Instant.parse("2026-01-01T00:00:00Z")
|
||||
)
|
||||
val stageGrant = ApprovalGrant(
|
||||
id = GrantId("g2"),
|
||||
scope = GrantScope.STAGE(StageId("st2")),
|
||||
permittedTiers = setOf(Tier.T4),
|
||||
reason = "",
|
||||
timestamp = Instant.parse("2026-01-01T00:00:00Z")
|
||||
)
|
||||
|
||||
val decision = engine.evaluate(
|
||||
request("r1", Tier.T4),
|
||||
context(ApprovalMode.DENY),
|
||||
listOf(sessionGrant, stageGrant),
|
||||
now
|
||||
)
|
||||
assertEquals(
|
||||
ApprovalStatus.PENDING, decision.state,
|
||||
"Stage grant for different stage should not apply"
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `expired grant is ignored`() {
|
||||
val past = Instant.parse("2025-12-31T23:59:59Z")
|
||||
val expiredGrant = ApprovalGrant(
|
||||
id = GrantId("g1"),
|
||||
scope = GrantScope.SESSION,
|
||||
permittedTiers = setOf(Tier.T2),
|
||||
reason = "",
|
||||
timestamp = past,
|
||||
expiresAt = past // expired before 'now'
|
||||
)
|
||||
val decision = engine.evaluate(
|
||||
request("r1", Tier.T2),
|
||||
context(ApprovalMode.DENY),
|
||||
listOf(expiredGrant),
|
||||
now
|
||||
)
|
||||
assertEquals(ApprovalStatus.PENDING, decision.state)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `timestamp passed to evaluate is used as resolution timestamp`() {
|
||||
val myNow = Instant.parse("2026-07-01T12:00:00Z")
|
||||
val decision = engine.evaluate(
|
||||
request("r1", Tier.T0),
|
||||
context(ApprovalMode.YOLO),
|
||||
emptyList(),
|
||||
myNow
|
||||
)
|
||||
assertEquals(myNow, decision.resolutionTimestamp)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import com.correx.core.validation.approval.ApprovalTrigger
|
||||
import com.correx.core.validation.model.ValidationIssue
|
||||
import com.correx.core.validation.model.ValidationReport
|
||||
import com.correx.core.validation.model.ValidationSection
|
||||
import com.correx.core.validation.model.ValidationSeverity
|
||||
import org.junit.jupiter.api.Assertions.assertNotNull
|
||||
import org.junit.jupiter.api.Assertions.assertNull
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class ApprovalTriggerTest {
|
||||
|
||||
private val trigger = ApprovalTrigger()
|
||||
|
||||
@Test
|
||||
fun `should trigger approval on error`() {
|
||||
|
||||
val report = ValidationReport(
|
||||
sections = listOf(
|
||||
ValidationSection(
|
||||
name = "graph",
|
||||
issues = listOf(
|
||||
ValidationIssue(
|
||||
code = "GRAPH_DANGLING_TRANSITION",
|
||||
message = "error",
|
||||
severity = ValidationSeverity.ERROR,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
val result = trigger.evaluate(report)
|
||||
|
||||
assertNotNull(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should not trigger approval on clean report`() {
|
||||
|
||||
val report = ValidationReport(
|
||||
sections = listOf(
|
||||
ValidationSection("graph", emptyList()),
|
||||
),
|
||||
)
|
||||
|
||||
val result = trigger.evaluate(report)
|
||||
|
||||
assertNull(result)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
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.ApprovalOutcome
|
||||
import com.correx.core.approvals.model.ApprovalScopeIdentity
|
||||
import com.correx.core.approvals.ApprovalStatus
|
||||
import com.correx.core.approvals.GrantScope
|
||||
import com.correx.core.events.types.GrantId
|
||||
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.assertNull
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class GrantSemanticsTest {
|
||||
|
||||
private val engine = DefaultApprovalEngine()
|
||||
|
||||
@Test
|
||||
fun `grant permits exact tier - auto_approved`() {
|
||||
val req = request("r1", Tier.T2)
|
||||
val grant = ApprovalGrant(
|
||||
id = GrantId("g1"),
|
||||
scope = GrantScope.SESSION,
|
||||
permittedTiers = setOf(Tier.T2),
|
||||
reason = "test",
|
||||
timestamp = Instant.parse("2026-01-01T00:00:00Z")
|
||||
)
|
||||
val ctx = ApprovalContext(
|
||||
ApprovalScopeIdentity(SessionId("s1"), null, null),
|
||||
ApprovalMode.DENY
|
||||
)
|
||||
val now = Instant.parse("2026-01-01T00:00:01Z")
|
||||
|
||||
val decision = engine.evaluate(req, ctx, listOf(grant), now)
|
||||
assertEquals(ApprovalOutcome.AUTO_APPROVED, decision.outcome)
|
||||
assertEquals(ApprovalStatus.COMPLETED, decision.state)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `grant does not permit other tiers`() {
|
||||
val req = request("r1", Tier.T3)
|
||||
val grant = ApprovalGrant(
|
||||
id = GrantId("g1"),
|
||||
scope = GrantScope.SESSION,
|
||||
permittedTiers = setOf(Tier.T2),
|
||||
reason = "test",
|
||||
timestamp = Instant.parse("2026-01-01T00:00:00Z")
|
||||
)
|
||||
val ctx = ApprovalContext(
|
||||
ApprovalScopeIdentity(SessionId("s1"), null, null),
|
||||
ApprovalMode.DENY
|
||||
)
|
||||
val now = Instant.parse("2026-01-01T00:00:01Z")
|
||||
|
||||
val decision = engine.evaluate(req, ctx, listOf(grant), now)
|
||||
assertEquals(ApprovalStatus.PENDING, decision.state)
|
||||
assertNull(decision.outcome)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `multiple grants - any match is enough`() {
|
||||
val req = request("r1", Tier.T4)
|
||||
val grants = listOf(
|
||||
ApprovalGrant(
|
||||
GrantId("g1"),
|
||||
GrantScope.SESSION,
|
||||
setOf(Tier.T3),
|
||||
"",
|
||||
Instant.parse("2026-01-01T00:00:00Z")
|
||||
),
|
||||
ApprovalGrant(
|
||||
GrantId("g2"),
|
||||
GrantScope.SESSION,
|
||||
setOf(Tier.T4),
|
||||
"",
|
||||
Instant.parse("2026-01-01T00:00:00Z")
|
||||
)
|
||||
)
|
||||
val ctx = ApprovalContext(
|
||||
ApprovalScopeIdentity(SessionId("s1"), null, null),
|
||||
ApprovalMode.DENY
|
||||
)
|
||||
val now = Instant.parse("2026-01-01T00:00:01Z")
|
||||
|
||||
val decision = engine.evaluate(req, ctx, grants, now)
|
||||
assertEquals(ApprovalOutcome.AUTO_APPROVED, decision.outcome)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
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.ApprovalOutcome
|
||||
import com.correx.core.approvals.model.ApprovalScopeIdentity
|
||||
import com.correx.core.approvals.ApprovalStatus
|
||||
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.Test
|
||||
|
||||
class ModeApprovalTest {
|
||||
|
||||
private val engine = DefaultApprovalEngine()
|
||||
|
||||
private fun contextForMode(mode: ApprovalMode) = ApprovalContext(
|
||||
ApprovalScopeIdentity(SessionId("s1"), null, null),
|
||||
mode
|
||||
)
|
||||
|
||||
private val now = Instant.parse("2026-01-01T00:00:01Z")
|
||||
|
||||
@Test
|
||||
fun `DENY mode allows only T0`() {
|
||||
val ctx = contextForMode(ApprovalMode.DENY)
|
||||
assertEquals(
|
||||
ApprovalOutcome.AUTO_APPROVED,
|
||||
engine.evaluate(request("r1", Tier.T0), ctx, emptyList(), now).outcome
|
||||
)
|
||||
assertEquals(
|
||||
ApprovalStatus.PENDING,
|
||||
engine.evaluate(request("r2", Tier.T1), ctx, emptyList(), now).state
|
||||
)
|
||||
assertEquals(
|
||||
ApprovalStatus.PENDING,
|
||||
engine.evaluate(request("r3", Tier.T4), ctx, emptyList(), now).state
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `PROMPT mode allows T0-T1`() {
|
||||
val ctx = contextForMode(ApprovalMode.PROMPT)
|
||||
assertEquals(
|
||||
ApprovalOutcome.AUTO_APPROVED,
|
||||
engine.evaluate(request("r1", Tier.T0), ctx, emptyList(), now).outcome
|
||||
)
|
||||
assertEquals(
|
||||
ApprovalOutcome.AUTO_APPROVED,
|
||||
engine.evaluate(request("r2", Tier.T1), ctx, emptyList(), now).outcome
|
||||
)
|
||||
assertEquals(
|
||||
ApprovalStatus.PENDING,
|
||||
engine.evaluate(request("r3", Tier.T2), ctx, emptyList(), now).state
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `AUTO mode allows T0-T2`() {
|
||||
val ctx = contextForMode(ApprovalMode.AUTO)
|
||||
assertEquals(
|
||||
ApprovalOutcome.AUTO_APPROVED,
|
||||
engine.evaluate(request("r1", Tier.T0), ctx, emptyList(), now).outcome
|
||||
)
|
||||
assertEquals(
|
||||
ApprovalOutcome.AUTO_APPROVED,
|
||||
engine.evaluate(request("r2", Tier.T2), ctx, emptyList(), now).outcome
|
||||
)
|
||||
assertEquals(
|
||||
ApprovalStatus.PENDING,
|
||||
engine.evaluate(request("r3", Tier.T3), ctx, emptyList(), now).state
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `YOLO mode allows all tiers, including T4`() {
|
||||
val ctx = contextForMode(ApprovalMode.YOLO)
|
||||
assertEquals(
|
||||
ApprovalOutcome.AUTO_APPROVED,
|
||||
engine.evaluate(request("r1", Tier.T0), ctx, emptyList(), now).outcome
|
||||
)
|
||||
assertEquals(
|
||||
ApprovalOutcome.AUTO_APPROVED,
|
||||
engine.evaluate(request("r2", Tier.T4), ctx, emptyList(), now).outcome
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
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.approvals.GrantScope
|
||||
import com.correx.core.events.types.GrantId
|
||||
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.Test
|
||||
import org.junit.jupiter.api.assertDoesNotThrow
|
||||
|
||||
class NoExecutionCouplingTest {
|
||||
|
||||
@Test
|
||||
fun `engine does not mutate grants or context`() {
|
||||
val engine = DefaultApprovalEngine()
|
||||
val grants = mutableListOf(
|
||||
ApprovalGrant(
|
||||
GrantId("g1"),
|
||||
GrantScope.SESSION,
|
||||
setOf(Tier.T2),
|
||||
"",
|
||||
Instant.parse("2026-01-01T00:00:00Z")
|
||||
)
|
||||
)
|
||||
val originalGrants = grants.toList()
|
||||
val ctx = ApprovalContext(
|
||||
ApprovalScopeIdentity(SessionId("s1"), null, null),
|
||||
ApprovalMode.DENY
|
||||
)
|
||||
val now = Instant.parse("2026-01-01T00:00:01Z")
|
||||
|
||||
engine.evaluate(request("r1", Tier.T2), ctx, grants, now)
|
||||
|
||||
assertEquals(originalGrants, grants, "Grants list must not be mutated")
|
||||
assertEquals(ApprovalMode.DENY, ctx.mode, "Context must not be mutated")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `engine does not throw for valid input`() {
|
||||
val engine = DefaultApprovalEngine()
|
||||
assertDoesNotThrow {
|
||||
engine.evaluate(
|
||||
request("r1", Tier.T0),
|
||||
ApprovalContext(
|
||||
ApprovalScopeIdentity(SessionId("s1"), null, null),
|
||||
ApprovalMode.YOLO
|
||||
),
|
||||
emptyList(),
|
||||
Instant.parse("2026-01-01T00:00:01Z")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
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.approvals.GrantScope
|
||||
import com.correx.core.sessions.ApprovalMode
|
||||
import com.correx.core.events.types.GrantId
|
||||
import com.correx.core.events.types.SessionId
|
||||
import com.correx.testing.fixtures.request
|
||||
import kotlinx.datetime.Instant
|
||||
import org.junit.jupiter.api.Assertions
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class TierImmutabilityTest {
|
||||
|
||||
private val engine = DefaultApprovalEngine()
|
||||
|
||||
@Test
|
||||
fun `engine never modifies tier - always returns original`() {
|
||||
val tiers = listOf(Tier.T0, Tier.T1, Tier.T2, Tier.T3, Tier.T4)
|
||||
val ctx = ApprovalContext(
|
||||
ApprovalScopeIdentity(SessionId("s1"), null, null),
|
||||
ApprovalMode.YOLO
|
||||
)
|
||||
val now = Instant.parse("2026-01-01T00:00:01Z")
|
||||
|
||||
tiers.forEach { tier ->
|
||||
val req = request("r1", tier)
|
||||
val decision = engine.evaluate(req, ctx, emptyList(), now)
|
||||
Assertions.assertEquals(tier, decision.tier, "Tier must remain unchanged")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `grant does not change tier in decision`() {
|
||||
val tier = Tier.T3
|
||||
val req = request("r1", tier)
|
||||
val grant = ApprovalGrant(
|
||||
id = GrantId("g1"),
|
||||
scope = GrantScope.SESSION,
|
||||
permittedTiers = setOf(tier),
|
||||
reason = "test",
|
||||
timestamp = Instant.parse("2026-01-01T00:00:00Z")
|
||||
)
|
||||
val ctx = ApprovalContext(
|
||||
ApprovalScopeIdentity(SessionId("s1"), null, null),
|
||||
ApprovalMode.DENY
|
||||
)
|
||||
val now = Instant.parse("2026-01-01T00:00:01Z")
|
||||
|
||||
val decision = engine.evaluate(req, ctx, listOf(grant), now)
|
||||
Assertions.assertEquals(tier, decision.tier)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user