diff --git a/src/main/kotlin/mcp/code/analysis/config/AppConfig.kt b/src/main/kotlin/mcp/code/analysis/config/AppConfig.kt index 1f09a8a..ff71658 100644 --- a/src/main/kotlin/mcp/code/analysis/config/AppConfig.kt +++ b/src/main/kotlin/mcp/code/analysis/config/AppConfig.kt @@ -15,23 +15,6 @@ data class AppConfig( ) { companion object { - /** - * Creates an instance of [AppConfig] by retrieving values from environment variables. If an environment variable is - * not set, a default value is used. - * - * @return An instance of [AppConfig] with the retrieved or default values. - */ - fun fromEnv(): AppConfig = - AppConfig( - serverPort = System.getenv("SERVER_PORT")?.toIntOrNull() ?: 3001, - githubToken = System.getenv("GITHUB_TOKEN") ?: "", - cloneDirectory = System.getenv("CLONE_DIRECTORY") ?: "/tmp/mcp-github-code-analyzer/clones", - logDirectory = System.getenv("LOGS_DIRECTORY") ?: "/tmp/mcp-github-code-analyzer/logs", - modelApiUrl = System.getenv("MODEL_API_URL") ?: "http://localhost:11434/api", - modelApiKey = System.getenv("MODEL_API_KEY") ?: "", - modelName = System.getenv("MODEL_NAME") ?: "llama3.2", - ) - /** * Creates an instance of [AppConfig] by retrieving values from environment variables. If an environment variable is * not set, a default value is used. diff --git a/src/main/kotlin/mcp/code/analysis/server/Server.kt b/src/main/kotlin/mcp/code/analysis/server/Server.kt index 3533460..635ecf2 100644 --- a/src/main/kotlin/mcp/code/analysis/server/Server.kt +++ b/src/main/kotlin/mcp/code/analysis/server/Server.kt @@ -27,7 +27,7 @@ import org.slf4j.LoggerFactory * Server for analyzing GitHub repositories using the Model Context Protocol (MCP). Provides functionalities for * analyzing GitHub repositories and checking analysis status. */ -data class Server( +open class Server( private val repositoryAnalysisService: RepositoryAnalysisService = RepositoryAnalysisService(), private val logger: Logger = LoggerFactory.getLogger(Server::class.java), private val implementation: Implementation = @@ -66,7 +66,7 @@ data class Server( * * @param port The port number on which the SSE MCP server will listen for client connections. */ - fun runSseMcpServerWithPlainConfiguration(port: Int): Unit = runBlocking { + open fun runSseMcpServerWithPlainConfiguration(port: Int): Unit = runBlocking { val servers = ConcurrentMap() logger.info("Starting SSE server on port $port.") logger.info("Use inspector to connect to the http://localhost:$port/sse") @@ -111,7 +111,7 @@ data class Server( * * @param port The port number on which the SSE MCP server will listen for client connections. */ - fun runSseMcpServerUsingKtorPlugin(port: Int): Unit = runBlocking { + open fun runSseMcpServerUsingKtorPlugin(port: Int): Unit = runBlocking { logger.info("Starting SSE server on port $port") logger.info("Use inspector to connect to http://localhost:$port/sse") @@ -123,7 +123,7 @@ data class Server( * * @return The configured MCP server instance. */ - private fun configureServer(): SdkServer { + open fun configureServer(): SdkServer { val server = SdkServer(implementation, serverOptions) server.addTool( diff --git a/src/main/kotlin/mcp/code/analysis/service/CodeAnalyzer.kt b/src/main/kotlin/mcp/code/analysis/service/CodeAnalyzer.kt index f3454f2..cf15373 100644 --- a/src/main/kotlin/mcp/code/analysis/service/CodeAnalyzer.kt +++ b/src/main/kotlin/mcp/code/analysis/service/CodeAnalyzer.kt @@ -37,20 +37,20 @@ data class CodeAnalyzer( val relativePath = file.absolutePath.substring(repoDir.absolutePath.length + 1) val lang = getLanguageFromExtension(file.extension) val content = file.readLines().joinToString("\n") - """--- - |File: $relativePath - |~~~$lang - |$content - |~~~""" - .trimIndent() + """|--- + |File: $relativePath + |~~~$lang + |$content + |~~~""" + .trimMargin() } .toList() .also { snippets -> logger.info("Collected ${snippets.size} code snippets from ${repoDir.absolutePath}") logger.debug( - """Snippets Found: - |${snippets.joinToString("\n")}""" - .trimIndent() + """|Snippets Found: + |${snippets.joinToString("\n")}""" + .trimMargin() ) } diff --git a/src/main/kotlin/mcp/code/analysis/service/ModelContextService.kt b/src/main/kotlin/mcp/code/analysis/service/ModelContextService.kt index d92859b..a9915ab 100644 --- a/src/main/kotlin/mcp/code/analysis/service/ModelContextService.kt +++ b/src/main/kotlin/mcp/code/analysis/service/ModelContextService.kt @@ -46,9 +46,9 @@ data class ModelContextService( suspend fun generateResponse(prompt: String): String { return try { logger.info( - """Sending request to Ollama with prompt: - |${prompt}...""" - .trimIndent() + """|Sending request to Ollama with prompt: + |${prompt}...""" + .trimMargin() ) val request = OllamaRequest(model = config.modelName, prompt = prompt) val ollamaApiUrl = "${config.modelApiUrl}/generate" @@ -61,9 +61,9 @@ data class ModelContextService( } else { val response = httpResponse.body() logger.info( - """Received response from Ollama: - |${response.response}""" - .trimIndent() + """|Received response from Ollama: + |${response.response}""" + .trimMargin() ) response.response ?: "No response generated" } @@ -80,66 +80,60 @@ data class ModelContextService( * @return A structured prompt for the model */ fun buildInsightsPrompt(readme: String) = - """ - |You are an expert codebase analyst with deep expertise in software architecture, secure and scalable system design, and programming languages including Java, Kotlin, Python, Go, Scala, JavaScript, TypeScript, C++, Rust, Ruby, and others. - | - |You will be provided with the README file of a repository. Based on the README **alone**, provide a comprehensive analysis covering the following aspects: - | - |1. **Overall architecture** — inferred from descriptions, diagrams, setup steps, or configuration details. - |2. **Primary programming languages** — identify the main languages used and describe how they interact if applicable. - |3. **Key components and dependencies** — identify modules, services, tools, or third-party integrations and their relationships. - |4. **Design patterns** — mention any explicitly referenced or implicitly suggested architectural or code patterns. - |5. **Code quality signals** — identify any potential issues or areas of improvement (e.g., based on structure, naming, tooling). - |6. **Security considerations** — highlight any security best practices followed or missing (e.g., credential handling, auth mechanisms). - |7. **Performance considerations** — discuss caching, concurrency, resource management, or deployment implications. - |8. **Language-specific practices** — note idiomatic usage or violations of best practices for the identified languages. - | - |If any of the above are not explicitly described, provide clearly labeled **inferences** based on available information. - | - |Format your response in markdown using clear sections. Include direct references to specific README content where relevant. If multiple languages are involved, explain any cross-language integration points. - | - |README Content: - |~~~markdown - |${readme.replace("```","~~~")} - |~~~""" - .trimIndent() + """|You are an expert codebase analyst with deep expertise in software architecture, secure and scalable system design, and programming languages including Java, Kotlin, Python, Go, Scala, JavaScript, TypeScript, C++, Rust, Ruby, and others. + | + |You will be provided with the README file of a repository. Based on the README **alone**, provide a comprehensive analysis covering the following aspects: + | + |1. **Overall architecture** — inferred from descriptions, diagrams, setup steps, or configuration details. + |2. **Primary programming languages** — identify the main languages used and describe how they interact if applicable. + |3. **Key components and dependencies** — identify modules, services, tools, or third-party integrations and their relationships. + |4. **Design patterns** — mention any explicitly referenced or implicitly suggested architectural or code patterns. + |5. **Code quality signals** — identify any potential issues or areas of improvement (e.g., based on structure, naming, tooling). + |6. **Security considerations** — highlight any security best practices followed or missing (e.g., credential handling, auth mechanisms). + |7. **Performance considerations** — discuss caching, concurrency, resource management, or deployment implications. + |8. **Language-specific practices** — note idiomatic usage or violations of best practices for the identified languages. + | + |If any of the above are not explicitly described, provide clearly labeled **inferences** based on available information. + | + |Format your response in markdown using clear sections. Include direct references to specific README content where relevant. If multiple languages are involved, explain any cross-language integration points. + | + |README Content: + |~~~markdown + |${readme.replace("```","~~~")} + |~~~""" + .trimMargin() /** * Build a summary prompt for the model context based on the provided code structure and snippets. * * @param codeStructure Map representing the structure of the codebase * @param codeSnippets List of code snippets from the repository - * @param insights List of insights generated from the README analysis * @return A structured prompt for the model */ - fun buildSummaryPrompt(codeStructure: Map, codeSnippets: List, insights: String): String = - """ - |You are analyzing a software code repository. You are provided with the following information: - | - |**Code Structure:** - |${codeStructure.entries.joinToString("\n") { "${it.key}: ${it.value}" }} - | - |**Code Snippets:** - |${codeSnippets.joinToString("\n\n")} - | - |**Key Insights:** - |$insights - | - |Using this information, write a comprehensive and accessible summary of the codebase. Your goal is to help a technically proficient developer who is new to the project quickly understand its structure and purpose. - | - |Your summary must cover the following aspects: - | - |1. **Main purpose** of the project - |2. **Core architecture and components** - |3. **Technologies and programming languages** used - |4. **Key functionality and workflows** - |5. **Potential areas for improvement or refactoring** - | - |Where helpful, include **brief illustrative code snippets** from the examples provided to clarify key concepts, architectural decisions, or coding patterns. - | - |Format your response using markdown with clear section headings and concise, informative language. Avoid speculation beyond the provided inputs unless clearly stated as inference. - |""" - .trimIndent() + fun buildSummaryPrompt(codeStructure: Map, codeSnippets: List): String = + """|You are analyzing a software code repository. You are provided with the following information: + | + |**Code Structure:** + |${codeStructure.entries.joinToString("\n") { "${it.key}: ${it.value}" }} + | + |**Code Snippets:** + |${codeSnippets.joinToString("\n\n")} + | + |Using this information, write a comprehensive and accessible summary of the codebase. Your goal is to help a technically proficient developer who is new to the project quickly understand its structure and purpose. + | + |Your summary must cover the following aspects: + | + |1. **Main purpose** of the project + |2. **Core architecture and components** + |3. **Technologies and programming languages** used + |4. **Key functionality and workflows** + |5. **Potential areas for improvement or refactoring** + | + |Where helpful, include **brief illustrative code snippets** from the examples provided to clarify key concepts, architectural decisions, or coding patterns. + | + |Format your response using markdown with clear section headings and concise, informative language. Avoid speculation beyond the provided inputs unless clearly stated as inference. + |""" + .trimMargin() private suspend fun sendRequest(url: String, request: OllamaRequest): HttpResponse { return httpClient.post(url) { diff --git a/src/main/kotlin/mcp/code/analysis/service/RepositoryAnalysisService.kt b/src/main/kotlin/mcp/code/analysis/service/RepositoryAnalysisService.kt index f54a818..39da3b9 100644 --- a/src/main/kotlin/mcp/code/analysis/service/RepositoryAnalysisService.kt +++ b/src/main/kotlin/mcp/code/analysis/service/RepositoryAnalysisService.kt @@ -1,5 +1,7 @@ package mcp.code.analysis.service +import kotlinx.coroutines.* + /** Service for analyzing Git repositories. */ data class RepositoryAnalysisService( private val gitService: GitService = GitService(), @@ -13,7 +15,7 @@ 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 { + fun analyzeRepository(repoUrl: String, branch: String): String { return try { val repoDir = gitService.cloneRepository(repoUrl, branch) @@ -22,10 +24,17 @@ data class RepositoryAnalysisService( val codeSnippets = codeAnalyzer.collectAllCodeSnippets(repoDir) val insightsPrompt = modelContextService.buildInsightsPrompt(readmeContent) - val insightsResponse = modelContextService.generateResponse(insightsPrompt) + val summaryPrompt = modelContextService.buildSummaryPrompt(codeStructure, codeSnippets) - val summaryPrompt = modelContextService.buildSummaryPrompt(codeStructure, codeSnippets, insightsResponse) - modelContextService.generateResponse(summaryPrompt) + runBlocking { + val insights = async { modelContextService.generateResponse(insightsPrompt) } + val summary = async { modelContextService.generateResponse(summaryPrompt) } + """|${insights.await()} + | + |${summary.await()} + |""" + .trimMargin() + } } catch (e: Exception) { throw Exception("Error analyzing repository: ${e.message}", e) } diff --git a/src/test/kotlin/mcp/code/analysis/server/ServerTest.kt b/src/test/kotlin/mcp/code/analysis/server/ServerTest.kt index 910d4ca..bdc476c 100644 --- a/src/test/kotlin/mcp/code/analysis/server/ServerTest.kt +++ b/src/test/kotlin/mcp/code/analysis/server/ServerTest.kt @@ -1,16 +1,24 @@ package mcp.code.analysis.server +import io.ktor.http.* +import io.ktor.server.application.* +import io.ktor.server.cio.* +import io.ktor.server.engine.* +import io.ktor.server.response.* +import io.ktor.server.routing.* +import io.ktor.server.sse.* +import io.ktor.util.collections.* import io.mockk.* import io.modelcontextprotocol.kotlin.sdk.* +import io.modelcontextprotocol.kotlin.sdk.server.* import io.modelcontextprotocol.kotlin.sdk.server.Server as SdkServer -import io.modelcontextprotocol.kotlin.sdk.server.StdioServerTransport +import kotlin.invoke import kotlin.test.assertEquals import kotlinx.coroutines.runBlocking import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonPrimitive import mcp.code.analysis.service.RepositoryAnalysisService import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test import org.slf4j.Logger @@ -44,20 +52,17 @@ class ServerTest { } @Test - @Disabled("Temporarily disabled") fun `configureServer should register analyze-repository tool`() { // Arrange mockkConstructor(SdkServer::class) - val mockSdkServer = mockk(relaxed = true) - every { anyConstructed() } returns mockSdkServer - every { mockSdkServer.addTool(any(), any(), any(), any()) } returns mockk() + every { anyConstructed().addTool(any(), any(), any(), any()) } returns mockk() val configureServerMethod = server.javaClass.getDeclaredMethod("configureServer") configureServerMethod.isAccessible = true configureServerMethod.invoke(server) - // Act & Assert - verify { mockSdkServer.addTool(any(), any(), any(), any()) } + // Assert + verify { anyConstructed().addTool(any(), any(), any(), any()) } } @Test @@ -91,23 +96,84 @@ class ServerTest { } @Test - @Disabled("Temporarily disabled") fun `runMcpServerUsingStdio should connect transport`() = runBlocking { // Arrange + val mockSdkServer = mockk(relaxed = true) + mockkConstructor(StdioServerTransport::class) - val mockTransport = mockk(relaxed = true) - every { anyConstructed() } returns mockTransport - mockkConstructor(SdkServer::class) + coEvery { mockSdkServer.connect(any()) } just Runs + coEvery { mockSdkServer.onClose(any()) } answers { firstArg<() -> Unit>().invoke() } + + // Act + val testServer = TestableServer(repositoryAnalysisService, logger, mockSdkServer) + testServer.runMcpServerUsingStdio() + + // Assert + coVerify { mockSdkServer.connect(any()) } + } + + @Test + fun `runSseMcpServerWithPlainConfiguration should start server`() { + // Arrange + val mockSdkServer = mockk(relaxed = true) + val mockServer = mockk>(relaxed = true) + + mockkStatic("io.ktor.server.engine.EmbeddedServerKt") + every { embeddedServer(CIO, host = "0.0.0.0", port = 3001, module = any()) } returns mockServer + every { mockServer.start(wait = true) } returns mockServer + + coEvery { mockSdkServer.connect(any()) } just Runs + coEvery { mockSdkServer.onClose(any()) } answers { firstArg<() -> Unit>().invoke() } + + val testServer = spyk(TestableServer(repositoryAnalysisService, logger, mockSdkServer), recordPrivateCalls = true) + + // Act + testServer.runSseMcpServerWithPlainConfiguration(3001) + + // Assert + verify { embeddedServer(CIO, host = "0.0.0.0", port = 3001, module = any()) } + } + + @Test + fun `runSseMcpServerUsingKtorPlugin should start server`() { + // Arrange val mockSdkServer = mockk(relaxed = true) - every { anyConstructed() } returns mockSdkServer + val mockServer = mockk>(relaxed = true) + + mockkStatic("io.ktor.server.engine.EmbeddedServerKt") + every { embeddedServer(CIO, host = "0.0.0.0", port = 3001, module = any()) } returns mockServer + every { mockServer.start(wait = true) } returns mockServer + coEvery { mockSdkServer.connect(any()) } just Runs coEvery { mockSdkServer.onClose(any()) } answers { firstArg<() -> Unit>().invoke() } + val testServer = spyk(TestableServer(repositoryAnalysisService, logger, mockSdkServer), recordPrivateCalls = true) + // Act - server.runMcpServerUsingStdio() + testServer.runSseMcpServerUsingKtorPlugin(3001) // Assert - coVerify { mockSdkServer.connect(mockTransport) } + verify { embeddedServer(CIO, host = "0.0.0.0", port = 3001, module = any()) } + } + + class TestableServer( + repositoryAnalysisService: RepositoryAnalysisService, + logger: Logger, + private val sdkServer: SdkServer, + ) : Server(repositoryAnalysisService, logger) { + override fun configureServer(): SdkServer = sdkServer + + override fun runSseMcpServerUsingKtorPlugin(port: Int) { + embeddedServer(CIO, host = "0.0.0.0", port = port) { mcp { configureServer() } }.start(wait = false) + } + + override fun runSseMcpServerWithPlainConfiguration(port: Int) { + embeddedServer(CIO, host = "0.0.0.0", port = port) { + install(SSE) + routing {} + } + .start(wait = false) + } } } diff --git a/src/test/kotlin/mcp/code/analysis/service/CodeAnalyzerTest.kt b/src/test/kotlin/mcp/code/analysis/service/CodeAnalyzerTest.kt index da69ccf..0529f4a 100644 --- a/src/test/kotlin/mcp/code/analysis/service/CodeAnalyzerTest.kt +++ b/src/test/kotlin/mcp/code/analysis/service/CodeAnalyzerTest.kt @@ -43,12 +43,12 @@ class CodeAnalyzerTest { files = mapOf( "README.md" to - """# Project + """|# Project |This is a test project""" .trimMargin() ), expectedContent = - """# Project + """|# Project |This is a test project""" .trimMargin(), containsLogMessage = "Readme file found", diff --git a/src/test/kotlin/mcp/code/analysis/service/ModelContextServiceTest.kt b/src/test/kotlin/mcp/code/analysis/service/ModelContextServiceTest.kt index a73a14a..69ea0f6 100644 --- a/src/test/kotlin/mcp/code/analysis/service/ModelContextServiceTest.kt +++ b/src/test/kotlin/mcp/code/analysis/service/ModelContextServiceTest.kt @@ -41,7 +41,6 @@ class ModelContextServiceTest { val name: String, val codeStructure: Map, val codeSnippets: List, - val insights: String, val expectedPromptContains: List, ) @@ -90,16 +89,15 @@ class ModelContextServiceTest { InsightsPromptTestCase( name = "standard README with code blocks", readmeContent = - """ - # Test Project - - This is a test project with ```code blocks``` - - ## Features - - Feature 1 - - Feature 2 - """ - .trimIndent(), + """|# Test Project + | + |This is a test project with ```code blocks``` + | + |## Features + |- Feature 1 + |- Feature 2 + |""" + .trimMargin(), expectedContains = listOf( "README Content:", @@ -118,18 +116,16 @@ class ModelContextServiceTest { InsightsPromptTestCase( name = "README with multiple code blocks", readmeContent = - """ - # Code Examples - ```kotlin - fun test() {} - ``` - - And more code: - ```java - public void test() {} - ``` - """ - .trimIndent(), + """|# Code Examples + |```kotlin + |fun test() {} + |``` + | + |And more code: + |```java + |public void test() {} + |```""" + .trimMargin(), expectedContains = listOf("~~~kotlin", "~~~java", "fun test() {}"), shouldNotContain = listOf("```"), ), @@ -214,18 +210,13 @@ class ModelContextServiceTest { codeStructure = mapOf("main.kt" to mapOf("language" to "kotlin")), codeSnippets = listOf( - """File: main.kt - |~~~kotlin - |fun main() {} - |~~~""" + """|File: main.kt + |~~~kotlin + |fun main() {} + |~~~""" .trimMargin() ), - insights = - """Overall architecture: Simple - |Language: Kotlin""" - .trimMargin(), - expectedPromptContains = - listOf("Code Snippets:", "fun main() {}", "Key Insights:", "Overall architecture: Simple"), + expectedPromptContains = listOf("Code Snippets:", "fun main() {}"), ), SummaryTestCase( name = "complex project", @@ -235,52 +226,37 @@ class ModelContextServiceTest { ), codeSnippets = listOf( - """File: src/main.kt - |~~~kotlin - |fun main() {} - |~~~""" + """|File: src/main.kt + |~~~kotlin + |fun main() {} + |~~~""" .trimMargin(), - """File: src/util.kt - |~~~kotlin - |fun util() {} - |~~~""" + """|File: src/util.kt + |~~~kotlin + |fun util() {} + |~~~""" .trimMargin(), ), - insights = - """Overall architecture: Modular - |Primary languages: Kotlin - |Design patterns: Singleton""" - .trimMargin(), - expectedPromptContains = - listOf( - "Code Snippets:", - "fun main() {}", - "fun util() {}", - "Key Insights:", - "Overall architecture: Modular", - "Design patterns: Singleton", - ), + expectedPromptContains = listOf("Code Snippets:", "fun main() {}", "fun util() {}"), ), SummaryTestCase( name = "empty project", codeStructure = emptyMap(), codeSnippets = emptyList(), - insights = "", - expectedPromptContains = listOf("Code Snippets:", "Key Insights:"), + expectedPromptContains = listOf("Code Snippets:"), ), ) testCases.forEach { testCase -> val mockService = mockk() coEvery { mockService.generateResponse(any()) } returns "Mocked summary response" - coEvery { mockService.buildSummaryPrompt(any(), any(), any()) } coAnswers + coEvery { mockService.buildSummaryPrompt(any(), any()) } coAnswers { val codeStructure = firstArg>() val codeSnippets = secondArg>() - val insights = thirdArg() // Act - val prompt = service.buildSummaryPrompt(codeStructure, codeSnippets, insights) + val prompt = service.buildSummaryPrompt(codeStructure, codeSnippets) // Assert testCase.expectedPromptContains.forEach { expected -> @@ -291,11 +267,11 @@ class ModelContextServiceTest { } // Act - val summary = mockService.buildSummaryPrompt(testCase.codeStructure, testCase.codeSnippets, testCase.insights) + val summary = mockService.buildSummaryPrompt(testCase.codeStructure, testCase.codeSnippets) // Assert assertEquals("Mocked summary response", summary) - coVerify { mockService.buildSummaryPrompt(testCase.codeStructure, testCase.codeSnippets, testCase.insights) } + coVerify { mockService.buildSummaryPrompt(testCase.codeStructure, testCase.codeSnippets) } } } } diff --git a/src/test/kotlin/mcp/code/analysis/service/RepositoryAnalysisServiceTest.kt b/src/test/kotlin/mcp/code/analysis/service/RepositoryAnalysisServiceTest.kt index adcce04..42c1c9a 100644 --- a/src/test/kotlin/mcp/code/analysis/service/RepositoryAnalysisServiceTest.kt +++ b/src/test/kotlin/mcp/code/analysis/service/RepositoryAnalysisServiceTest.kt @@ -6,8 +6,10 @@ import io.mockk.mockk import io.mockk.verify import java.io.File import java.lang.Exception +import kotlin.test.assertEquals import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows @@ -44,30 +46,33 @@ class RepositoryAnalysisServiceTest { val readmeContent = "# Test Repository" val codeSnippets = listOf( - """--- - |File: src/Main.kt - |~~~kotlin - |fun main() {} - |~~~""" - .trimIndent() + """|--- + |File: src/Main.kt + |~~~kotlin + |fun main() {} + |~~~""" + .trimMargin() ) - val modelResponse = "Repository Analysis: This is a simple Kotlin project" val insightsPrompt = "insights prompt" val summaryPrompt = "summary prompt" + val insightsResponse = "insights" + val summaryResponse = "summary" every { gitService.cloneRepository(repoUrl, branch) } returns clonedRepo every { codeAnalyzer.findReadmeFile(clonedRepo) } returns readmeContent every { modelContextService.buildInsightsPrompt(readmeContent) } returns insightsPrompt - every { modelContextService.buildSummaryPrompt(any(), any(), any()) } returns summaryPrompt + every { modelContextService.buildSummaryPrompt(any(), any()) } returns summaryPrompt every { codeAnalyzer.analyzeStructure(clonedRepo) } returns emptyMap() every { codeAnalyzer.collectAllCodeSnippets(clonedRepo) } returns codeSnippets - coEvery { modelContextService.generateResponse(any()) } returns modelResponse + coEvery { modelContextService.generateResponse(insightsPrompt) } returns insightsResponse + coEvery { modelContextService.generateResponse(summaryPrompt) } returns summaryResponse // Act val result = repositoryAnalysisService.analyzeRepository(repoUrl, branch) // Assert - assert(result == modelResponse) + assertTrue(result.contains(insightsResponse)) + assertTrue(result.contains(summaryResponse)) verify { gitService.cloneRepository(repoUrl, branch) } verify { codeAnalyzer.findReadmeFile(clonedRepo) } verify { codeAnalyzer.collectAllCodeSnippets(clonedRepo) } @@ -113,12 +118,12 @@ class RepositoryAnalysisServiceTest { val readmeContent = "# Test Repository" val codeSnippets = listOf( - """--- - |File: src/Main.kt - |~~~kotlin - |fun main() {} - |~~~""" - .trimIndent() + """|--- + |File: src/Main.kt + |~~~kotlin + |fun main() {} + |~~~""" + .trimMargin() ) every { gitService.cloneRepository(repoUrl, branch) } returns clonedRepo @@ -150,14 +155,21 @@ class RepositoryAnalysisServiceTest { every { codeAnalyzer.analyzeStructure(clonedRepo) } returns emptyMap() every { codeAnalyzer.collectAllCodeSnippets(clonedRepo) } returns emptySnippets every { modelContextService.buildInsightsPrompt(readmeContent) } returns insightsPrompt - every { modelContextService.buildSummaryPrompt(any(), any(), any()) } returns summaryPrompt + every { modelContextService.buildSummaryPrompt(any(), any()) } returns summaryPrompt coEvery { modelContextService.generateResponse(any()) } returns modelResponse // Act val result = repositoryAnalysisService.analyzeRepository(repoUrl, branch) // Assert - assert(result == modelResponse) + assertEquals( + """|$modelResponse + | + |$modelResponse + |""" + .trimMargin(), + result, + ) verify { codeAnalyzer.collectAllCodeSnippets(clonedRepo) } verify { runBlocking { modelContextService.generateResponse("Insights prompt") } @@ -174,12 +186,12 @@ class RepositoryAnalysisServiceTest { val noReadme = "No README content available." val codeSnippets = listOf( - """"--- - |File: src/Main.kt - |~~~kotlin - |fun main() {} - |~~~""" - .trimIndent() + """"|--- + |File: src/Main.kt + |~~~kotlin + |fun main() {} + |~~~""" + .trimMargin() ) val modelResponse = "Repository Analysis: Kotlin project without README" val insightsPrompt = "No README content available" @@ -190,14 +202,21 @@ class RepositoryAnalysisServiceTest { every { codeAnalyzer.analyzeStructure(clonedRepo) } returns emptyMap() every { codeAnalyzer.collectAllCodeSnippets(clonedRepo) } returns codeSnippets every { modelContextService.buildInsightsPrompt(noReadme) } returns insightsPrompt - every { modelContextService.buildSummaryPrompt(any(), any(), any()) } returns summaryPrompt + every { modelContextService.buildSummaryPrompt(any(), any()) } returns summaryPrompt coEvery { modelContextService.generateResponse(any()) } returns modelResponse // Act val result = repositoryAnalysisService.analyzeRepository(repoUrl, branch) // Assert - assert(result == modelResponse) + assertEquals( + """|$modelResponse + | + |$modelResponse + |""" + .trimMargin(), + result, + ) verify { codeAnalyzer.findReadmeFile(clonedRepo) } verify { runBlocking {