import com.correx.core.approvals.ApprovalProjector import com.correx.core.approvals.DefaultApprovalReducer import com.correx.core.approvals.DefaultApprovalRepository import com.correx.core.approvals.domain.DefaultApprovalEngine import com.correx.core.artifactstore.ArtifactStore import com.correx.core.artifacts.DefaultArtifactReducer import com.correx.core.events.events.ArtifactContentStoredEvent import com.correx.core.events.events.EventMetadata import com.correx.core.events.events.NewEvent import com.correx.core.events.types.ArtifactId import com.correx.core.events.types.EventId import com.correx.core.events.types.SessionId import com.correx.core.events.types.StageId import com.correx.core.inference.InferenceRepository import com.correx.core.inference.InferenceState import com.correx.core.journal.DecisionJournalProjector import com.correx.core.journal.DefaultDecisionJournalReducer import com.correx.core.journal.DefaultDecisionJournalRepository import com.correx.core.kernel.orchestration.DefaultOrchestrationReducer import com.correx.core.kernel.orchestration.DefaultSessionOrchestrator import com.correx.core.kernel.orchestration.OrchestrationProjector import com.correx.core.kernel.orchestration.OrchestrationRepository import com.correx.core.kernel.orchestration.OrchestratorEngines import com.correx.core.kernel.orchestration.OrchestratorRepositories import com.correx.core.kernel.retry.DefaultRetryCoordinator import com.correx.core.risk.DefaultRiskAssessor import com.correx.core.sessions.DefaultSessionRepository import com.correx.core.sessions.projections.replay.DefaultEventReplayer import com.correx.core.sessions.projections.replay.EventReplayer import com.correx.core.utils.TypeId import com.correx.core.validation.pipeline.ValidationPipeline import com.correx.infrastructure.persistence.InMemoryEventStore import com.correx.infrastructure.persistence.artifact.LiveArtifactRepository import com.correx.testing.fixtures.InferenceFixtures import com.correx.testing.fixtures.context.ContextFixtures import com.correx.testing.fixtures.transitions.TransitionFixtures import com.correx.testing.kernel.MockSessionEventReplayer import kotlinx.coroutines.runBlocking import kotlinx.datetime.Clock import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test import java.util.UUID class RehydrateTest { private val sessionId: SessionId = TypeId("session-rehydrate") // The CAS is content-addressed: the stored key is a content hash, NOT the // logical slot name. F-021 — rehydrate must resolve slot.name -> contentHash // via ArtifactContentStoredEvent and fetch by hash, never pass the slot name // (which is non-hex and would crash a real CAS) to ArtifactStore.get. private val slotName: ArtifactId = TypeId("analysis") private val contentHash: ArtifactId = TypeId("a1b2c3d4") private val artifactContent = """{"status":"success"}""" private val eventStore = InMemoryEventStore() private val fakeArtifactStore: ArtifactStore = object : ArtifactStore { override suspend fun put(bytes: ByteArray): ArtifactId = contentHash override suspend fun get(id: ArtifactId): ByteArray? = if (id == contentHash) artifactContent.toByteArray(Charsets.UTF_8) else null override suspend fun flushBefore(commit: suspend () -> Unit) = commit() } private val orchestrationRepository = OrchestrationRepository( DefaultEventReplayer(eventStore, OrchestrationProjector(DefaultOrchestrationReducer())), ) private val inferenceRepository = InferenceRepository( object : EventReplayer { override fun rebuild(sessionId: SessionId) = InferenceState() }, ) private val approvalRepository = DefaultApprovalRepository( DefaultEventReplayer(eventStore, ApprovalProjector(DefaultApprovalReducer())), ) private val decisionJournalRepository = DefaultDecisionJournalRepository( DefaultEventReplayer(eventStore, DecisionJournalProjector(DefaultDecisionJournalReducer())), ) private val repositories = OrchestratorRepositories( eventStore = eventStore, inferenceRepository = inferenceRepository, orchestrationRepository = orchestrationRepository, sessionRepository = DefaultSessionRepository(MockSessionEventReplayer()), artifactRepository = LiveArtifactRepository(eventStore, DefaultArtifactReducer()), approvalRepository = approvalRepository, ) private val engines = OrchestratorEngines( transitionResolver = TransitionFixtures.simpleResolver(), contextPackBuilder = ContextFixtures.simpleBuilder(), inferenceRouter = InferenceFixtures.fixedRouter(), validationPipeline = ValidationPipeline(validators = emptyList()), approvalEngine = DefaultApprovalEngine(), riskAssessor = DefaultRiskAssessor(), ) private val orchestrator = DefaultSessionOrchestrator( repositories = repositories, engines = engines, retryCoordinator = DefaultRetryCoordinator(eventStore), artifactStore = fakeArtifactStore, decisionJournalRepository = decisionJournalRepository, ) private suspend fun appendContentStoredEvent() { eventStore.append( NewEvent( metadata = EventMetadata( eventId = EventId(UUID.randomUUID().toString()), sessionId = sessionId, timestamp = Clock.System.now(), schemaVersion = 1, causationId = null, correlationId = null, ), payload = ArtifactContentStoredEvent( artifactId = slotName, contentHash = contentHash, sessionId = sessionId, stageId = StageId("stage-1"), ), ), ) } @Test fun `rehydrate resolves slot name to content hash and populates cache`(): Unit = runBlocking { appendContentStoredEvent() orchestrator.rehydrate(sessionId) // Cache is keyed by the logical slot name; content fetched from CAS by hash. val cached = orchestrator.validatedArtifactContent(sessionId, slotName) assertEquals(artifactContent, cached) } @Test fun `rehydrate is a no-op when there are no ArtifactContentStoredEvents`(): Unit = runBlocking { orchestrator.rehydrate(sessionId) val cached = orchestrator.validatedArtifactContent(sessionId, slotName) assertEquals(null, cached) } }