diff --git a/src/main/kotlin/mcp/code/analysis/processor/CodeAnalyzer.kt b/src/main/kotlin/mcp/code/analysis/processor/CodeAnalyzer.kt index 3982efc..17dd711 100644 --- a/src/main/kotlin/mcp/code/analysis/processor/CodeAnalyzer.kt +++ b/src/main/kotlin/mcp/code/analysis/processor/CodeAnalyzer.kt @@ -1,7 +1,6 @@ package mcp.code.analysis.processor import java.io.File -import kotlin.text.lines import mcp.code.analysis.service.ModelContextService import org.slf4j.Logger import org.slf4j.LoggerFactory @@ -13,8 +12,6 @@ data class LanguagePatterns( val blockCommentEnd: String, ) -data class ProcessingState(val lines: List = emptyList(), val inCommentBlock: Boolean = false) - /** * Responsible for analyzing the structure of a codebase. Identifies files, directories, and their respective metadata * such as size, language, imports, and declarations. @@ -126,15 +123,14 @@ data class CodeAnalyzer( findCodeFiles(repoDir) .filter { it.isRelevantCodeFile() && !it.isTestFile() } .mapNotNull { file -> - try { - val relativePath = file.getRelativePathFrom(repoDir) - val language = getLanguageFromExtension(file.extension) - val content = file.readText() - summarizeCodeContent(relativePath, language, content, maxLines) - } catch (e: Exception) { - logger.warn("Error processing file ${file.absolutePath}: ${e.message}") - null - } + runCatching { + val relativePath = file.getRelativePathFrom(repoDir) + val language = getLanguageFromExtension(file.extension) + val content = file.readText() + summarizeCodeContent(relativePath, language, content, maxLines) + } + .onFailure { e -> logger.warn("Error processing file ${file.absolutePath}: ${e.message}") } + .getOrNull() } .also { logCollectionResults(it, repoDir) } diff --git a/src/main/kotlin/mcp/code/analysis/processor/CodeContentProcessor.kt b/src/main/kotlin/mcp/code/analysis/processor/CodeContentProcessor.kt index 2c85f8e..79462fe 100644 --- a/src/main/kotlin/mcp/code/analysis/processor/CodeContentProcessor.kt +++ b/src/main/kotlin/mcp/code/analysis/processor/CodeContentProcessor.kt @@ -13,32 +13,67 @@ internal class CodeContentProcessor(private val patterns: LanguagePatterns, priv * Processes a list of code lines and returns those that should be included in the summary. Lines are included if they * are definitions, comments, or part of a block comment. * + * The processing follows a two-pass approach: + * 1. First pass: Decides which original lines should be included, tracking comment block state + * 2. Second pass: Builds output with explicit separators between non-contiguous regions + * + * During the first pass, lines are included if they are definitions, comments, or part of a block comment. The + * comment block state is tracked to ensure multi-line comments are properly captured. + * + * During the second pass, separators ("...") are added between non-contiguous regions to indicate omitted code + * sections. The maxLines limit is strictly respected, prioritizing code lines over separators. + * * @param lines The lines of code to process. - * @return A list of lines selected for summarization. + * @return A list of lines selected for summarization, with separators indicating omitted sections. */ fun processContent(lines: List): List { - val finalState = - lines.fold(ProcessingState()) { state, line -> - if (state.lines.size >= maxLines) return@fold state + if (lines.isEmpty()) return emptyList() + + // First pass: compute inclusion flags functionally while tracking the comment block state + data class Pass1(val flags: MutableList, val inBlock: Boolean) + val pass1 = + lines.foldIndexed(Pass1(mutableListOf(), false)) { idx, acc, line -> val trimmed = line.trim() - val nextInCommentBlock = determineCommentBlockState(trimmed, state.inCommentBlock) - val shouldIncludeLine = isDefinition(line) || isCommentLine(line) || state.inCommentBlock - - val updatedLines = - if (shouldIncludeLine) { - when { - isDefinition(line) -> state.lines + processDefinitionLine(line) - else -> state.lines + line - } - } else { - state.lines - } - - ProcessingState(updatedLines, nextInCommentBlock) + val shouldInclude = isDefinition(line) || isCommentLine(line) || acc.inBlock + acc.flags.add(shouldInclude) + val nextInCommentBlock = determineCommentBlockState(trimmed, acc.inBlock) + acc.copy(flags = acc.flags, inBlock = nextInCommentBlock) + } + + val includeFlags: List = pass1.flags + + // Second pass: build output with separators between non-contiguous regions + // Accumulates second-pass output and the index of the last included source line + data class OutputAcc(val result: MutableList, val lastIdx: Int) + + fun maybeAddSeparatorFn(state: OutputAcc, nextIndex: Int): OutputAcc { + if (state.result.isEmpty()) return state + val isGap = nextIndex != state.lastIdx + 1 + if (!isGap) return state + if (state.result.size + 2 > maxLines) return state + state.result.add("...") + return state + } + + val finalAcc: OutputAcc = + lines.indices.fold(OutputAcc(mutableListOf(), -2)) { acc, i -> + if (!includeFlags[i]) return@fold acc + + val afterSep = maybeAddSeparatorFn(acc, i) + + val line = lines[i] + val toAdd = if (isDefinition(line)) processDefinitionLine(line) else line + + if (afterSep.result.size >= maxLines) return@fold afterSep + + afterSep.result.add(toAdd) + val updated = afterSep.copy(result = afterSep.result, lastIdx = i) + + if (updated.result.size >= maxLines) updated else updated } - return finalState.lines + return finalAcc.result } private fun isDefinition(line: String): Boolean = patterns.definitionPattern.containsMatchIn(line.trim()) diff --git a/src/main/kotlin/mcp/code/analysis/server/Mcp.kt b/src/main/kotlin/mcp/code/analysis/server/Mcp.kt index f913bcc..c610baf 100644 --- a/src/main/kotlin/mcp/code/analysis/server/Mcp.kt +++ b/src/main/kotlin/mcp/code/analysis/server/Mcp.kt @@ -7,7 +7,6 @@ import io.ktor.server.engine.* import io.ktor.server.response.* import io.ktor.server.routing.* import io.ktor.server.sse.* -import io.ktor.sse.* import io.ktor.util.collections.* import io.modelcontextprotocol.kotlin.sdk.* import io.modelcontextprotocol.kotlin.sdk.server.Server as SdkServer @@ -15,9 +14,10 @@ import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions import io.modelcontextprotocol.kotlin.sdk.server.SseServerTransport import io.modelcontextprotocol.kotlin.sdk.server.StdioServerTransport import io.modelcontextprotocol.kotlin.sdk.server.mcp -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.* -import kotlinx.io.IOException +import kotlinx.coroutines.Job +import kotlinx.coroutines.awaitCancellation +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withTimeout import kotlinx.io.asSink import kotlinx.io.asSource import kotlinx.io.buffered @@ -85,59 +85,50 @@ class Mcp( val server = configureServer() servers[transport.sessionId] = server - val heartbeatJob = launch { - flow { - while (true) { - emit(Unit) - delay(15_000) - } - } - .onEach { send(ServerSentEvent(event = "heartbeat")) } - .catch { e -> - when (e) { - is IOException -> logger.debug("Client disconnected during heartbeat: ${e.message}") - else -> logger.error("Heartbeat error: ${e.message}", e) - } - } - .onCompletion { logger.debug("Heartbeat job terminated for session: ${transport.sessionId}") } - .collect() - } - server.onClose { - logger.info("Server closed") + logger.info("Server closed for session: ${transport.sessionId}") servers.remove(transport.sessionId) } - server.connect(transport) - try { + server.connect(transport) + logger.info("Server connected for session: ${transport.sessionId}") awaitCancellation() + } catch (e: Exception) { + logger.error("Connection error: ${e.message}", e) } finally { - heartbeatJob.cancel() + servers.remove(transport.sessionId) logger.info("SSE connection closed for session: ${transport.sessionId}") } } post("/message") { try { - val sessionId = call.request.queryParameters["sessionId"] ?: return@post call.respond(HttpStatusCode.BadRequest, "Missing sessionId parameter") - val transport = servers[sessionId]?.transport as? SseServerTransport - if (transport == null) { + val server = servers[sessionId] + if (server == null) { + logger.warn("Session not found: $sessionId") call.respond(HttpStatusCode.NotFound, "Session not found") return@post } + val transport = server.transport as? SseServerTransport + if (transport == null) { + logger.warn("Invalid transport for session: $sessionId") + call.respond(HttpStatusCode.InternalServerError, "Invalid transport") + return@post + } + logger.debug("Handling message for session: $sessionId") - transport.handlePostMessage(call) + withTimeout(3_600_000) { transport.handlePostMessage(call) } call.respond(HttpStatusCode.OK) } catch (e: Exception) { logger.error("Error handling message: ${e.message}", e) - call.respond(HttpStatusCode.InternalServerError, "Error handling message: ${e.message}") + call.respond(HttpStatusCode.InternalServerError, "Error: ${e.message}") } } } diff --git a/src/main/kotlin/mcp/code/analysis/service/RepositoryAnalysisService.kt b/src/main/kotlin/mcp/code/analysis/service/RepositoryAnalysisService.kt index da65f1e..5b34fe2 100644 --- a/src/main/kotlin/mcp/code/analysis/service/RepositoryAnalysisService.kt +++ b/src/main/kotlin/mcp/code/analysis/service/RepositoryAnalysisService.kt @@ -16,22 +16,16 @@ data class RepositoryAnalysisService( * @param branch The branch of the repository to analyze. * @return A summary of the analysis results. */ - suspend fun analyzeRepository(repoUrl: String, branch: String): String { - return try { - val repoDir = gitService.cloneRepository(repoUrl, branch) - - val readme = codeAnalyzer.findReadmeFile(repoDir) - val codeSnippets = codeAnalyzer.collectSummarizedCodeSnippets(repoDir) - - val insightsPrompt = modelContextService.buildInsightsPrompt(codeSnippets, readme) - val insightsResponse = modelContextService.generateResponse(insightsPrompt) - - val summaryPrompt = modelContextService.buildSummaryPrompt(insightsResponse) - val summaryResponse = modelContextService.generateResponse(summaryPrompt) - - summaryResponse - } catch (e: Exception) { - throw Exception("Error analyzing repository: ${e.message}", e) - } - } + suspend fun analyzeRepository(repoUrl: String, branch: String): String = + runCatching { gitService.cloneRepository(repoUrl, branch) } + .mapCatching { repoDir -> + val readme = codeAnalyzer.findReadmeFile(repoDir) + val codeSnippets = codeAnalyzer.collectSummarizedCodeSnippets(repoDir) + val insightsPrompt = modelContextService.buildInsightsPrompt(codeSnippets, readme) + insightsPrompt + } + .mapCatching { insightsPrompt -> modelContextService.generateResponse(insightsPrompt) } + .mapCatching { insightsResponse -> modelContextService.buildSummaryPrompt(insightsResponse) } + .mapCatching { summaryPrompt -> modelContextService.generateResponse(summaryPrompt) } + .getOrElse { e -> throw Exception("Error analyzing repository: ${e.message}", e) } } diff --git a/src/test/kotlin/mcp/code/analysis/config/AppConfigTest.kt b/src/test/kotlin/mcp/code/analysis/config/AppConfigTest.kt index 149353c..05b2d77 100644 --- a/src/test/kotlin/mcp/code/analysis/config/AppConfigTest.kt +++ b/src/test/kotlin/mcp/code/analysis/config/AppConfigTest.kt @@ -1,6 +1,6 @@ package mcp.code.analysis.config -import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test class AppConfigTest { diff --git a/src/test/kotlin/mcp/code/analysis/processor/CodeContentProcessorPropertyTest.kt b/src/test/kotlin/mcp/code/analysis/processor/CodeContentProcessorPropertyTest.kt new file mode 100644 index 0000000..b2e1526 --- /dev/null +++ b/src/test/kotlin/mcp/code/analysis/processor/CodeContentProcessorPropertyTest.kt @@ -0,0 +1,534 @@ +package mcp.code.analysis.processor + +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.collections.shouldContain +import io.kotest.property.arbitrary.arbitrary +import io.kotest.property.checkAll + +class CodeContentProcessorPropertyTest : + StringSpec({ + // Build the same language patterns used by CodeAnalyzer + fun createLanguagePatterns(): Map = + mapOf( + "kotlin" to + LanguagePatterns( + Regex( + "\\b(class|interface|object|enum\\s+class|data\\s+class|sealed\\s+class|fun|val|var|const|typealias|annotation\\s+class|import|package)\\b" + ), + listOf("//"), + "/*", + "*/", + ), + "scala" to + LanguagePatterns( + Regex( + "\\b(class|object|trait|case\\s+class|case\\s+object|def|val|var|lazy\\s+val|type|implicit|sealed|abstract|override|package\\s+object|import|package)\\b" + ), + listOf("//"), + "/*", + "*/", + ), + "java" to + LanguagePatterns( + Regex( + "\\b(class|interface|enum|@interface|record|public|private|protected|static|abstract|final|synchronized|volatile|native|transient|strictfp|void|import|package)\\b" + ), + listOf("//"), + "/*", + "*/", + ), + "python" to + LanguagePatterns( + Regex("\\b(def|class|async\\s+def)\\b|@\\w+|\\b(import|from)\\b"), + listOf("#"), + "\"\"\"", + "\"\"\"", + ), + "ruby" to + LanguagePatterns( + Regex("\\b(def|class|module|attr_\\w+|require|include|extend)\\b"), + listOf("#"), + "=begin", + "=end", + ), + "javascript" to + LanguagePatterns( + Regex("\\b(function|class|const|let|var|import|export|interface|type|enum|namespace)\\b"), + listOf("//"), + "/*", + "*/", + ), + "typescript" to + LanguagePatterns( + Regex("\\b(function|class|const|let|var|import|export|interface|type|enum|namespace)\\b"), + listOf("//"), + "/*", + "*/", + ), + "go" to + LanguagePatterns( + Regex("\\b(func|type|struct|interface|package|import|var|const)\\b"), + listOf("//"), + "/*", + "*/", + ), + "rust" to + LanguagePatterns( + Regex("\\b(fn|struct|enum|trait|impl|pub|use|mod|const|static|type|async|unsafe)\\b"), + listOf("//"), + "/*", + "*/", + ), + "c" to + LanguagePatterns( + Regex("\\b(struct|enum|typedef|void|int|char|bool|extern|static|class)\\b"), + listOf("//"), + "/*", + "*/", + ), + "cpp" to + LanguagePatterns( + Regex("\\b(class|struct|enum|typedef|namespace|template|void|int|char|bool|auto|extern|static|virtual)\\b"), + listOf("//"), + "/*", + "*/", + ), + "default" to + LanguagePatterns( + Regex("\\b(class|interface|object|enum|fun|def|function|public|private|protected|static)\\b"), + listOf("//", "#"), + "/*", + "*/", + ), + ) + + val langPatterns = createLanguagePatterns() + + // Language generator (same set tested in CodeAnalyzerPropertyTest) + val languageGenerator = arbitrary { + listOf("kotlin", "java", "scala", "python", "ruby", "javascript", "typescript", "go", "c", "cpp", "rust").random() + } + + // Generate code content for different languages (duplicated from CodeAnalyzerPropertyTest) + fun generateCodeForLanguage(language: String): String { + return when (language) { + "kotlin" -> + """ + package test + + // This is a comment + /* Block comment + with multiple lines */ + class TestClass { + fun testMethod() { + // Method comment + val x = 1 + } + } + + object Singleton { + val constant = 42 + } + """ + .trimIndent() + + "java" -> + """ + package test; + + // This is a comment + /* Block comment + with multiple lines */ + public class TestClass { + // Field comment + private int field; + + public void testMethod() { + // Method comment + int x = 1; + } + } + + interface TestInterface { + void testMethod(); + } + """ + .trimIndent() + + "scala" -> + """ + package test + + // This is a comment + /* Block comment + with multiple lines */ + class TestClass { + def testMethod(): Unit = { + // Method comment + val x = 1 + } + } + + object Singleton { + val constant = 42 + } + + trait TestTrait { + def abstractMethod(): Unit + } + """ + .trimIndent() + + "python" -> + """ + # This is a comment + + \"\"\" + Block comment + with multiple lines + \"\"\" + + def test_function(): + # Function comment + x = 1 + + class TestClass: + \"\"\"Class docstring\"\"\" + def __init__(self): + self.value = 42 + + def method(self): + return self.value + """ + .trimIndent() + + "ruby" -> + """ + # This is a comment + + =begin + Block comment + with multiple lines + =end + + def test_method + # Method comment + x = 1 + end + + class TestClass + def initialize + @value = 42 + end + + def method + @value + end + end + + module TestModule + def self.module_method + puts "Hello" + end + end + """ + .trimIndent() + + "javascript", + "typescript" -> + """ + // This is a comment + + /* Block comment + with multiple lines */ + + function testFunction() { + // Function comment + const x = 1; + } + + class TestClass { + constructor() { + this.value = 42; + } + + method() { + return this.value; + } + } + + const arrowFn = () => { + return "Hello"; + }; + """ + .trimIndent() + + "go" -> + """ + package test + + // This is a comment + + /* Block comment + with multiple lines */ + + func testFunction() { + // Function comment + x := 1 + } + + type TestStruct struct { + Value int + } + + func (t TestStruct) Method() int { + return t.Value + } + + type TestInterface interface { + Method() int + } + """ + .trimIndent() + + "c", + "cpp" -> + """ + // This is a comment + + /* Block comment + with multiple lines */ + + void testFunction() { + // Function comment + int x = 1; + } + + struct TestStruct { + int value; + }; + + class TestClass { + public: + TestClass() : value(42) {} + + int method() { + return value; + } + + private: + int value; + }; + """ + .trimIndent() + + "rust" -> + """ + // This is a comment + + /* Block comment + with multiple lines */ + + fn test_function() { + // Function comment + let x = 1; + } + + struct TestStruct { + value: i32, + } + + impl TestStruct { + fn new() -> Self { + TestStruct { value: 42 } + } + + fn method(&self) -> i32 { + self.value + } + } + + trait TestTrait { + fn trait_method(&self) -> i32; + } + """ + .trimIndent() + + else -> + """ + """ + .trimIndent() + } + } + + fun containsSubstring(lines: List, substr: String): Boolean = lines.any { it.contains(substr) } + + "processContent should extract definitions and comments for all languages" { + checkAll(50, languageGenerator) { language -> + val patterns = langPatterns[language] ?: error("Missing patterns for $language") + val processor = CodeContentProcessor(patterns, 100) + val content = generateCodeForLanguage(language) + val result = processor.processContent(content.lines()) + + when (language) { + "kotlin" -> { + assert(containsSubstring(result, "class TestClass")) + assert(containsSubstring(result, "fun testMethod")) + assert(containsSubstring(result, "object Singleton")) + assert(containsSubstring(result, "// This is a comment")) + assert(containsSubstring(result, "/* Block comment")) + } + "java" -> { + assert(containsSubstring(result, "public class TestClass")) + assert(containsSubstring(result, "interface TestInterface")) + assert(containsSubstring(result, "// This is a comment")) + assert(containsSubstring(result, "/* Block comment")) + } + "scala" -> { + assert(containsSubstring(result, "class TestClass")) + assert(containsSubstring(result, "def testMethod")) + assert(containsSubstring(result, "object Singleton")) + assert(containsSubstring(result, "trait TestTrait")) + assert(containsSubstring(result, "// This is a comment")) + } + "python" -> { + assert(containsSubstring(result, "def test_function")) + assert(containsSubstring(result, "class TestClass")) + assert(containsSubstring(result, "# This is a comment")) + } + "ruby" -> { + assert(containsSubstring(result, "def test_method")) + assert(containsSubstring(result, "class TestClass")) + assert(containsSubstring(result, "module TestModule")) + assert(containsSubstring(result, "# This is a comment")) + } + "javascript", + "typescript" -> { + assert(containsSubstring(result, "function testFunction")) + assert(containsSubstring(result, "class TestClass")) + assert(containsSubstring(result, "const arrowFn")) + assert(containsSubstring(result, "// This is a comment")) + } + "go" -> { + assert(containsSubstring(result, "func testFunction")) + assert(containsSubstring(result, "type TestStruct struct")) + assert(containsSubstring(result, "type TestInterface interface")) + assert(containsSubstring(result, "// This is a comment")) + } + "c", + "cpp" -> { + assert(containsSubstring(result, "void testFunction")) + assert(containsSubstring(result, "struct TestStruct")) + assert(containsSubstring(result, "class TestClass")) + assert(containsSubstring(result, "// This is a comment")) + } + "rust" -> { + assert(containsSubstring(result, "fn test_function")) + assert(containsSubstring(result, "struct TestStruct")) + assert(containsSubstring(result, "impl TestStruct")) + assert(containsSubstring(result, "trait TestTrait")) + assert(containsSubstring(result, "// This is a comment")) + } + } + } + } + + "processContent should respect maxLines parameter" { + checkAll(50, languageGenerator) { language -> + val patterns = langPatterns[language] ?: error("Missing patterns for $language") + val processor = CodeContentProcessor(patterns, 10) + val largeContent = generateCodeForLanguage(language).repeat(10) + val result = processor.processContent(largeContent.lines()) + assert(result.size <= 10) { "Result should have at most 10 lines, but had ${'$'}{result.size}" } + } + } + + "processContent should handle empty content" { + checkAll(20, languageGenerator) { language -> + val patterns = langPatterns[language] ?: error("Missing patterns for $language") + val processor = CodeContentProcessor(patterns, 100) + val result = processor.processContent(emptyList()) + assert(result.isEmpty()) { "Expected empty result for empty input" } + } + } + + "processContent should handle content with only comments" { + checkAll(20, languageGenerator) { language -> + val patterns = langPatterns[language] ?: error("Missing patterns for $language") + val processor = CodeContentProcessor(patterns, 100) + val commentOnlyContent = + if (language in listOf("python", "ruby")) "# This is only a comment\n# Another comment line" + else "// This is only a comment\n// Another comment line" + val result = processor.processContent(commentOnlyContent.lines()) + result.shouldContain(commentOnlyContent.lines()[0]) + result.shouldContain(commentOnlyContent.lines()[1]) + } + } + + "processContent should handle content with only definitions" { + checkAll(20, languageGenerator) { language -> + val patterns = langPatterns[language] ?: error("Missing patterns for $language") + val processor = CodeContentProcessor(patterns, 100) + val definitionOnlyContent = + when (language) { + "kotlin" -> "class Test {}\nfun testMethod() {}" + "java" -> "public class Test {}\npublic void testMethod() {}" + "scala" -> "class Test {}\ndef testMethod() {}" + "python" -> "class Test:\n pass\ndef test_function():\n pass" + "ruby" -> "class Test\nend\ndef test_method\nend" + "javascript", + "typescript" -> "class Test {}\nfunction testMethod() {}" + "go" -> "type Test struct {}\nfunc testFunction() {}" + "c", + "cpp" -> "struct Test {};\nvoid testFunction() {}" + "rust" -> "struct Test {}\nfn test_function() {}" + else -> "class Test {}\nfunction testMethod() {}" + } + val result = processor.processContent(definitionOnlyContent.lines()) + when (language) { + "kotlin" -> { + assert(containsSubstring(result, "class Test")) + assert(containsSubstring(result, "fun testMethod")) + } + "java" -> { + assert(containsSubstring(result, "public class Test")) + assert(containsSubstring(result, "public void testMethod")) + } + "scala" -> { + assert(containsSubstring(result, "class Test")) + assert(containsSubstring(result, "def testMethod")) + } + "python" -> { + assert(containsSubstring(result, "class Test")) + assert(containsSubstring(result, "def test_function")) + } + "ruby" -> { + assert(containsSubstring(result, "class Test")) + assert(containsSubstring(result, "def test_method")) + } + "javascript", + "typescript" -> { + assert(containsSubstring(result, "class Test")) + assert(containsSubstring(result, "function testMethod")) + } + "go" -> { + assert(containsSubstring(result, "type Test struct")) + assert(containsSubstring(result, "func testFunction")) + } + "c", + "cpp" -> { + assert(containsSubstring(result, "struct Test")) + assert(containsSubstring(result, "void testFunction")) + } + "rust" -> { + assert(containsSubstring(result, "struct Test")) + assert(containsSubstring(result, "fn test_function")) + } + else -> { + assert(containsSubstring(result, "class Test")) + assert(containsSubstring(result, "function testMethod")) + } + } + } + } + }) diff --git a/src/test/kotlin/mcp/code/analysis/service/ModelContextServiceTest.kt b/src/test/kotlin/mcp/code/analysis/service/ModelContextServiceTest.kt index d80c1f6..5005954 100644 --- a/src/test/kotlin/mcp/code/analysis/service/ModelContextServiceTest.kt +++ b/src/test/kotlin/mcp/code/analysis/service/ModelContextServiceTest.kt @@ -5,7 +5,7 @@ import io.ktor.client.engine.mock.* import io.ktor.client.plugins.contentnegotiation.* import io.ktor.http.* import io.ktor.serialization.kotlinx.json.* -import io.mockk.* +import io.mockk.mockk import kotlinx.coroutines.runBlocking import kotlinx.serialization.json.Json import mcp.code.analysis.config.AppConfig diff --git a/src/test/kotlin/mcp/code/analysis/service/RepositoryAnalysisServiceTest.kt b/src/test/kotlin/mcp/code/analysis/service/RepositoryAnalysisServiceTest.kt index f782ec9..97f4083 100644 --- a/src/test/kotlin/mcp/code/analysis/service/RepositoryAnalysisServiceTest.kt +++ b/src/test/kotlin/mcp/code/analysis/service/RepositoryAnalysisServiceTest.kt @@ -5,7 +5,6 @@ import io.mockk.every import io.mockk.mockk import io.mockk.verify import java.io.File -import java.lang.Exception import kotlin.test.assertEquals import kotlinx.coroutines.test.runTest import mcp.code.analysis.processor.CodeAnalyzer