219e2c762e
New core:artifacts-store interface + infrastructure/artifacts-cas implementation: segment files + SQLite index, Blake3 hashing, group-commit fsync via flushBefore, recovery tail-scan, manual compactor, and oldest-first disk-cap evictor. Inference events now carry promptArtifactId / responseArtifactId; orchestrators put bytes before emitting. SqliteEventStore wraps its txn in artifactStore.flushBefore so segment data is fsynced before the event commit, making the crash window non-corrupting (TailScanner re-indexes orphan tail records on reopen). Compactor and evictor are mutually exclusive via maintenanceMutex. Step 9 (cloud sync) deferred to a later epic. See docs/reviews/2026-05-18-cas-steps-1-8-review.md for the final review.
191 lines
7.1 KiB
Kotlin
191 lines
7.1 KiB
Kotlin
import com.correx.core.events.events.InferenceCompletedEvent
|
|
import com.correx.core.events.events.InferenceFailedEvent
|
|
import com.correx.core.events.events.InferenceStartedEvent
|
|
import com.correx.core.events.events.InferenceTimeoutEvent
|
|
import com.correx.core.events.events.ModelLoadedEvent
|
|
import com.correx.core.events.types.InferenceRequestId
|
|
import com.correx.core.events.types.ProviderId
|
|
import com.correx.core.events.types.SessionId
|
|
import com.correx.core.events.types.StageId
|
|
import com.correx.core.inference.DefaultInferenceReducer
|
|
import com.correx.core.inference.InferenceRecord
|
|
import com.correx.core.inference.InferenceReducer
|
|
import com.correx.core.inference.InferenceState
|
|
import com.correx.core.inference.InferenceStatus
|
|
import com.correx.core.inference.TokenUsage
|
|
import com.correx.core.utils.TypeId
|
|
import com.correx.testing.fixtures.EventFixtures.stored
|
|
import org.junit.jupiter.api.Assertions.assertEquals
|
|
import org.junit.jupiter.api.BeforeEach
|
|
import org.junit.jupiter.api.Test
|
|
|
|
class InferenceReducerTest {
|
|
|
|
private lateinit var reducer: InferenceReducer
|
|
|
|
@BeforeEach
|
|
fun setup() {
|
|
reducer = DefaultInferenceReducer()
|
|
}
|
|
|
|
@Test
|
|
fun `should start a new record on InferenceStartedEvent`() {
|
|
val initialState = InferenceState(emptyList())
|
|
val event = stored(
|
|
payload = InferenceStartedEvent(
|
|
requestId = InferenceRequestId("req1"),
|
|
sessionId = SessionId("sess1"),
|
|
stageId = StageId("stageA"),
|
|
providerId = ProviderId("providerX"),
|
|
promptArtifactId = TypeId("00".repeat(32)),
|
|
),
|
|
)
|
|
val newState = reducer.reduce(initialState, event)
|
|
|
|
assertEquals(1, newState.records.size)
|
|
val record = newState.records.first()
|
|
assertEquals(InferenceStatus.STARTED, record.status)
|
|
assertEquals("req1", record.requestId.value)
|
|
}
|
|
|
|
@Test
|
|
fun `should complete a record on InferenceCompletedEvent`() {
|
|
val startedRecord = InferenceRecord(
|
|
requestId = InferenceRequestId("req1"),
|
|
sessionId = SessionId("sess1"),
|
|
stageId = StageId("stageA"),
|
|
providerId = ProviderId("providerX"),
|
|
status = InferenceStatus.STARTED,
|
|
)
|
|
val initialState = InferenceState(listOf(startedRecord))
|
|
|
|
val event = stored(
|
|
payload = InferenceCompletedEvent(
|
|
requestId = InferenceRequestId("req1"),
|
|
sessionId = SessionId("sess1"),
|
|
stageId = StageId("stageA"),
|
|
providerId = ProviderId("providerX"),
|
|
tokensUsed = TokenUsage(50, 50),
|
|
latencyMs = 500L,
|
|
responseArtifactId = TypeId("00".repeat(32)),
|
|
),
|
|
)
|
|
val newState = reducer.reduce(initialState, event)
|
|
|
|
assertEquals(1, newState.records.size)
|
|
val record = newState.records.first()
|
|
assertEquals(InferenceStatus.COMPLETED, record.status)
|
|
assertEquals(100, record.tokensUsed?.totalTokens)
|
|
assertEquals(500L, record.latencyMs)
|
|
}
|
|
|
|
@Test
|
|
fun `should fail a record on InferenceFailedEvent`() {
|
|
val startedRecord = InferenceRecord(
|
|
requestId = InferenceRequestId("req1"),
|
|
sessionId = SessionId("sess1"),
|
|
stageId = StageId("stageA"),
|
|
providerId = ProviderId("providerX"),
|
|
status = InferenceStatus.STARTED,
|
|
)
|
|
val initialState = InferenceState(listOf(startedRecord))
|
|
|
|
val event = stored(
|
|
payload = InferenceFailedEvent(
|
|
requestId = InferenceRequestId("req1"),
|
|
sessionId = SessionId("sess1"),
|
|
stageId = StageId("stageA"),
|
|
providerId = ProviderId("providerX"),
|
|
reason = "Network failure",
|
|
),
|
|
)
|
|
val newState = reducer.reduce(initialState, event)
|
|
|
|
assertEquals(1, newState.records.size)
|
|
val record = newState.records.first()
|
|
assertEquals(InferenceStatus.FAILED, record.status)
|
|
assertEquals("Network failure", record.failureReason)
|
|
}
|
|
|
|
@Test
|
|
fun `should timeout a record on InferenceTimeoutEvent`() {
|
|
val startedRecord = InferenceRecord(
|
|
requestId = InferenceRequestId("req1"),
|
|
sessionId = SessionId("sess1"),
|
|
stageId = StageId("stageA"),
|
|
providerId = ProviderId("providerX"),
|
|
status = InferenceStatus.STARTED,
|
|
)
|
|
val initialState = InferenceState(listOf(startedRecord))
|
|
|
|
val event = stored(
|
|
payload = InferenceTimeoutEvent(
|
|
requestId = InferenceRequestId("req1"),
|
|
sessionId = SessionId("sess1"),
|
|
stageId = StageId("stageA"),
|
|
providerId = ProviderId("providerX"),
|
|
timeoutMs = 10000L,
|
|
),
|
|
)
|
|
val newState = reducer.reduce(initialState, event)
|
|
|
|
assertEquals(1, newState.records.size)
|
|
val record = newState.records.first()
|
|
assertEquals(InferenceStatus.TIMED_OUT, record.status)
|
|
assertEquals(10000L, record.latencyMs)
|
|
}
|
|
|
|
@Test
|
|
fun `should pass state unchanged on unrelated events`() {
|
|
val startedRecord = InferenceRecord(
|
|
requestId = InferenceRequestId("req1"),
|
|
sessionId = SessionId("sess1"),
|
|
stageId = StageId("stageA"),
|
|
providerId = ProviderId("providerX"),
|
|
status = InferenceStatus.STARTED,
|
|
)
|
|
val initialState = InferenceState(listOf(startedRecord))
|
|
|
|
val event = stored(
|
|
payload = ModelLoadedEvent(
|
|
sessionId = SessionId("sess1"),
|
|
providerId = ProviderId("providerX"),
|
|
modelId = "model",
|
|
),
|
|
)
|
|
val newState = reducer.reduce(initialState, event)
|
|
|
|
assertEquals(1, newState.records.size)
|
|
assertEquals(InferenceStatus.STARTED, newState.records.first().status)
|
|
}
|
|
|
|
@Test
|
|
fun `should not modify record if requestId does not match`() {
|
|
val startedRecord = InferenceRecord(
|
|
requestId = InferenceRequestId("req1"),
|
|
sessionId = SessionId("sess1"),
|
|
stageId = StageId("stageA"),
|
|
providerId = ProviderId("providerX"),
|
|
status = InferenceStatus.STARTED,
|
|
)
|
|
val initialState = InferenceState(listOf(startedRecord))
|
|
|
|
// Event for a different request
|
|
val event = stored(
|
|
payload = InferenceCompletedEvent(
|
|
requestId = InferenceRequestId("req2"),
|
|
sessionId = SessionId("sess1"),
|
|
stageId = StageId("stageA"),
|
|
providerId = ProviderId("providerX"),
|
|
tokensUsed = TokenUsage(50, 50),
|
|
latencyMs = 500L,
|
|
responseArtifactId = TypeId("00".repeat(32)),
|
|
),
|
|
)
|
|
val newState = reducer.reduce(initialState, event)
|
|
|
|
// The record should remain unchanged
|
|
assertEquals(1, newState.records.size)
|
|
assertEquals(InferenceStatus.STARTED, newState.records.first().status)
|
|
}
|
|
} |