Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions spec-grpc/src/main/java/io/a2a/grpc/utils/JSONRPCUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -576,7 +576,7 @@ public static String toJsonRPCRequest(@Nullable String requestId, String method,
output.name("method").value(method);
}
if (payload != null) {
String resultValue = JsonFormat.printer().includingDefaultValueFields().omittingInsignificantWhitespace().print(payload);
String resultValue = JsonFormat.printer().omittingInsignificantWhitespace().print(payload);
output.name("params").jsonValue(resultValue);
}
output.endObject();
Expand All @@ -599,7 +599,7 @@ public static String toJsonRPCResultResponse(Object requestId, com.google.protob
output.name("id").value(number.longValue());
}
}
String resultValue = JsonFormat.printer().includingDefaultValueFields().omittingInsignificantWhitespace().print(builder);
String resultValue = JsonFormat.printer().omittingInsignificantWhitespace().print(builder);
output.name("result").jsonValue(resultValue);
output.endObject();
return result.toString();
Expand Down
141 changes: 124 additions & 17 deletions spec-grpc/src/test/java/io/a2a/grpc/utils/JSONRPCUtilsTest.java
Original file line number Diff line number Diff line change
@@ -1,33 +1,24 @@
package io.a2a.grpc.utils;

import static io.a2a.grpc.utils.JSONRPCUtils.ERROR_MESSAGE;
import static io.a2a.spec.A2AMethods.GET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD;
import static io.a2a.spec.A2AMethods.SET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.fail;

import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.JsonSyntaxException;

import io.a2a.grpc.Role;
import io.a2a.jsonrpc.common.json.InvalidParamsJsonMappingException;
import io.a2a.jsonrpc.common.json.JsonMappingException;
import io.a2a.jsonrpc.common.json.JsonProcessingException;
import io.a2a.jsonrpc.common.wrappers.A2ARequest;
import io.a2a.jsonrpc.common.wrappers.GetTaskPushNotificationConfigRequest;
import io.a2a.jsonrpc.common.wrappers.GetTaskPushNotificationConfigResponse;
import io.a2a.jsonrpc.common.wrappers.CreateTaskPushNotificationConfigRequest;
import io.a2a.jsonrpc.common.wrappers.CreateTaskPushNotificationConfigResponse;
import io.a2a.jsonrpc.common.wrappers.SendMessageRequest;
import io.a2a.jsonrpc.common.wrappers.*;
import io.a2a.spec.InvalidParamsError;
import io.a2a.spec.JSONParseError;
import io.a2a.spec.Message;
import io.a2a.spec.PushNotificationConfig;
import io.a2a.spec.TaskPushNotificationConfig;
import org.junit.jupiter.api.Test;

import static io.a2a.grpc.utils.JSONRPCUtils.ERROR_MESSAGE;
import static io.a2a.spec.A2AMethods.GET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD;
import static io.a2a.spec.A2AMethods.SET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD;
import static org.junit.jupiter.api.Assertions.*;

public class JSONRPCUtilsTest {

@Test
Expand Down Expand Up @@ -388,6 +379,122 @@ public void testParseErrorResponse_InvalidParams() throws Exception {
assertEquals("Invalid params", response.getError().getMessage());
}

// ── toJsonRPCRequest serialization ────────────────────────────────────────

@Test
public void testToJsonRPCRequest_TextPart_NullMetadata_OnlyTextFieldInPart() {
io.a2a.grpc.SendMessageRequest request = io.a2a.grpc.SendMessageRequest.newBuilder()
.setMessage(io.a2a.grpc.Message.newBuilder()
.setMessageId("msg-1")
.setRole(Role.ROLE_USER)
.addParts(io.a2a.grpc.Part.newBuilder()
.setText("hello")
.build())
.build())
.build();

String json = JSONRPCUtils.toJsonRPCRequest("req-1", "SendMessage", request);

JsonObject part = getFirstPart(json, "params");

assertEquals(1, part.size(), "TextPart with no metadata should only have 'text' field");
assertTrue(part.has("text"));
assertFalse(part.has("filename"), "filename must not appear for TextPart");
assertFalse(part.has("mediaType"), "mediaType must not appear for TextPart");
assertFalse(part.has("metadata"), "metadata must not appear when not set");
}

@Test
public void testToJsonRPCRequest_TextPart_WithMetadata_OnlyTextAndMetadataFields() {
io.a2a.grpc.SendMessageRequest request = io.a2a.grpc.SendMessageRequest.newBuilder()
.setMessage(io.a2a.grpc.Message.newBuilder()
.setMessageId("msg-1")
.setRole(Role.ROLE_USER)
.addParts(io.a2a.grpc.Part.newBuilder()
.setText("hello")
.setMetadata(com.google.protobuf.Struct.newBuilder()
.putFields("key",
com.google.protobuf.Value.newBuilder()
.setStringValue("value")
.build())
.build())
.build())
.build())
.build();

String json = JSONRPCUtils.toJsonRPCRequest("req-1", "SendMessage", request);

JsonObject part = getFirstPart(json, "params");

assertEquals(2, part.size(), "TextPart with metadata should only have 'text' and 'metadata' fields");
assertTrue(part.has("text"));
assertTrue(part.has("metadata"));
assertFalse(part.has("filename"), "filename must not appear for TextPart");
assertFalse(part.has("mediaType"), "mediaType must not appear for TextPart");
}

// ── toJsonRPCResultResponse serialization ─────────────────────────────────

@Test
public void testToJsonRPCResultResponse_TextPart_NullMetadata_OnlyTextFieldInPart() {
io.a2a.grpc.SendMessageResponse response = io.a2a.grpc.SendMessageResponse.newBuilder()
.setMessage(io.a2a.grpc.Message.newBuilder()
.setMessageId("msg-1")
.setRole(Role.ROLE_AGENT)
.addParts(io.a2a.grpc.Part.newBuilder()
.setText("hi there")
.build())
.build())
.build();

String json = JSONRPCUtils.toJsonRPCResultResponse("req-1", response);

JsonObject part = getFirstPart(json, "result");

assertEquals(1, part.size(), "TextPart with no metadata should only have 'text' field");
assertTrue(part.has("text"));
assertFalse(part.has("filename"), "filename must not appear for TextPart");
assertFalse(part.has("mediaType"), "mediaType must not appear for TextPart");
assertFalse(part.has("metadata"), "metadata must not appear when not set");
}

@Test
public void testToJsonRPCResultResponse_TextPart_WithMetadata_OnlyTextAndMetadataFields() {
io.a2a.grpc.SendMessageResponse response = io.a2a.grpc.SendMessageResponse.newBuilder()
.setMessage(io.a2a.grpc.Message.newBuilder()
.setMessageId("msg-1")
.setRole(Role.ROLE_AGENT)
.addParts(io.a2a.grpc.Part.newBuilder()
.setText("hi there")
.setMetadata(com.google.protobuf.Struct.newBuilder()
.putFields("tag",
com.google.protobuf.Value.newBuilder()
.setStringValue("reply")
.build())
.build())
.build())
.build())
.build();

String json = JSONRPCUtils.toJsonRPCResultResponse("req-1", response);

JsonObject part = getFirstPart(json, "result");

assertEquals(2, part.size(), "TextPart with metadata should only have 'text' and 'metadata' fields");
assertTrue(part.has("text"));
assertTrue(part.has("metadata"));
assertFalse(part.has("filename"), "filename must not appear for TextPart");
assertFalse(part.has("mediaType"), "mediaType must not appear for TextPart");
}

private JsonObject getFirstPart(String json, String topLevelField) {
return JsonParser.parseString(json).getAsJsonObject()
.get(topLevelField).getAsJsonObject()
.get("message").getAsJsonObject()
.get("parts").getAsJsonArray()
.get(0).getAsJsonObject();
}

@Test
public void testParseErrorResponse_ParseError() throws Exception {
String errorResponse = """
Expand Down
Loading