cdee5f2245
Event log integrity (sole source of truth): - SqliteEventStore: serialize append/appendAll under a Mutex, enable WAL + busy_timeout + synchronous=NORMAL, roll back on any Throwable. Replaces the app-computed `SELECT MAX(seq)+1` read-modify-write that dropped events under concurrent appends (race was invisible to CI: only the in-memory store and single-threaded :memory: tests were ever exercised). - Serialization: encodeDefaults + ignoreUnknownKeys, explicit @SerialName on all 46 EventPayload subclasses and event-referenced enum constants, @Serializable on RiskLevel/RiskAction. Guards the append-only log against silent corruption from future class renames, field removals, or default-value changes. Approval/grant security (Invariant: approvals cannot widen authority): - Tier authorization is now a ceiling (request.tier.level <= max granted) instead of set membership; SESSION grants bind to a specific toolName instead of matching everything; handleCreateGrant rejects empty/over-tier/scopeless grants. Router chat wiring (Phase 0-2): ChatInput round-trip, StartChatSession from IDLE, router-panel layout, workflows behind Ctrl+W. Tests: SqliteEventStore concurrency, serialization round-trip + discriminator stability + unknown-key tolerance, adversarial grant scope/tier; updated existing approval and reducer tests for the new grant semantics and StartChatSession effect.
59 lines
2.0 KiB
Kotlin
59 lines
2.0 KiB
Kotlin
import com.correx.core.approvals.GrantScope
|
|
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.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")
|
|
)
|
|
}
|
|
}
|
|
}
|