|
| 1 | +package com.google.adk.a2a.converters; |
| 2 | + |
| 3 | +import static com.google.common.truth.Truth.assertThat; |
| 4 | +import static java.nio.charset.StandardCharsets.UTF_8; |
| 5 | + |
| 6 | +import com.google.common.collect.ImmutableList; |
| 7 | +import com.google.common.collect.ImmutableMap; |
| 8 | +import com.google.genai.types.Blob; |
| 9 | +import com.google.genai.types.FileData; |
| 10 | +import com.google.genai.types.FunctionCall; |
| 11 | +import com.google.genai.types.FunctionResponse; |
| 12 | +import com.google.genai.types.Part; |
| 13 | +import io.a2a.spec.DataPart; |
| 14 | +import io.a2a.spec.FilePart; |
| 15 | +import io.a2a.spec.FileWithBytes; |
| 16 | +import io.a2a.spec.FileWithUri; |
| 17 | +import io.a2a.spec.TextPart; |
| 18 | +import java.util.Base64; |
| 19 | +import java.util.Optional; |
| 20 | +import org.junit.Test; |
| 21 | +import org.junit.runner.RunWith; |
| 22 | +import org.junit.runners.JUnit4; |
| 23 | + |
| 24 | +@RunWith(JUnit4.class) |
| 25 | +public class PartConverterTest { |
| 26 | + |
| 27 | + @Test |
| 28 | + public void toGenaiPart_withNullPart_returnsEmpty() { |
| 29 | + assertThat(PartConverter.toGenaiPart(null)).isEmpty(); |
| 30 | + } |
| 31 | + |
| 32 | + @Test |
| 33 | + public void toGenaiPart_withTextPart_returnsGenaiTextPart() { |
| 34 | + TextPart textPart = new TextPart("Hello"); |
| 35 | + |
| 36 | + Optional<Part> result = PartConverter.toGenaiPart(textPart); |
| 37 | + |
| 38 | + assertThat(result).isPresent(); |
| 39 | + assertThat(result.get().text()).hasValue("Hello"); |
| 40 | + } |
| 41 | + |
| 42 | + @Test |
| 43 | + public void toGenaiPart_withFilePartUri_returnsGenaiFilePart() { |
| 44 | + FilePart filePart = new FilePart(new FileWithUri("text/plain", "file.txt", "http://file.txt")); |
| 45 | + |
| 46 | + Optional<Part> result = PartConverter.toGenaiPart(filePart); |
| 47 | + |
| 48 | + assertThat(result).isPresent(); |
| 49 | + assertThat(result.get().fileData()).isPresent(); |
| 50 | + FileData fileData = result.get().fileData().get(); |
| 51 | + assertThat(fileData.mimeType()).hasValue("text/plain"); |
| 52 | + assertThat(fileData.fileUri()).hasValue("http://file.txt"); |
| 53 | + } |
| 54 | + |
| 55 | + @Test |
| 56 | + public void toGenaiPart_withFilePartBytes_returnsGenaiBlobPart() { |
| 57 | + byte[] bytes = "file content".getBytes(UTF_8); |
| 58 | + String encoded = Base64.getEncoder().encodeToString(bytes); |
| 59 | + FilePart filePart = new FilePart(new FileWithBytes("text/plain", "file.txt", encoded)); |
| 60 | + |
| 61 | + Optional<Part> result = PartConverter.toGenaiPart(filePart); |
| 62 | + |
| 63 | + assertThat(result).isPresent(); |
| 64 | + assertThat(result.get().inlineData()).isPresent(); |
| 65 | + Blob blob = result.get().inlineData().get(); |
| 66 | + assertThat(blob.mimeType()).hasValue("text/plain"); |
| 67 | + assertThat(blob.data().get()).isEqualTo(bytes); |
| 68 | + } |
| 69 | + |
| 70 | + @Test |
| 71 | + public void toGenaiPart_withFilePartBytes_handlesNullBytes() { |
| 72 | + FilePart filePart = new FilePart(new FileWithBytes("text/plain", "file.txt", null)); |
| 73 | + assertThat(PartConverter.toGenaiPart(filePart)).isEmpty(); |
| 74 | + } |
| 75 | + |
| 76 | + @Test |
| 77 | + public void toGenaiPart_withFilePartBytes_handlesInvalidBase64() { |
| 78 | + FilePart filePart = |
| 79 | + new FilePart(new FileWithBytes("text/plain", "file.txt", "invalid-base64!")); |
| 80 | + assertThat(PartConverter.toGenaiPart(filePart)).isEmpty(); |
| 81 | + } |
| 82 | + |
| 83 | + @Test |
| 84 | + public void toGenaiPart_withDataPartFunctionCall_returnsGenaiFunctionCallPart() { |
| 85 | + ImmutableMap<String, Object> data = |
| 86 | + ImmutableMap.of("name", "func", "id", "1", "args", ImmutableMap.of()); |
| 87 | + DataPart dataPart = |
| 88 | + new DataPart( |
| 89 | + data, |
| 90 | + ImmutableMap.of( |
| 91 | + PartConverter.A2A_DATA_PART_METADATA_TYPE_KEY, |
| 92 | + PartConverter.A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL)); |
| 93 | + |
| 94 | + Optional<Part> result = PartConverter.toGenaiPart(dataPart); |
| 95 | + |
| 96 | + assertThat(result).isPresent(); |
| 97 | + assertThat(result.get().functionCall()).isPresent(); |
| 98 | + FunctionCall functionCall = result.get().functionCall().get(); |
| 99 | + assertThat(functionCall.name()).hasValue("func"); |
| 100 | + assertThat(functionCall.id()).hasValue("1"); |
| 101 | + assertThat(functionCall.args()).hasValue(ImmutableMap.of()); |
| 102 | + } |
| 103 | + |
| 104 | + @Test |
| 105 | + public void toGenaiPart_withDataPartFunctionCallByNameAndArgs_returnsGenaiFunctionCallPart() { |
| 106 | + ImmutableMap<String, Object> data = |
| 107 | + ImmutableMap.of("name", "func", "id", "1", "args", ImmutableMap.of("param", "value")); |
| 108 | + DataPart dataPart = new DataPart(data, null); |
| 109 | + |
| 110 | + Optional<Part> result = PartConverter.toGenaiPart(dataPart); |
| 111 | + |
| 112 | + assertThat(result).isPresent(); |
| 113 | + assertThat(result.get().functionCall()).isPresent(); |
| 114 | + FunctionCall functionCall = result.get().functionCall().get(); |
| 115 | + assertThat(functionCall.name()).hasValue("func"); |
| 116 | + assertThat(functionCall.id()).hasValue("1"); |
| 117 | + assertThat(functionCall.args()).hasValue(ImmutableMap.of("param", "value")); |
| 118 | + } |
| 119 | + |
| 120 | + @Test |
| 121 | + public void toGenaiPart_withDataPartFunctionResponse_returnsGenaiFunctionResponsePart() { |
| 122 | + ImmutableMap<String, Object> data = |
| 123 | + ImmutableMap.of("name", "func", "id", "1", "response", ImmutableMap.of()); |
| 124 | + DataPart dataPart = |
| 125 | + new DataPart( |
| 126 | + data, |
| 127 | + ImmutableMap.of( |
| 128 | + PartConverter.A2A_DATA_PART_METADATA_TYPE_KEY, |
| 129 | + PartConverter.A2A_DATA_PART_METADATA_TYPE_FUNCTION_RESPONSE)); |
| 130 | + |
| 131 | + Optional<Part> result = PartConverter.toGenaiPart(dataPart); |
| 132 | + |
| 133 | + assertThat(result).isPresent(); |
| 134 | + assertThat(result.get().functionResponse()).isPresent(); |
| 135 | + FunctionResponse functionResponse = result.get().functionResponse().get(); |
| 136 | + assertThat(functionResponse.name()).hasValue("func"); |
| 137 | + assertThat(functionResponse.id()).hasValue("1"); |
| 138 | + assertThat(functionResponse.response()).hasValue(ImmutableMap.of()); |
| 139 | + } |
| 140 | + |
| 141 | + @Test |
| 142 | + public void |
| 143 | + toGenaiPart_withDataPartFunctionResponseByNameAndResponse_returnsGenaiFunctionResponsePart() { |
| 144 | + ImmutableMap<String, Object> data = |
| 145 | + ImmutableMap.of("name", "func", "id", "1", "response", ImmutableMap.of("result", "value")); |
| 146 | + DataPart dataPart = new DataPart(data, null); |
| 147 | + |
| 148 | + Optional<Part> result = PartConverter.toGenaiPart(dataPart); |
| 149 | + |
| 150 | + assertThat(result).isPresent(); |
| 151 | + assertThat(result.get().functionResponse()).isPresent(); |
| 152 | + FunctionResponse functionResponse = result.get().functionResponse().get(); |
| 153 | + assertThat(functionResponse.name()).hasValue("func"); |
| 154 | + assertThat(functionResponse.id()).hasValue("1"); |
| 155 | + assertThat(functionResponse.response()).hasValue(ImmutableMap.of("result", "value")); |
| 156 | + } |
| 157 | + |
| 158 | + @Test |
| 159 | + public void toGenaiPart_withOtherDataPart_returnsGenaiTextPartWithJson() { |
| 160 | + ImmutableMap<String, Object> data = ImmutableMap.of("key", "value"); |
| 161 | + DataPart dataPart = new DataPart(data, null); |
| 162 | + |
| 163 | + Optional<Part> result = PartConverter.toGenaiPart(dataPart); |
| 164 | + |
| 165 | + assertThat(result).isPresent(); |
| 166 | + assertThat(result.get().text()).hasValue("{\"key\":\"value\"}"); |
| 167 | + } |
| 168 | + |
| 169 | + @Test |
| 170 | + public void toGenaiParts_convertsAllSupportedParts() { |
| 171 | + ImmutableList<io.a2a.spec.Part<?>> a2aParts = |
| 172 | + ImmutableList.of( |
| 173 | + new TextPart("text"), |
| 174 | + new FilePart(new FileWithUri("text/plain", "file.txt", "http://file.txt"))); |
| 175 | + |
| 176 | + ImmutableList<Part> result = PartConverter.toGenaiParts(a2aParts); |
| 177 | + |
| 178 | + assertThat(result).hasSize(2); |
| 179 | + assertThat(result.get(0).text()).hasValue("text"); |
| 180 | + assertThat(result.get(1).fileData()).isPresent(); |
| 181 | + } |
| 182 | + |
| 183 | + @Test |
| 184 | + public void convertGenaiPartToA2aPart_withNullPart_returnsEmpty() { |
| 185 | + assertThat(PartConverter.convertGenaiPartToA2aPart(null)).isEmpty(); |
| 186 | + } |
| 187 | + |
| 188 | + @Test |
| 189 | + public void convertGenaiPartToA2aPart_withTextPart_returnsEmpty() { |
| 190 | + Part part = Part.builder().text("text").build(); |
| 191 | + assertThat(PartConverter.convertGenaiPartToA2aPart(part)).isEmpty(); |
| 192 | + } |
| 193 | + |
| 194 | + @Test |
| 195 | + public void convertGenaiPartToA2aPart_withFunctionCallPart_returnsDataPart() { |
| 196 | + Part part = |
| 197 | + Part.builder() |
| 198 | + .functionCall( |
| 199 | + FunctionCall.builder() |
| 200 | + .name("func") |
| 201 | + .id("1") |
| 202 | + .args(ImmutableMap.of("param", "value")) |
| 203 | + .build()) |
| 204 | + .build(); |
| 205 | + |
| 206 | + Optional<DataPart> result = PartConverter.convertGenaiPartToA2aPart(part); |
| 207 | + |
| 208 | + assertThat(result).isPresent(); |
| 209 | + DataPart dataPart = result.get(); |
| 210 | + assertThat(dataPart.getData()) |
| 211 | + .containsExactly("name", "func", "id", "1", "args", ImmutableMap.of("param", "value")); |
| 212 | + assertThat(dataPart.getMetadata()) |
| 213 | + .containsEntry( |
| 214 | + PartConverter.A2A_DATA_PART_METADATA_TYPE_KEY, |
| 215 | + PartConverter.A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL); |
| 216 | + } |
| 217 | + |
| 218 | + @Test |
| 219 | + public void convertGenaiPartToA2aPart_withFunctionResponsePart_returnsDataPart() { |
| 220 | + Part part = |
| 221 | + Part.builder() |
| 222 | + .functionResponse( |
| 223 | + FunctionResponse.builder() |
| 224 | + .name("func") |
| 225 | + .id("1") |
| 226 | + .response(ImmutableMap.of("result", "value")) |
| 227 | + .build()) |
| 228 | + .build(); |
| 229 | + |
| 230 | + Optional<DataPart> result = PartConverter.convertGenaiPartToA2aPart(part); |
| 231 | + |
| 232 | + assertThat(result).isPresent(); |
| 233 | + DataPart dataPart = result.get(); |
| 234 | + assertThat(dataPart.getData()) |
| 235 | + .containsExactly("name", "func", "id", "1", "response", ImmutableMap.of("result", "value")); |
| 236 | + assertThat(dataPart.getMetadata()) |
| 237 | + .containsEntry( |
| 238 | + PartConverter.A2A_DATA_PART_METADATA_TYPE_KEY, |
| 239 | + PartConverter.A2A_DATA_PART_METADATA_TYPE_FUNCTION_RESPONSE); |
| 240 | + } |
| 241 | + |
| 242 | + @Test |
| 243 | + public void fromGenaiPart_withNullPart_returnsEmpty() { |
| 244 | + assertThat(PartConverter.fromGenaiPart(null)).isEmpty(); |
| 245 | + } |
| 246 | + |
| 247 | + @Test |
| 248 | + public void fromGenaiPart_withTextPart_returnsTextPart() { |
| 249 | + Part part = Part.builder().text("text").build(); |
| 250 | + |
| 251 | + Optional<io.a2a.spec.Part<?>> result = PartConverter.fromGenaiPart(part); |
| 252 | + |
| 253 | + assertThat(result).isPresent(); |
| 254 | + assertThat(result.get()).isInstanceOf(TextPart.class); |
| 255 | + assertThat(((TextPart) result.get()).getText()).isEqualTo("text"); |
| 256 | + } |
| 257 | + |
| 258 | + @Test |
| 259 | + public void fromGenaiPart_withFileDataPart_returnsFilePartWithUri() { |
| 260 | + Part part = |
| 261 | + Part.builder() |
| 262 | + .fileData(FileData.builder().mimeType("text/plain").fileUri("http://file.txt").build()) |
| 263 | + .build(); |
| 264 | + |
| 265 | + Optional<io.a2a.spec.Part<?>> result = PartConverter.fromGenaiPart(part); |
| 266 | + |
| 267 | + assertThat(result).isPresent(); |
| 268 | + assertThat(result.get()).isInstanceOf(FilePart.class); |
| 269 | + FilePart filePart = (FilePart) result.get(); |
| 270 | + assertThat(filePart.getFile()).isInstanceOf(FileWithUri.class); |
| 271 | + FileWithUri fileWithUri = (FileWithUri) filePart.getFile(); |
| 272 | + assertThat(fileWithUri.mimeType()).isEqualTo("text/plain"); |
| 273 | + assertThat(fileWithUri.uri()).isEqualTo("http://file.txt"); |
| 274 | + } |
| 275 | + |
| 276 | + @Test |
| 277 | + public void fromGenaiPart_withInlineDataPart_returnsFilePartWithBytes() { |
| 278 | + byte[] bytes = "content".getBytes(UTF_8); |
| 279 | + Part part = |
| 280 | + Part.builder() |
| 281 | + .inlineData(Blob.builder().mimeType("text/plain").data(bytes).build()) |
| 282 | + .build(); |
| 283 | + |
| 284 | + Optional<io.a2a.spec.Part<?>> result = PartConverter.fromGenaiPart(part); |
| 285 | + |
| 286 | + assertThat(result).isPresent(); |
| 287 | + assertThat(result.get()).isInstanceOf(FilePart.class); |
| 288 | + FilePart filePart = (FilePart) result.get(); |
| 289 | + assertThat(filePart.getFile()).isInstanceOf(FileWithBytes.class); |
| 290 | + FileWithBytes fileWithBytes = (FileWithBytes) filePart.getFile(); |
| 291 | + assertThat(fileWithBytes.mimeType()).isEqualTo("text/plain"); |
| 292 | + assertThat(Base64.getDecoder().decode(fileWithBytes.bytes())).isEqualTo(bytes); |
| 293 | + } |
| 294 | + |
| 295 | + @Test |
| 296 | + public void fromGenaiPart_withFunctionCallPart_returnsDataPart() { |
| 297 | + Part part = |
| 298 | + Part.builder() |
| 299 | + .functionCall( |
| 300 | + FunctionCall.builder().name("func").id("1").args(ImmutableMap.of()).build()) |
| 301 | + .build(); |
| 302 | + |
| 303 | + Optional<io.a2a.spec.Part<?>> result = PartConverter.fromGenaiPart(part); |
| 304 | + |
| 305 | + assertThat(result).isPresent(); |
| 306 | + assertThat(result.get()).isInstanceOf(DataPart.class); |
| 307 | + DataPart dataPart = (DataPart) result.get(); |
| 308 | + assertThat(dataPart.getData()) |
| 309 | + .containsExactly("name", "func", "id", "1", "args", ImmutableMap.of()); |
| 310 | + assertThat(dataPart.getMetadata()) |
| 311 | + .containsEntry( |
| 312 | + PartConverter.A2A_DATA_PART_METADATA_TYPE_KEY, |
| 313 | + PartConverter.A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL); |
| 314 | + } |
| 315 | + |
| 316 | + @Test |
| 317 | + public void fromGenaiPart_withFunctionResponsePart_returnsDataPart() { |
| 318 | + Part part = |
| 319 | + Part.builder() |
| 320 | + .functionResponse( |
| 321 | + FunctionResponse.builder().name("func").id("1").response(ImmutableMap.of()).build()) |
| 322 | + .build(); |
| 323 | + |
| 324 | + Optional<io.a2a.spec.Part<?>> result = PartConverter.fromGenaiPart(part); |
| 325 | + |
| 326 | + assertThat(result).isPresent(); |
| 327 | + assertThat(result.get()).isInstanceOf(DataPart.class); |
| 328 | + DataPart dataPart = (DataPart) result.get(); |
| 329 | + assertThat(dataPart.getData()) |
| 330 | + .containsExactly("name", "func", "id", "1", "response", ImmutableMap.of()); |
| 331 | + assertThat(dataPart.getMetadata()) |
| 332 | + .containsEntry( |
| 333 | + PartConverter.A2A_DATA_PART_METADATA_TYPE_KEY, |
| 334 | + PartConverter.A2A_DATA_PART_METADATA_TYPE_FUNCTION_RESPONSE); |
| 335 | + } |
| 336 | + |
| 337 | + @Test |
| 338 | + public void toGenaiPart_dataPartWithEmptyStringCoercedToEmptyMap() { |
| 339 | + ImmutableMap<String, Object> data = ImmutableMap.of("name", "func", "id", "1", "args", ""); |
| 340 | + DataPart dataPart = new DataPart(data, null); |
| 341 | + |
| 342 | + Optional<Part> result = PartConverter.toGenaiPart(dataPart); |
| 343 | + |
| 344 | + assertThat(result).isPresent(); |
| 345 | + assertThat(result.get().functionCall()).isPresent(); |
| 346 | + assertThat(result.get().functionCall().get().args()).hasValue(ImmutableMap.of()); |
| 347 | + } |
| 348 | + |
| 349 | + @Test |
| 350 | + public void toGenaiPart_dataPartWithNonMapCoercedToMap() { |
| 351 | + ImmutableMap<String, Object> data = ImmutableMap.of("name", "func", "id", "1", "args", 123); |
| 352 | + DataPart dataPart = new DataPart(data, null); |
| 353 | + |
| 354 | + Optional<Part> result = PartConverter.toGenaiPart(dataPart); |
| 355 | + |
| 356 | + assertThat(result).isPresent(); |
| 357 | + assertThat(result.get().functionCall()).isPresent(); |
| 358 | + assertThat(result.get().functionCall().get().args()).hasValue(ImmutableMap.of("value", 123)); |
| 359 | + } |
| 360 | +} |
0 commit comments