diff --git a/apps/cli/src/test/kotlin/dev/diff2test/android/cli/D2tConfigTest.kt b/apps/cli/src/test/kotlin/dev/diff2test/android/cli/D2tConfigTest.kt index 023acc9..d742ea8 100644 --- a/apps/cli/src/test/kotlin/dev/diff2test/android/cli/D2tConfigTest.kt +++ b/apps/cli/src/test/kotlin/dev/diff2test/android/cli/D2tConfigTest.kt @@ -119,6 +119,27 @@ class D2tConfigTest { assertContains(report, "Supported now: yes") } + @Test + fun `defaults anthropic config to native messages protocol`() { + val configFile = Files.createTempFile("d2t-anthropic-config", ".toml") + Files.writeString( + configFile, + """ + [ai] + provider = "anthropic" + """.trimIndent(), + ) + val loadResult = loadConfig(configFile) + val resolved = resolveAiConfiguration(loadResult, mapOf("ANTHROPIC_API_KEY" to "sk-ant")) + + assertEquals(AiProvider.ANTHROPIC, resolved?.provider) + assertEquals(AiProtocol.ANTHROPIC_MESSAGES, resolved?.protocol) + assertEquals("ANTHROPIC_API_KEY", resolved?.apiKeyEnv) + assertEquals("claude-sonnet-4-5", resolved?.model) + assertEquals("https://api.anthropic.com/v1", resolved?.baseUrl) + assertTrue(resolved?.supportedByGenerator == true) + } + @Test fun `doctor report supports native gemini protocol`() { val loadResult = ConfigLoadResult.Loaded( diff --git a/docs/providers/anthropic.md b/docs/providers/anthropic.md new file mode 100644 index 0000000..c123f97 --- /dev/null +++ b/docs/providers/anthropic.md @@ -0,0 +1,37 @@ +# Anthropic Provider + +`d2t` supports the native Anthropic Messages API. + +## Config + +```toml +[ai] +enabled = true +provider = "anthropic" +protocol = "anthropic-messages" +api_key_env = "ANTHROPIC_API_KEY" +model = "claude-sonnet-4-5" +base_url = "https://api.anthropic.com/v1" +connect_timeout_seconds = 30 +request_timeout_seconds = 300 +``` + +## Environment + +```bash +export ANTHROPIC_API_KEY="..." +``` + +## Verify + +```bash +d2t doctor +d2t auto --ai +``` + +## Notes + +- `d2t` calls `POST /v1/messages` +- authentication is sent with `x-api-key` +- `anthropic-version: 2023-06-01` is always attached +- use `--strict-ai` if you want the command to fail instead of falling back when AI generation is rejected diff --git a/modules/test-generator/src/test/kotlin/dev/diff2test/android/testgenerator/OpenAiResponsesTestGeneratorTest.kt b/modules/test-generator/src/test/kotlin/dev/diff2test/android/testgenerator/OpenAiResponsesTestGeneratorTest.kt index f4a26d3..fe5fbc7 100644 --- a/modules/test-generator/src/test/kotlin/dev/diff2test/android/testgenerator/OpenAiResponsesTestGeneratorTest.kt +++ b/modules/test-generator/src/test/kotlin/dev/diff2test/android/testgenerator/OpenAiResponsesTestGeneratorTest.kt @@ -26,9 +26,10 @@ import javax.net.ssl.SSLContext import javax.net.ssl.SSLParameters import javax.net.ssl.SSLSession import kotlin.test.Test -import kotlin.test.assertEquals import kotlin.test.assertContains +import kotlin.test.assertEquals import kotlin.test.assertFailsWith +import kotlin.test.assertNotNull import kotlin.test.assertTrue class OpenAiResponsesTestGeneratorTest { @@ -132,6 +133,28 @@ class OpenAiResponsesTestGeneratorTest { assertEquals(listOf("anthropic"), payload.warnings) } + @Test + fun `anthropic generator sends native headers to messages endpoint`() { + val capture = RequestCapture() + val generator = AnthropicMessagesTestGenerator( + config = AnthropicMessagesConfig( + apiKey = "sk-ant", + model = "claude-sonnet-4-5", + baseUrl = "https://api.anthropic.com/v1", + ), + httpClient = capturingHttpClient(capture, 200, anthropicResponseBody()), + ) + + val bundle = generator.generate(plan(), context(), analysis()) + + assertContains(bundle.files.single().content, "class SignUpViewModelGeneratedTest") + val request = assertNotNull(capture.request) + assertEquals("https://api.anthropic.com/v1/messages", request.uri().toString()) + assertEquals("sk-ant", request.headers().firstValue("x-api-key").orElse(null)) + assertEquals("2023-06-01", request.headers().firstValue("anthropic-version").orElse(null)) + assertEquals("application/json", request.headers().firstValue("Accept").orElse(null)) + } + @Test fun `extracts structured payload from chat completions response body`() { val responseBody = """ @@ -588,7 +611,19 @@ class OpenAiResponsesTestGeneratorTest { ) } + private data class RequestCapture( + var request: HttpRequest? = null, + ) + private fun fakeHttpClient(statusCode: Int, body: String): HttpClient { + return capturingHttpClient(RequestCapture(), statusCode, body) + } + + private fun capturingHttpClient( + capture: RequestCapture, + statusCode: Int, + body: String, + ): HttpClient { return object : HttpClient() { override fun cookieHandler(): Optional = Optional.empty() override fun connectTimeout(): Optional = Optional.empty() @@ -603,8 +638,13 @@ class OpenAiResponsesTestGeneratorTest { request: HttpRequest?, responseBodyHandler: HttpResponse.BodyHandler?, ): HttpResponse { + capture.request = request @Suppress("UNCHECKED_CAST") - return fakeResponse(statusCode, body) as HttpResponse + return fakeResponse( + statusCode = statusCode, + body = body, + uri = request?.uri()?.toString() ?: "http://127.0.0.1:12345/responses", + ) as HttpResponse } override fun sendAsync( @@ -624,16 +664,27 @@ class OpenAiResponsesTestGeneratorTest { } } - private fun fakeResponse(statusCode: Int, body: String): HttpResponse { + private fun fakeResponse(statusCode: Int, body: String, uri: String): HttpResponse { return object : HttpResponse { override fun statusCode(): Int = statusCode - override fun request(): HttpRequest = HttpRequest.newBuilder().uri(URI.create("http://127.0.0.1:12345/responses")).build() + override fun request(): HttpRequest = HttpRequest.newBuilder().uri(URI.create(uri)).build() override fun previousResponse(): Optional> = Optional.empty() override fun headers(): HttpHeaders = HttpHeaders.of(emptyMap()) { _, _ -> true } override fun body(): String = body override fun sslSession(): Optional = Optional.empty() - override fun uri(): URI = URI.create("http://127.0.0.1:12345/responses") + override fun uri(): URI = URI.create(uri) override fun version(): HttpClient.Version = HttpClient.Version.HTTP_1_1 } } + + private fun anthropicResponseBody(): String = """ + { + "content": [ + { + "type": "text", + "text": "{\"content\":\"package com.example.auth\\n\\nclass SignUpViewModelGeneratedTest\",\"warnings\":[]}" + } + ] + } + """.trimIndent() }