import com.correx.core.context.model.ContextLayer import com.correx.core.events.events.EventMetadata import com.correx.core.events.events.NewEvent import com.correx.core.events.events.RepoKnowledgeHit import com.correx.core.events.events.RepoKnowledgeRetrievedEvent import com.correx.core.events.events.RepoMapComputedEvent import com.correx.core.events.events.RepoMapEntry import com.correx.core.events.events.StoredEvent 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.kernel.orchestration.buildRelevantFilesEntry import com.correx.core.sessions.projections.Projection import com.correx.core.sessions.projections.replay.DefaultEventReplayer import com.correx.infrastructure.persistence.InMemoryEventStore import kotlinx.coroutines.runBlocking import kotlinx.datetime.Clock import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test /** * Projects the last RepoKnowledgeRetrievedEvent for a given stage. * Returns null when no such event is recorded — replay can detect the absence. */ private class RepoKnowledgeProjection( private val stageId: StageId, ) : Projection { override fun initial(): RepoKnowledgeRetrievedEvent? = null override fun apply(state: RepoKnowledgeRetrievedEvent?, event: StoredEvent): RepoKnowledgeRetrievedEvent? = when (val p = event.payload) { is RepoKnowledgeRetrievedEvent -> if (p.stageId == stageId) p else state else -> state } } /** * Projects the latest RepoMapComputedEvent — used for the legacy fallback path. */ private class RepoMapComputedProjection : Projection { override fun initial(): RepoMapComputedEvent? = null override fun apply(state: RepoMapComputedEvent?, event: StoredEvent): RepoMapComputedEvent? = when (val p = event.payload) { is RepoMapComputedEvent -> p else -> state } } /** * Proves that context derivation reads the recorded RepoKnowledgeRetrievedEvent * (invariant #9 — replay never re-queries the vector index). */ class RepoKnowledgeReplayTest { private val sessionId = SessionId("s-replay-1") private val stageId = StageId("impl") @Test fun `replay test A - recorded hits produce relevantFiles entry without calling retriever`(): Unit = runBlocking { val store = InMemoryEventStore() store.append( NewEvent( metadata = EventMetadata( eventId = EventId("rkr1"), sessionId = sessionId, timestamp = Clock.System.now(), schemaVersion = 1, causationId = null, correlationId = null, ), payload = RepoKnowledgeRetrievedEvent( sessionId = sessionId, stageId = stageId, query = "parse the input", hits = listOf(RepoKnowledgeHit("src/Parser.kt", "src/Parser.kt: Parser", 0.95f)), ), ), ) val recorded = DefaultEventReplayer(store, RepoKnowledgeProjection(stageId)).rebuild(sessionId) // Context derivation: recorded event present → build relevant-files entry from hits. // Retriever is never consulted (null). This mirrors the guard in buildContextualRepoEntries. requireNotNull(recorded) { "expected RepoKnowledgeRetrievedEvent to be recorded" } val entry = buildRelevantFilesEntry(recorded.hits) assertEquals("relevantFiles", entry.sourceType) assertEquals(ContextLayer.L3, entry.layer) assertTrue( entry.content.contains("src/Parser.kt: Parser"), "recorded hit text must appear in context entry content", ) } @Test fun `replay test B - log without retrieved event falls back to repoMap sourceType`(): Unit = runBlocking { val store = InMemoryEventStore() val mapEntries = listOf( RepoMapEntry("src/Main.kt", 1.0, listOf("main")), RepoMapEntry("src/Util.kt", 0.5, listOf("clamp")), ) store.append( NewEvent( metadata = EventMetadata( eventId = EventId("rmc1"), sessionId = sessionId, timestamp = Clock.System.now(), schemaVersion = 1, causationId = null, correlationId = null, ), payload = RepoMapComputedEvent( sessionId = sessionId, repoRoot = "/repo", entries = mapEntries, computedAt = Clock.System.now(), ), ), ) // No RepoKnowledgeRetrievedEvent recorded — the knowledge projection yields null. val recorded = DefaultEventReplayer(store, RepoKnowledgeProjection(stageId)).rebuild(sessionId) assertEquals(null, recorded, "no retrieval event should yield null") // Legacy fallback path: read RepoMapComputedEvent and produce a repoMap entry. val repoMap = DefaultEventReplayer(store, RepoMapComputedProjection()).rebuild(sessionId) requireNotNull(repoMap) { "expected RepoMapComputedEvent to be recorded" } val content = buildString { appendLine("## Repo map (top files by recency — read files for detail)") repoMap.entries.sortedByDescending { it.score }.forEach { entry -> val symbols = if (entry.symbols.isEmpty()) "" else ": ${entry.symbols.joinToString(", ")}" appendLine("- ${entry.path}$symbols") } }.trimEnd() // Assert sourceType matches the legacy path constant used in buildRepoMapEntries. assertTrue(content.contains("src/Main.kt"), "repo map content must include indexed paths") assertTrue(content.contains("src/Util.kt"), "repo map content must include indexed paths") // sourceType "repoMap" is asserted by construction — the legacy buildRepoMapEntries // hard-codes sourceType = "repoMap". We verify the event shape drives the fallback branch. assertTrue(content.contains("## Repo map"), "legacy fallback header must be present") } }