epic-12: after epic audit and init commit
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
plugins {
|
||||
id 'java-library'
|
||||
id 'org.jetbrains.kotlin.jvm'
|
||||
id 'org.jetbrains.kotlin.plugin.serialization'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":core:events"))
|
||||
implementation(project(":core:kernel"))
|
||||
implementation(project(":core:sessions"))
|
||||
implementation(project(":core:inference"))
|
||||
implementation project(':testing:fixtures')
|
||||
|
||||
testImplementation "org.jetbrains.kotlin:kotlin-test"
|
||||
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test"
|
||||
testImplementation project(':infrastructure:persistence')
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.correx.testing.kernel
|
||||
|
||||
import com.correx.core.events.orchestration.OrchestrationState
|
||||
import com.correx.core.events.orchestration.OrchestrationStatus
|
||||
import com.correx.core.events.types.SessionId
|
||||
import com.correx.core.events.types.StageId
|
||||
import com.correx.core.sessions.SessionState
|
||||
import com.correx.core.sessions.SessionStatus
|
||||
import com.correx.core.sessions.projections.replay.EventReplayer
|
||||
import kotlinx.datetime.Clock
|
||||
|
||||
class MockOrchestrationEventReplayer : EventReplayer<OrchestrationState> {
|
||||
private val states = mutableMapOf<SessionId, OrchestrationState>()
|
||||
|
||||
fun setSessionState(sessionId: SessionId, state: OrchestrationState) {
|
||||
states[sessionId] = state
|
||||
}
|
||||
|
||||
override fun rebuild(sessionId: SessionId): OrchestrationState {
|
||||
return states[sessionId] ?: OrchestrationState(
|
||||
status = OrchestrationStatus.RUNNING,
|
||||
currentStageId = StageId("stage-1"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class MockSessionEventReplayer : EventReplayer<SessionState> {
|
||||
private val states = mutableMapOf<SessionId, SessionState>()
|
||||
|
||||
fun setSessionState(sessionId: SessionId, state: SessionState) {
|
||||
states[sessionId] = state
|
||||
}
|
||||
|
||||
override fun rebuild(sessionId: SessionId): SessionState {
|
||||
return states[sessionId] ?: SessionState(
|
||||
status = SessionStatus.ACTIVE,
|
||||
createdAt = Clock.System.now(),
|
||||
updatedAt = Clock.System.now(),
|
||||
invalidTransitions = 0,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
import com.correx.core.events.events.RetryAttemptedEvent
|
||||
import com.correx.core.events.stores.EventStore
|
||||
import com.correx.core.events.types.SessionId
|
||||
import com.correx.core.events.types.StageId
|
||||
import com.correx.core.events.execution.RetryPolicy
|
||||
import com.correx.core.kernel.retry.DefaultRetryCoordinator
|
||||
import com.correx.infrastructure.persistence.InMemoryEventStore
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.currentTime
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Assertions
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.util.*
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class DefaultRetryCoordinatorTest {
|
||||
|
||||
private lateinit var eventStore: EventStore
|
||||
private lateinit var retryCoordinator: DefaultRetryCoordinator
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
eventStore = InMemoryEventStore()
|
||||
retryCoordinator = DefaultRetryCoordinator(eventStore)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should return true when currentAttempt is 0 and maxAttempts is 3`() = runTest {
|
||||
val sessionId = SessionId(UUID.randomUUID().toString())
|
||||
val stageId = StageId("stage1")
|
||||
val policy = RetryPolicy(maxAttempts = 3, backoffMs = 0L)
|
||||
|
||||
val result = retryCoordinator.shouldRetry(
|
||||
sessionId = sessionId,
|
||||
stageId = stageId,
|
||||
currentAttempt = 0,
|
||||
policy = policy,
|
||||
failureReason = "test failure",
|
||||
)
|
||||
|
||||
Assertions.assertTrue(result)
|
||||
|
||||
val events = eventStore.read(sessionId)
|
||||
Assertions.assertEquals(1, events.size)
|
||||
val event = events.first().payload as RetryAttemptedEvent
|
||||
Assertions.assertEquals(1, event.attemptNumber)
|
||||
Assertions.assertEquals(stageId, event.stageId)
|
||||
Assertions.assertEquals("test failure", event.failureReason)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should return true when currentAttempt is 2 and maxAttempts is 3`() = runTest {
|
||||
val sessionId = SessionId(UUID.randomUUID().toString())
|
||||
val stageId = StageId("stage1")
|
||||
val policy = RetryPolicy(maxAttempts = 3, backoffMs = 0L)
|
||||
|
||||
// First retry
|
||||
retryCoordinator.shouldRetry(
|
||||
sessionId = sessionId,
|
||||
stageId = stageId,
|
||||
currentAttempt = 0,
|
||||
policy = policy,
|
||||
failureReason = "test failure",
|
||||
)
|
||||
|
||||
// Second retry (currentAttempt = 2, which is the third attempt)
|
||||
val result = retryCoordinator.shouldRetry(
|
||||
sessionId = sessionId,
|
||||
stageId = stageId,
|
||||
currentAttempt = 2,
|
||||
policy = policy,
|
||||
failureReason = "test failure",
|
||||
)
|
||||
|
||||
Assertions.assertTrue(result)
|
||||
|
||||
val events = eventStore.read(sessionId)
|
||||
Assertions.assertEquals(2, events.size)
|
||||
val lastEvent = events.last().payload as RetryAttemptedEvent
|
||||
Assertions.assertEquals(3, lastEvent.attemptNumber)
|
||||
Assertions.assertEquals("test failure", lastEvent.failureReason)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should return false when currentAttempt equals maxAttempts`() = runTest {
|
||||
val sessionId = SessionId(UUID.randomUUID().toString())
|
||||
val stageId = StageId("stage1")
|
||||
val policy = RetryPolicy(maxAttempts = 3, backoffMs = 0L)
|
||||
|
||||
// First two retries
|
||||
retryCoordinator.shouldRetry(
|
||||
sessionId = sessionId,
|
||||
stageId = stageId,
|
||||
currentAttempt = 0,
|
||||
policy = policy,
|
||||
failureReason = "test failure",
|
||||
)
|
||||
retryCoordinator.shouldRetry(
|
||||
sessionId = sessionId,
|
||||
stageId = stageId,
|
||||
currentAttempt = 1,
|
||||
policy = policy,
|
||||
failureReason = "test failure",
|
||||
)
|
||||
|
||||
// currentAttempt = 3 equals maxAttempts = 3, should not retry
|
||||
val result = retryCoordinator.shouldRetry(
|
||||
sessionId = sessionId,
|
||||
stageId = stageId,
|
||||
currentAttempt = 3,
|
||||
policy = policy,
|
||||
failureReason = "test failure",
|
||||
)
|
||||
|
||||
Assertions.assertFalse(result)
|
||||
|
||||
val events = eventStore.read(sessionId)
|
||||
Assertions.assertEquals(2, events.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should return false when currentAttempt is greater than maxAttempts`() = runTest {
|
||||
val sessionId = SessionId(UUID.randomUUID().toString())
|
||||
val stageId = StageId("stage1")
|
||||
val policy = RetryPolicy(maxAttempts = 3, backoffMs = 0L)
|
||||
|
||||
// First retry
|
||||
retryCoordinator.shouldRetry(
|
||||
sessionId = sessionId,
|
||||
stageId = stageId,
|
||||
currentAttempt = 0,
|
||||
policy = policy,
|
||||
failureReason = "test failure",
|
||||
)
|
||||
|
||||
// currentAttempt = 5 > maxAttempts = 3, should not retry
|
||||
val result = retryCoordinator.shouldRetry(
|
||||
sessionId = sessionId,
|
||||
stageId = stageId,
|
||||
currentAttempt = 5,
|
||||
policy = policy,
|
||||
failureReason = "test failure",
|
||||
)
|
||||
|
||||
Assertions.assertFalse(result)
|
||||
|
||||
val events = eventStore.read(sessionId)
|
||||
Assertions.assertEquals(1, events.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun `should delay by backoffMs before retrying`() = runTest {
|
||||
val policy = RetryPolicy(maxAttempts = 3, backoffMs = 1000L)
|
||||
|
||||
retryCoordinator.shouldRetry(
|
||||
sessionId = SessionId(UUID.randomUUID().toString()),
|
||||
stageId = StageId("stage1"),
|
||||
currentAttempt = 0,
|
||||
policy = policy,
|
||||
failureReason = "test failure",
|
||||
)
|
||||
|
||||
// virtual time should have advanced by backoffMs
|
||||
assertEquals(1000L, currentTime)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
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.FinishReason
|
||||
import com.correx.core.inference.ProviderHealth
|
||||
import com.correx.core.inference.TokenUsage
|
||||
import com.correx.core.kernel.replay.ReplayInferenceProvider
|
||||
import com.correx.core.kernel.retry.exception.ReplayArtifactMissingException
|
||||
import com.correx.infrastructure.persistence.InMemoryEventStore
|
||||
import com.correx.testing.fixtures.InferenceFixtures
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Assertions
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.assertThrows
|
||||
|
||||
class ReplayInferenceProviderTest {
|
||||
|
||||
private lateinit var eventStore: InMemoryEventStore
|
||||
private lateinit var replayProvider: ReplayInferenceProvider
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
eventStore = InMemoryEventStore()
|
||||
replayProvider = ReplayInferenceProvider(eventStore)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `response carries correct tokensUsed and latencyMs when matching stageId`() = runTest {
|
||||
val sessionId = SessionId("s1")
|
||||
val stageId = StageId("stage1")
|
||||
val requestId = InferenceRequestId("req1")
|
||||
val providerId = ProviderId("test-provider")
|
||||
val tokensUsed = TokenUsage(promptTokens = 100, completionTokens = 200)
|
||||
val latencyMs = 500L
|
||||
|
||||
// Record an inference completed event
|
||||
eventStore.append(
|
||||
InferenceFixtures.newInferenceCompletedEvent(
|
||||
requestId = requestId,
|
||||
sessionId = sessionId,
|
||||
stageId = stageId,
|
||||
providerId = providerId,
|
||||
tokensUsed = tokensUsed,
|
||||
latencyMs = latencyMs,
|
||||
),
|
||||
)
|
||||
|
||||
val request = InferenceFixtures.inferenceRequest(
|
||||
sessionId = sessionId,
|
||||
stageId = stageId,
|
||||
requestId = requestId,
|
||||
)
|
||||
|
||||
val response = replayProvider.infer(request)
|
||||
|
||||
Assertions.assertEquals(request.requestId, response.requestId)
|
||||
Assertions.assertEquals(tokensUsed, response.tokensUsed)
|
||||
Assertions.assertEquals(latencyMs, response.latencyMs)
|
||||
Assertions.assertEquals("", response.text)
|
||||
Assertions.assertEquals(FinishReason.Stop, response.finishReason)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ReplayArtifactMissingException thrown when event for different stageId`(): Unit = runBlocking {
|
||||
val sessionId = SessionId("s1")
|
||||
val stageId1 = StageId("stage1")
|
||||
val stageId2 = StageId("stage2")
|
||||
val requestId = InferenceRequestId("req1")
|
||||
val providerId = ProviderId("test-provider")
|
||||
|
||||
// Record event for stage1
|
||||
eventStore.append(
|
||||
InferenceFixtures.newInferenceCompletedEvent(
|
||||
requestId = requestId,
|
||||
sessionId = sessionId,
|
||||
stageId = stageId1,
|
||||
providerId = providerId,
|
||||
tokensUsed = TokenUsage(promptTokens = 100, completionTokens = 200),
|
||||
latencyMs = 500L,
|
||||
),
|
||||
)
|
||||
|
||||
val request = InferenceFixtures.inferenceRequest(
|
||||
sessionId = sessionId,
|
||||
stageId = stageId2, // Different stage
|
||||
requestId = requestId,
|
||||
)
|
||||
|
||||
assertThrows<ReplayArtifactMissingException> {
|
||||
replayProvider.infer(request)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ReplayArtifactMissingException thrown when event store is empty`(): Unit = runBlocking {
|
||||
val sessionId = SessionId("s1")
|
||||
val stageId = StageId("stage1")
|
||||
val requestId = InferenceRequestId("req1")
|
||||
|
||||
val request = InferenceFixtures.inferenceRequest(
|
||||
sessionId = sessionId,
|
||||
stageId = stageId,
|
||||
requestId = requestId,
|
||||
)
|
||||
|
||||
assertThrows<ReplayArtifactMissingException> {
|
||||
replayProvider.infer(request)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `text is empty string by design`() = runTest {
|
||||
val sessionId = SessionId("s1")
|
||||
val stageId = StageId("stage1")
|
||||
val requestId = InferenceRequestId("req1")
|
||||
val providerId = ProviderId("test-provider")
|
||||
|
||||
eventStore.append(
|
||||
InferenceFixtures.newInferenceCompletedEvent(
|
||||
requestId = requestId,
|
||||
sessionId = sessionId,
|
||||
stageId = stageId,
|
||||
providerId = providerId,
|
||||
tokensUsed = TokenUsage(promptTokens = 100, completionTokens = 50),
|
||||
latencyMs = 300L,
|
||||
),
|
||||
)
|
||||
|
||||
val request = InferenceFixtures.inferenceRequest(
|
||||
sessionId = sessionId,
|
||||
stageId = stageId,
|
||||
requestId = requestId,
|
||||
)
|
||||
|
||||
val response = replayProvider.infer(request)
|
||||
|
||||
Assertions.assertEquals("", response.text)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `capabilities returns empty set`() {
|
||||
val capabilities = replayProvider.capabilities()
|
||||
|
||||
Assertions.assertTrue(capabilities.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `healthCheck returns Healthy`() = runTest {
|
||||
val health = replayProvider.healthCheck()
|
||||
|
||||
Assertions.assertEquals(ProviderHealth.Healthy, health)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `id is ReplayProvider`() {
|
||||
Assertions.assertEquals(ProviderId("replay-provider"), replayProvider.id)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user