89487db72a
rehydrate() now scans ArtifactContentStoredEvent (the durable slot->CAS-hash bridge) and fetches stored bytes by content hash, keying the cache by the logical slot name. The slot name is never passed to the hash-keyed CAS, so the odd-length-hex crash is gone and artifact content is restored on cold-start resume. RehydrateTest reworked to use a content-addressed fake store (slot name != content hash) so it actually exercises the bug. Replay (#8) is unaffected — rehydrate is live-resume-only. Also bump router narration maxTokens 1024 -> 4096: reasoning models were spending the whole budget thinking and emitting empty content (finishReason= length), so narrations never reached the TUI. Removes the stale TUI-refactor progress doc.
149 lines
6.5 KiB
Kotlin
149 lines
6.5 KiB
Kotlin
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<InferenceState> {
|
|
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)
|
|
}
|
|
}
|