diff --git a/pom.xml b/pom.xml
index a064798276e..a9c9f2eea05 100644
--- a/pom.xml
+++ b/pom.xml
@@ -179,6 +179,12 @@
kotlin-reflect
${kotlin.version}
+
+ org.jetbrains.kotlin
+ kotlin-test-junit5
+ ${kotlin.version}
+ test
+
com.fasterxml.jackson.module
@@ -190,6 +196,37 @@
error_prone_annotations
2.36.0
+
+
+ org.assertj
+ assertj-core
+ 3.25.3
+ test
+
+
+ org.wiremock
+ wiremock
+ 3.5.4
+ test
+
+
+ org.junit-pioneer
+ junit-pioneer
+ 2.2.0
+ test
+
+
+ org.mockito.kotlin
+ mockito-kotlin
+ 5.4.0
+ test
+
+
+ org.mockito
+ mockito-junit-jupiter
+ 5.12.0
+ test
+
@@ -244,6 +281,9 @@
plain
+
+ **/ProGuardCompatibilityTest.kt
+
@@ -345,7 +385,7 @@
- 1.8
+ 11
-Xjvm-default=all
diff --git a/src/test/java/com/google/genai/interactions/client/okhttp/OkHttpClientTest.kt b/src/test/java/com/google/genai/interactions/client/okhttp/OkHttpClientTest.kt
new file mode 100644
index 00000000000..805600666ce
--- /dev/null
+++ b/src/test/java/com/google/genai/interactions/client/okhttp/OkHttpClientTest.kt
@@ -0,0 +1,60 @@
+/*
+* Copyright 2025 Google LLC
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+
+package com.google.genai.interactions.client.okhttp
+
+import com.google.genai.interactions.core.http.HttpMethod
+import com.google.genai.interactions.core.http.HttpRequest
+import com.github.tomakehurst.wiremock.client.WireMock.*
+import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo
+import com.github.tomakehurst.wiremock.junit5.WireMockTest
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.parallel.ResourceLock
+
+@WireMockTest
+@ResourceLock("https://github.com/wiremock/wiremock/issues/169")
+internal class OkHttpClientTest {
+
+ private lateinit var baseUrl: String
+ private lateinit var httpClient: OkHttpClient
+
+ @BeforeEach
+ fun beforeEach(wmRuntimeInfo: WireMockRuntimeInfo) {
+ baseUrl = wmRuntimeInfo.httpBaseUrl
+ httpClient = OkHttpClient.builder().build()
+ }
+
+ @Test
+ fun executeAsync_whenFutureCancelled_cancelsUnderlyingCall() {
+ stubFor(post(urlPathEqualTo("/something")).willReturn(ok()))
+ val responseFuture =
+ httpClient.executeAsync(
+ HttpRequest.builder()
+ .method(HttpMethod.POST)
+ .baseUrl(baseUrl)
+ .addPathSegment("something")
+ .build()
+ )
+ val call = httpClient.okHttpClient.dispatcher.runningCalls().single()
+
+ responseFuture.cancel(false)
+
+ // Should have cancelled the underlying call
+ assertThat(call.isCanceled()).isTrue()
+ }
+}
diff --git a/src/test/java/com/google/genai/interactions/core/ClientOptionsTest.kt b/src/test/java/com/google/genai/interactions/core/ClientOptionsTest.kt
new file mode 100644
index 00000000000..98c3762bfcb
--- /dev/null
+++ b/src/test/java/com/google/genai/interactions/core/ClientOptionsTest.kt
@@ -0,0 +1,73 @@
+/*
+* Copyright 2025 Google LLC
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+
+// File generated from our OpenAPI spec by Stainless.
+
+package com.google.genai.interactions.core
+
+import com.google.genai.interactions.core.http.HttpClient
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import org.mockito.junit.jupiter.MockitoExtension
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+import org.mockito.kotlin.verify
+
+@ExtendWith(MockitoExtension::class)
+internal class ClientOptionsTest {
+
+ private val httpClient = mock()
+
+ @Test
+ fun putHeader_canOverwriteDefaultHeader() {
+ val clientOptions =
+ ClientOptions.builder()
+ .httpClient(httpClient)
+ .putHeader("User-Agent", "My User Agent")
+ .apiKey("My API Key")
+ .build()
+
+ assertThat(clientOptions.headers.values("User-Agent")).containsExactly("My User Agent")
+ }
+
+ @Test
+ fun toBuilder_apiKeyCanBeUpdated() {
+ var clientOptions =
+ ClientOptions.builder().httpClient(httpClient).apiKey("My API Key").build()
+
+ clientOptions = clientOptions.toBuilder().apiKey("another My API Key").build()
+
+ assertThat(clientOptions.headers.values("x-goog-api-key"))
+ .containsExactly("another My API Key")
+ }
+
+ @Test
+ fun toBuilder_whenOriginalClientOptionsGarbageCollected_doesNotCloseOriginalClient() {
+ var clientOptions =
+ ClientOptions.builder().httpClient(httpClient).apiKey("My API Key").build()
+ verify(httpClient, never()).close()
+
+ // Overwrite the `clientOptions` variable so that the original `ClientOptions` is GC'd.
+ clientOptions = clientOptions.toBuilder().build()
+ System.gc()
+ Thread.sleep(100)
+
+ verify(httpClient, never()).close()
+ // This exists so that `clientOptions` is still reachable.
+ assertThat(clientOptions).isEqualTo(clientOptions)
+ }
+}
diff --git a/src/test/java/com/google/genai/interactions/core/ObjectMappersTest.kt b/src/test/java/com/google/genai/interactions/core/ObjectMappersTest.kt
new file mode 100644
index 00000000000..af2d8bd5160
--- /dev/null
+++ b/src/test/java/com/google/genai/interactions/core/ObjectMappersTest.kt
@@ -0,0 +1,133 @@
+/*
+* Copyright 2025 Google LLC
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+
+package com.google.genai.interactions.core
+
+import com.fasterxml.jackson.annotation.JsonProperty
+import com.fasterxml.jackson.databind.exc.MismatchedInputException
+import com.fasterxml.jackson.module.kotlin.readValue
+import java.time.LocalDate
+import java.time.LocalTime
+import java.time.OffsetDateTime
+import java.time.ZoneOffset
+import kotlin.reflect.KClass
+import org.assertj.core.api.Assertions.assertThat
+import org.assertj.core.api.Assertions.catchThrowable
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.params.ParameterizedTest
+import org.junit.jupiter.params.provider.EnumSource
+import org.junitpioneer.jupiter.cartesian.CartesianTest
+
+internal class ObjectMappersTest {
+
+ internal class ClassWithBooleanFieldPrefixedWithIs(private val isActive: JsonField) {
+
+ @JsonProperty("is_active") @ExcludeMissing fun _isActive() = isActive
+ }
+
+ @Test
+ fun write_whenFieldPrefixedWithIs_keepsPrefix() {
+ val value = ClassWithBooleanFieldPrefixedWithIs(JsonField.of(true))
+
+ val json = jsonMapper().writeValueAsString(value)
+
+ assertThat(json).isEqualTo("{\"is_active\":true}")
+ }
+
+ internal class Class(@get:JsonProperty("field") @JsonProperty("field") val field: String)
+
+ enum class ShapeTestCase(val value: Any, val kClass: KClass<*>) {
+ STRING("Hello World!", String::class),
+ BOOLEAN(true, Boolean::class),
+ FLOAT(3.14F, Float::class),
+ DOUBLE(3.14, Double::class),
+ INTEGER(42, Int::class),
+ LONG(42L, Long::class),
+ MAP(mapOf("property" to "value"), Map::class),
+ CLASS(Class("Hello World!"), Class::class),
+ LIST(listOf(1, 2, 3), List::class);
+
+ companion object {
+ val VALID_CONVERSIONS =
+ listOf(
+ FLOAT to DOUBLE,
+ DOUBLE to FLOAT,
+ INTEGER to FLOAT,
+ INTEGER to DOUBLE,
+ INTEGER to LONG,
+ LONG to FLOAT,
+ LONG to DOUBLE,
+ LONG to INTEGER,
+ CLASS to MAP,
+ )
+ }
+ }
+
+ @CartesianTest
+ fun read(@CartesianTest.Enum shape1: ShapeTestCase, @CartesianTest.Enum shape2: ShapeTestCase) {
+ val jsonMapper = jsonMapper()
+ val json = jsonMapper.writeValueAsString(shape1.value)
+
+ val e = catchThrowable { jsonMapper.readValue(json, shape2.kClass.java) }
+
+ if (shape1 == shape2 || shape1 to shape2 in ShapeTestCase.VALID_CONVERSIONS) {
+ assertThat(e).isNull()
+ } else {
+ assertThat(e).isInstanceOf(MismatchedInputException::class.java)
+ }
+ }
+
+ enum class LenientOffsetDateTimeTestCase(
+ val string: String,
+ val expectedOffsetDateTime: OffsetDateTime,
+ ) {
+ DATE(
+ "1998-04-21",
+ expectedOffsetDateTime =
+ OffsetDateTime.of(LocalDate.of(1998, 4, 21), LocalTime.of(0, 0), ZoneOffset.UTC),
+ ),
+ DATE_TIME(
+ "1998-04-21T04:00:00",
+ expectedOffsetDateTime =
+ OffsetDateTime.of(LocalDate.of(1998, 4, 21), LocalTime.of(4, 0), ZoneOffset.UTC),
+ ),
+ ZONED_DATE_TIME_1(
+ "1998-04-21T04:00:00+03:00",
+ expectedOffsetDateTime =
+ OffsetDateTime.of(
+ LocalDate.of(1998, 4, 21),
+ LocalTime.of(4, 0),
+ ZoneOffset.ofHours(3),
+ ),
+ ),
+ ZONED_DATE_TIME_2(
+ "1998-04-21T04:00:00Z",
+ expectedOffsetDateTime =
+ OffsetDateTime.of(LocalDate.of(1998, 4, 21), LocalTime.of(4, 0), ZoneOffset.UTC),
+ ),
+ }
+
+ @ParameterizedTest
+ @EnumSource
+ fun readOffsetDateTime_lenient(testCase: LenientOffsetDateTimeTestCase) {
+ val jsonMapper = jsonMapper()
+ val json = jsonMapper.writeValueAsString(testCase.string)
+
+ val offsetDateTime = jsonMapper().readValue(json)
+
+ assertThat(offsetDateTime).isEqualTo(testCase.expectedOffsetDateTime)
+ }
+}
diff --git a/src/test/java/com/google/genai/interactions/core/PhantomReachableTest.kt b/src/test/java/com/google/genai/interactions/core/PhantomReachableTest.kt
new file mode 100644
index 00000000000..74a1c4092f8
--- /dev/null
+++ b/src/test/java/com/google/genai/interactions/core/PhantomReachableTest.kt
@@ -0,0 +1,43 @@
+/*
+* Copyright 2025 Google LLC
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+
+package com.google.genai.interactions.core
+
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.Test
+
+internal class PhantomReachableTest {
+
+ @Test
+ fun closeWhenPhantomReachable_whenObservedIsGarbageCollected_closesCloseable() {
+ var closed = false
+ val closeable = AutoCloseable { closed = true }
+
+ closeWhenPhantomReachable(
+ // Pass an inline object for the object to observe so that it becomes immediately
+ // unreachable.
+ Any(),
+ closeable,
+ )
+
+ assertThat(closed).isFalse()
+
+ System.gc()
+ Thread.sleep(100)
+
+ assertThat(closed).isTrue()
+ }
+}
diff --git a/src/test/java/com/google/genai/interactions/core/UtilsTest.kt b/src/test/java/com/google/genai/interactions/core/UtilsTest.kt
new file mode 100644
index 00000000000..a6befa08016
--- /dev/null
+++ b/src/test/java/com/google/genai/interactions/core/UtilsTest.kt
@@ -0,0 +1,49 @@
+/*
+* Copyright 2025 Google LLC
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+
+package com.google.genai.interactions.core
+
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.Test
+
+internal class UtilsTest {
+ @Test
+ fun contentDeepEquals() {
+ assertThat(42 contentEquals 42).isTrue()
+ assertThat(42 contentEquals "Hello World!").isFalse()
+ assertThat(byteArrayOf(1, 2, 3) contentEquals byteArrayOf(1, 2, 3)).isTrue()
+ assertThat(byteArrayOf(1, 2, 3) contentEquals byteArrayOf(1, 2, 4)).isFalse()
+ assertThat(
+ arrayOf(byteArrayOf(1, 2), byteArrayOf(3)) contentEquals
+ arrayOf(byteArrayOf(1, 2), byteArrayOf(3))
+ )
+ .isTrue()
+ assertThat(
+ arrayOf(byteArrayOf(1, 2), byteArrayOf(3)) contentEquals
+ arrayOf(byteArrayOf(1), byteArrayOf(2, 3))
+ )
+ .isFalse()
+ }
+
+ @Test
+ fun contentToString() {
+ assertThat((42).contentToString()).isEqualTo("42")
+ assertThat("Hello World!".contentToString()).isEqualTo("Hello World!")
+ assertThat(byteArrayOf(1, 2, 3).contentToString()).isEqualTo("[1, 2, 3]")
+ assertThat(arrayOf(byteArrayOf(1, 2), byteArrayOf(3)).contentToString())
+ .isEqualTo("[[1, 2], [3]]")
+ }
+}
diff --git a/src/test/java/com/google/genai/interactions/core/ValuesTest.kt b/src/test/java/com/google/genai/interactions/core/ValuesTest.kt
new file mode 100644
index 00000000000..2673093e491
--- /dev/null
+++ b/src/test/java/com/google/genai/interactions/core/ValuesTest.kt
@@ -0,0 +1,160 @@
+/*
+* Copyright 2025 Google LLC
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+
+package com.google.genai.interactions.core
+
+import java.util.Optional
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.params.ParameterizedTest
+import org.junit.jupiter.params.provider.EnumSource
+
+internal class ValuesTest {
+ companion object {
+ private val NON_JSON = Any()
+ }
+
+ enum class TestCase(
+ val value: JsonField<*>,
+ val expectedIsMissing: Boolean = false,
+ val expectedIsNull: Boolean = false,
+ val expectedAsKnown: Optional<*> = Optional.empty(),
+ val expectedAsBoolean: Optional = Optional.empty(),
+ val expectedAsNumber: Optional = Optional.empty(),
+ val expectedAsString: Optional = Optional.empty(),
+ val expectedAsArray: Optional> = Optional.empty(),
+ val expectedAsObject: Optional