package cc.unitmesh.devins.service import cc.unitmesh.devins.db.DocumentIndexRecord import cc.unitmesh.devins.db.DocumentIndexRepository import cc.unitmesh.devins.filesystem.ProjectFileSystem import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.test.runTest import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNotNull import kotlin.test.assertNull class DocumentIndexServiceTest { private class MockFileSystem : ProjectFileSystem { val files = mutableMapOf() override fun getProjectPath(): String = "/test/project" override fun readFile(path: String): String? = files[path] override fun readFileAsBytes(path: String): ByteArray? { return files[path]?.encodeToByteArray() } override fun writeFile(path: String, content: String): Boolean { files[path] = content return true } override fun exists(path: String): Boolean = files.containsKey(path) override fun isDirectory(path: String): Boolean = false override fun listFiles(path: String, pattern: String?): List { return files.keys.toList() } override fun searchFiles(pattern: String, maxDepth: Int, maxResults: Int): List { // Simple mock: return all markdown and PDF files return files.keys.filter { it.endsWith(".md") || it.endsWith(".pdf") } } override fun resolvePath(relativePath: String): String = "/test/project/$relativePath" } private class MockRepository : DocumentIndexRepository { val records = mutableMapOf() override fun save(record: DocumentIndexRecord) { records[record.path] = record } override fun get(path: String): DocumentIndexRecord? = records[path] override fun getAll(): List = records.values.toList() override fun delete(path: String) { records.remove(path) } override fun deleteAll() { records.clear() } } @Test fun `should index markdown file`() = runTest { val fs = MockFileSystem() val repo = MockRepository() val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) val service = DocumentIndexService(fs, repo, scope) // Add a test markdown file fs.files["docs/README.md"] = """ # Test Document This is a test document. """.trimIndent() // Index the file service.indexFile("docs/README.md") // Wait a bit for async operation kotlinx.coroutines.delay(100) // Verify index record was created val record = repo.get("docs/README.md") assertNotNull(record, "Index record should be created") assertEquals("docs/README.md", record.path) assertEquals("INDEXED", record.status) assertNull(record.error) } @Test fun `should skip unsupported file format`() = runTest { val fs = MockFileSystem() val repo = MockRepository() val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) val service = DocumentIndexService(fs, repo, scope) // Add an unsupported file fs.files["test.txt"] = "Some text content" // Try to index service.indexFile("test.txt") // Wait a bit kotlinx.coroutines.delay(100) // Verify no record was created (txt is not detected as supported format by default) val record = repo.get("test.txt") assertNull(record, "Should not index unsupported format") } @Test fun `should not re-index unchanged file`() = runTest { val fs = MockFileSystem() val repo = MockRepository() val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) val service = DocumentIndexService(fs, repo, scope) val content = "# Test\n\nContent" fs.files["test.md"] = content // Index first time service.indexFile("test.md") kotlinx.coroutines.delay(100) val firstRecord = repo.get("test.md") assertNotNull(firstRecord) val firstIndexedAt = firstRecord.indexedAt // Wait a bit to ensure timestamp would differ kotlinx.coroutines.delay(50) // Index again with same content service.indexFile("test.md") kotlinx.coroutines.delay(100) val secondRecord = repo.get("test.md") assertNotNull(secondRecord) // The indexedAt should be the same (file was not re-indexed) assertEquals(firstIndexedAt, secondRecord.indexedAt, "File should not be re-indexed if unchanged") } @Test fun `should re-index when file content changes`() = runTest { val fs = MockFileSystem() val repo = MockRepository() val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) val service = DocumentIndexService(fs, repo, scope) fs.files["test.md"] = "# Original" // Index first time service.indexFile("test.md") kotlinx.coroutines.delay(100) val firstRecord = repo.get("test.md") assertNotNull(firstRecord) val firstHash = firstRecord.hash // Change content fs.files["test.md"] = "# Modified\n\nNew content" // Index again service.indexFile("test.md") kotlinx.coroutines.delay(100) val secondRecord = repo.get("test.md") assertNotNull(secondRecord) // Hash should be different assertEquals("test.md", secondRecord.path) assertEquals("INDEXED", secondRecord.status) // Note: Due to the simple hash implementation (hashCode + size), // we can't assert they're different, but the logic should update it } }