test(fixtures): DeterministicHarness — event-log-in, derived-state-out builder
Provides a reusable builder that packages the event-log-in → derived-state-out pattern for deterministic tests. Implements fixed clock and deterministic event ID generation to ensure byte-identical event JSON across test runs with same payloads. Acceptance criteria met: - Two harnesses fed same payloads yield byte-identical stored event JSON - Fixed clock ensures timestamp determinism (2026-01-01T00:00:00Z) - Event IDs are deterministically generated (event-0, event-1, etc.) - Supports both givenEvents (append) and rebuild (replay via projection)
This commit is contained in:
@@ -14,5 +14,6 @@ dependencies {
|
|||||||
implementation(project(":core:context"))
|
implementation(project(":core:context"))
|
||||||
implementation(project(":core:kernel"))
|
implementation(project(":core:kernel"))
|
||||||
implementation(project(":core:validation"))
|
implementation(project(":core:validation"))
|
||||||
|
implementation(project(":infrastructure:persistence"))
|
||||||
}
|
}
|
||||||
tasks.named("koverVerify").configure { enabled = false }
|
tasks.named("koverVerify").configure { enabled = false }
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package com.correx.testing.fixtures
|
||||||
|
|
||||||
|
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.types.EventId
|
||||||
|
import com.correx.core.events.types.SessionId
|
||||||
|
import com.correx.infrastructure.persistence.InMemoryEventStore
|
||||||
|
import com.correx.core.sessions.projections.Projection
|
||||||
|
import com.correx.core.sessions.projections.replay.DefaultEventReplayer
|
||||||
|
import kotlinx.datetime.Instant
|
||||||
|
|
||||||
|
class DeterministicHarness {
|
||||||
|
val eventStore: InMemoryEventStore = InMemoryEventStore()
|
||||||
|
val clock: Instant = Instant.parse("2026-01-01T00:00:00Z")
|
||||||
|
|
||||||
|
private var eventCounter = 0
|
||||||
|
|
||||||
|
suspend fun givenEvents(sessionId: SessionId, vararg payloads: EventPayload) {
|
||||||
|
val events = payloads.map { payload ->
|
||||||
|
val eventId = EventId("event-${eventCounter++}")
|
||||||
|
NewEvent(
|
||||||
|
metadata = EventMetadata(
|
||||||
|
eventId = eventId,
|
||||||
|
sessionId = sessionId,
|
||||||
|
timestamp = clock,
|
||||||
|
schemaVersion = 1,
|
||||||
|
causationId = null,
|
||||||
|
correlationId = null
|
||||||
|
),
|
||||||
|
payload = payload
|
||||||
|
)
|
||||||
|
}
|
||||||
|
eventStore.appendAll(events)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <S> rebuild(sessionId: SessionId, projection: Projection<S>): S {
|
||||||
|
val replayer = DefaultEventReplayer(eventStore, projection)
|
||||||
|
return replayer.rebuild(sessionId)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import com.correx.core.events.events.ChatSessionStartedEvent
|
||||||
|
import com.correx.core.events.events.StoredEvent
|
||||||
|
import com.correx.core.events.events.ToolInvokedEvent
|
||||||
|
import com.correx.core.events.serialization.eventJson
|
||||||
|
import com.correx.core.events.types.SessionId
|
||||||
|
import com.correx.core.sessions.projections.Projection
|
||||||
|
import com.correx.testing.fixtures.DeterministicHarness
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
|
class DeterministicHarnessTest {
|
||||||
|
|
||||||
|
private data class SimpleState(val eventCount: Int = 0)
|
||||||
|
|
||||||
|
private val simpleProjection = object : Projection<SimpleState> {
|
||||||
|
override fun initial() = SimpleState()
|
||||||
|
override fun apply(state: SimpleState, event: StoredEvent) =
|
||||||
|
state.copy(eventCount = state.eventCount + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `two harnesses with same payloads yield identical events`() = runBlocking {
|
||||||
|
val harness1 = DeterministicHarness()
|
||||||
|
val harness2 = DeterministicHarness()
|
||||||
|
|
||||||
|
val sessionId = SessionId("test-session")
|
||||||
|
val payload1 = ChatSessionStartedEvent(sessionId)
|
||||||
|
val payload2 = ToolInvokedEvent("read_file")
|
||||||
|
|
||||||
|
harness1.givenEvents(sessionId, payload1, payload2)
|
||||||
|
harness2.givenEvents(sessionId, payload1, payload2)
|
||||||
|
|
||||||
|
val events1 = harness1.eventStore.read(sessionId)
|
||||||
|
val events2 = harness2.eventStore.read(sessionId)
|
||||||
|
|
||||||
|
assertEquals(events1.size, events2.size)
|
||||||
|
events1.zip(events2).forEach { (e1, e2) ->
|
||||||
|
assertEquals(e1.metadata.eventId, e2.metadata.eventId, "eventIds must match")
|
||||||
|
assertEquals(e1.metadata.timestamp, e2.metadata.timestamp, "timestamps must match")
|
||||||
|
assertEquals(e1.sequence, e2.sequence, "sequences must match")
|
||||||
|
assertEquals(e1.sessionSequence, e2.sessionSequence, "sessionSequences must match")
|
||||||
|
assertEquals(
|
||||||
|
eventJson.encodeToString(StoredEvent.serializer(), e1),
|
||||||
|
eventJson.encodeToString(StoredEvent.serializer(), e2),
|
||||||
|
"stored event JSON must be byte-identical",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `rebuild applies events deterministically`() = runBlocking {
|
||||||
|
val harness = DeterministicHarness()
|
||||||
|
val sessionId = SessionId("rebuild-test")
|
||||||
|
|
||||||
|
harness.givenEvents(
|
||||||
|
sessionId,
|
||||||
|
ChatSessionStartedEvent(sessionId),
|
||||||
|
ToolInvokedEvent("read_file"),
|
||||||
|
ToolInvokedEvent("write_file")
|
||||||
|
)
|
||||||
|
|
||||||
|
val state = harness.rebuild(sessionId, simpleProjection)
|
||||||
|
assertEquals(3, state.eventCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clock is fixed for deterministic timestamps`(): Unit = runBlocking {
|
||||||
|
val harness = DeterministicHarness()
|
||||||
|
val sessionId = SessionId("clock-test")
|
||||||
|
|
||||||
|
harness.givenEvents(sessionId, ChatSessionStartedEvent(sessionId))
|
||||||
|
|
||||||
|
val event = harness.eventStore.read(sessionId).first()
|
||||||
|
assertEquals("2026-01-01T00:00:00Z", event.metadata.timestamp.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user