import com.correx.apps.server.memory.FakeWorkspaceStateProbe import com.correx.apps.server.memory.L3RepoKnowledgeRetriever import com.correx.apps.server.memory.ProjectMemoryService import com.correx.apps.server.memory.RepoMapIndexerPort import com.correx.apps.server.memory.WorkspaceState import com.correx.core.config.ProjectConfig import com.correx.core.events.events.RepoMapEntry import com.correx.core.events.types.SessionId import com.correx.core.inference.Embedder import com.correx.core.journal.DecisionJournalProjector import com.correx.core.journal.DefaultDecisionJournalReducer import com.correx.core.journal.DefaultDecisionJournalRepository import com.correx.core.router.l3.InMemoryL3MemoryStore import com.correx.core.sessions.projections.replay.DefaultEventReplayer import com.correx.infrastructure.persistence.InMemoryEventStore import kotlinx.coroutines.runBlocking import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test import java.nio.file.Path private class CountingIndexer(private val entries: List) : RepoMapIndexerPort { var callCount = 0 override fun index(repoRoot: Path): List { callCount++ return entries } } /** * Deterministic embedder: encodes each text as a stable float vector using char codepoint sums. * Guarantees non-trivial cosine similarity separation between distinct texts while keeping * retrieval deterministic (no RNG, no network). */ private class DeterministicEmbedder(override val dimension: Int = 8) : Embedder { override suspend fun embed(text: String): FloatArray { val v = FloatArray(dimension) text.forEachIndexed { i, c -> v[i % dimension] += c.code.toFloat() } val norm = kotlin.math.sqrt(v.map { it * it }.sum()).coerceAtLeast(1e-6f) return FloatArray(dimension) { v[it] / norm } } } /** * Integration test: two sessions sharing the same workspace stateKey and L3 store. * * Proves: * 1. Exactly ONE index walk across both sessions (state-keyed reuse via L3 presence check). * 2. The second session's retriever returns non-empty hits from the shared L3 embeddings — * demonstrating that repo context is available without a per-session RepoMapComputedEvent. * * Note: this test depends on apps:server (ProjectMemoryService, L3RepoKnowledgeRetriever, * FakeWorkspaceStateProbe) and core:router (InMemoryL3MemoryStore). These were added to * testing/integration's classpath to allow end-to-end coverage without duplicating the * production classes inline. */ class RepoMapReuseIntegrationTest { private val repoRoot = "/repo-integration" private val stateKey = "git:abc123" private val entries = listOf( RepoMapEntry(path = "src/Parser.kt", score = 1.0, symbols = listOf("Parser", "Token")), RepoMapEntry(path = "src/Lexer.kt", score = 0.8, symbols = listOf("Lexer")), ) @Test fun `same stateKey — exactly one index walk across two sessions`(): Unit = runBlocking { val es = InMemoryEventStore() val l3 = InMemoryL3MemoryStore() val indexer = CountingIndexer(entries) val embedder = DeterministicEmbedder() val probe = FakeWorkspaceStateProbe(WorkspaceState(stateKey, "git", "main", false)) val svc = buildService(es, l3, indexer, embedder, probe) val sessionA = SessionId("integration-reuse-a") val sessionB = SessionId("integration-reuse-b") svc.observeAndRecord(sessionA, repoRoot) svc.indexAndRecord(sessionA, repoRoot) assertEquals(1, indexer.callCount, "first session must trigger index walk") svc.observeAndRecord(sessionB, repoRoot) svc.indexAndRecord(sessionB, repoRoot) assertEquals(1, indexer.callCount, "second session with same stateKey must skip index walk") } @Test fun `second session retrieves repo content from shared L3 embeddings`(): Unit = runBlocking { val es = InMemoryEventStore() val l3 = InMemoryL3MemoryStore() val indexer = CountingIndexer(entries) val embedder = DeterministicEmbedder() val probe = FakeWorkspaceStateProbe(WorkspaceState(stateKey, "git", "main", false)) val svc = buildService(es, l3, indexer, embedder, probe) val sessionA = SessionId("integration-retrieve-a") val sessionB = SessionId("integration-retrieve-b") // Session A: observe + index → entries embedded into shared L3. svc.observeAndRecord(sessionA, repoRoot) svc.indexAndRecord(sessionA, repoRoot) // Session B: observe + skip scan (stateKey reuse). svc.observeAndRecord(sessionB, repoRoot) svc.indexAndRecord(sessionB, repoRoot) // Session B has no per-session RepoMapComputedEvent but retriever reads shared L3. val retriever = L3RepoKnowledgeRetriever( embedder = embedder, l3MemoryStore = l3, repoRoot = repoRoot, ) val hits = retriever.retrieve(sessionB, "parse the input", k = 5) assertTrue(hits.isNotEmpty(), "second session must receive repo content from shared L3 embeddings") val allText = hits.joinToString(" ") { it.text } assertTrue( allText.contains("src/Parser.kt") || allText.contains("src/Lexer.kt"), "retrieved hits must reference indexed files; got: $allText", ) } private fun buildService( es: InMemoryEventStore, l3: InMemoryL3MemoryStore, indexer: CountingIndexer, embedder: Embedder, probe: FakeWorkspaceStateProbe, ) = ProjectMemoryService( config = ProjectConfig(enabled = true, root = repoRoot), embedder = embedder, l3MemoryStore = l3, journalRepository = DefaultDecisionJournalRepository( DefaultEventReplayer(es, DecisionJournalProjector(DefaultDecisionJournalReducer())), ), eventStore = es, indexer = indexer, workspaceStateProbe = probe, ) }