Skip to content

Commit 1dea97f

Browse files
committed
feat: harden spec immutability
1 parent 8a101ca commit 1dea97f

30 files changed

Lines changed: 261 additions & 134 deletions

File tree

client/base/src/main/java/org/a2aproject/sdk/client/ClientTaskManager.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ synchronized Task saveTaskEvent(TaskStatusUpdateEvent taskStatusUpdateEvent) thr
6868

6969
Task.Builder taskBuilder = Task.builder(task);
7070
if (taskStatusUpdateEvent.status().message() != null) {
71-
if (task.history() == null) {
71+
if (task.history().isEmpty()) {
7272
taskBuilder.history(taskStatusUpdateEvent.status().message());
7373
} else {
7474
List<Message> history = new ArrayList<>(task.history());
@@ -133,4 +133,4 @@ private void saveTask(Task task) {
133133
contextId = currentTask.contextId();
134134
}
135135
}
136-
}
136+
}

client/transport/spi/src/main/java/org/a2aproject/sdk/client/transport/spi/interceptors/auth/AuthInterceptor.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public AuthInterceptor(final CredentialService credentialService) {
3838
public PayloadAndHeaders intercept(String methodName, @Nullable Object payload, Map<String, String> headers,
3939
@Nullable AgentCard agentCard, @Nullable ClientCallContext clientCallContext) {
4040
Map<String, String> updatedHeaders = new HashMap<>(headers == null ? new HashMap<>() : headers);
41-
if (agentCard == null || agentCard.securityRequirements()== null || agentCard.securitySchemes() == null) {
41+
if (agentCard == null || agentCard.securityRequirements().isEmpty() || agentCard.securitySchemes().isEmpty()) {
4242
return new PayloadAndHeaders(payload, updatedHeaders);
4343
}
4444
for (SecurityRequirement requirement : agentCard.securityRequirements()) {

compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/params/MessageSendConfigurationMapper_v0_3.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
* Key differences:
1717
* <ul>
1818
* <li>v0.3: {@code PushNotificationConfig pushNotificationConfig, Boolean blocking}</li>
19-
* <li>v1.0: {@code TaskPushNotificationConfig taskPushNotificationConfig, Boolean returnImmediately}</li>
19+
* <li>v1.0: {@code TaskPushNotificationConfig taskPushNotificationConfig, boolean returnImmediately}</li>
2020
* </ul>
2121
* <p>
2222
* Conversion strategy:
@@ -61,7 +61,7 @@ default MessageSendConfiguration toV10(
6161
}
6262

6363
// Convert blocking to returnImmediately (inverse semantics)
64-
Boolean returnImmediately = v03.blocking() != null ? !v03.blocking() : null;
64+
boolean returnImmediately = v03.blocking() != null ? !v03.blocking() : false;
6565

6666
return new MessageSendConfiguration(
6767
v03.acceptedOutputModes(),
@@ -95,7 +95,7 @@ default MessageSendConfiguration_v0_3 fromV10(
9595
}
9696

9797
// Convert returnImmediately to blocking (inverse semantics)
98-
Boolean blocking = v10.returnImmediately() != null ? !v10.returnImmediately() : null;
98+
Boolean blocking = !v10.returnImmediately();
9999

100100
return new MessageSendConfiguration_v0_3(
101101
v10.acceptedOutputModes(),

server-common/src/main/java/org/a2aproject/sdk/server/agentexecution/RequestContext.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@
5151
*
5252
* // Access configuration if needed
5353
* MessageSendConfiguration config = context.getConfiguration();
54-
* boolean returnImmediately = config != null && Boolean.TRUE.equals(config.returnImmediately());
54+
* boolean returnImmediately = config != null && config.returnImmediately();
5555
*
5656
* // Process and respond...
5757
* }

server-common/src/main/java/org/a2aproject/sdk/server/extensions/A2AExtensions.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ public static Set<String> getRequestedExtensions(List<String> values) {
3535
}
3636

3737
public static @Nullable AgentExtension findExtensionByUri(AgentCard card, String uri) {
38-
if (card.capabilities() == null || card.capabilities().extensions() == null) {
38+
if (card.capabilities().extensions().isEmpty()) {
3939
return null;
4040
}
4141
for (AgentExtension extension : card.capabilities().extensions()) {
@@ -55,7 +55,7 @@ public static Set<String> getRequestedExtensions(List<String> values) {
5555
*/
5656
public static void validateRequiredExtensions(AgentCard agentCard, ServerCallContext context)
5757
throws ExtensionSupportRequiredError {
58-
if (agentCard.capabilities() == null || agentCard.capabilities().extensions() == null) {
58+
if (agentCard.capabilities().extensions().isEmpty()) {
5959
return;
6060
}
6161

server-common/src/main/java/org/a2aproject/sdk/server/requesthandlers/DefaultRequestHandler.java

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,7 @@ public Task onGetTask(TaskQueryParams params, ServerCallContext context) throws
316316
* @return the task with limited history, or the original task if no limiting needed
317317
*/
318318
private static Task limitTaskHistory(Task task, @Nullable Integer historyLength) {
319-
if (task.history() == null || historyLength == null || historyLength >= task.history().size()) {
319+
if (historyLength == null || historyLength >= task.history().size()) {
320320
return task;
321321
}
322322
// Keep only the most recent historyLength messages
@@ -453,15 +453,13 @@ public EventKind onMessageSend(MessageSendParams params, ServerCallContext conte
453453
ResultAggregator resultAggregator = new ResultAggregator(mss.taskManager, null, executor, eventConsumerExecutor);
454454

455455
// Default to blocking per A2A spec (returnImmediately defaults to false, meaning wait for completion)
456-
boolean returnImmediately = params.configuration() != null && Boolean.TRUE.equals(params.configuration().returnImmediately());
456+
boolean returnImmediately = params.configuration() != null && params.configuration().returnImmediately();
457457
boolean blocking = !returnImmediately;
458458

459459
// Log return behavior from client request
460-
if (params.configuration() != null && params.configuration().returnImmediately() != null) {
460+
if (params.configuration() != null) {
461461
LOGGER.debug("DefaultRequestHandler: Client requested returnImmediately={}, using blocking={} for task {}",
462462
params.configuration().returnImmediately(), blocking, taskId.get());
463-
} else if (params.configuration() != null) {
464-
LOGGER.debug("DefaultRequestHandler: Client sent configuration but returnImmediately=null, using default blocking={} for task {}", blocking, taskId.get());
465463
} else {
466464
LOGGER.debug("DefaultRequestHandler: Client sent no configuration, using default blocking={} for task {}", blocking, taskId.get());
467465
}

server-common/src/main/java/org/a2aproject/sdk/server/tasks/TaskManager.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ boolean saveTaskEvent(TaskStatusUpdateEvent event, boolean isReplicated, @Nullab
9090
.status(event.status());
9191

9292
if (task.status().message() != null) {
93-
List<Message> newHistory = task.history() == null ? new ArrayList<>() : new ArrayList<>(task.history());
93+
List<Message> newHistory = new ArrayList<>(task.history());
9494
newHistory.add(task.status().message());
9595
builder.history(newHistory);
9696
}

server-common/src/main/java/org/a2aproject/sdk/server/util/ArtifactUtils.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ public static Artifact newArtifact(String name, List<Part<?>> parts, @Nullable S
3333
description,
3434
parts,
3535
null,
36-
null
36+
List.of()
3737
);
3838
}
3939

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
package org.a2aproject.sdk.server.tasks;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
import static org.junit.jupiter.api.Assertions.assertNull;
5+
import static org.junit.jupiter.api.Assertions.assertSame;
6+
import static org.junit.jupiter.api.Assertions.assertTrue;
7+
8+
import java.util.List;
9+
import java.util.Map;
10+
11+
import org.a2aproject.sdk.spec.Artifact;
12+
import org.a2aproject.sdk.spec.ListTasksParams;
13+
import org.a2aproject.sdk.spec.Message;
14+
import org.a2aproject.sdk.spec.Task;
15+
import org.a2aproject.sdk.spec.TaskState;
16+
import org.a2aproject.sdk.spec.TaskStatus;
17+
import org.a2aproject.sdk.spec.TextPart;
18+
import org.junit.jupiter.api.Test;
19+
20+
public class InMemoryTaskStoreTest {
21+
22+
@Test
23+
public void testSaveAndGet() {
24+
InMemoryTaskStore store = new InMemoryTaskStore();
25+
Task task = sampleTask("task-abc");
26+
27+
store.save(task, false);
28+
29+
Task retrieved = store.get(task.id());
30+
assertSame(task, retrieved);
31+
}
32+
33+
@Test
34+
public void testGetNonExistent() {
35+
InMemoryTaskStore store = new InMemoryTaskStore();
36+
37+
Task retrieved = store.get("nonexistent");
38+
assertNull(retrieved);
39+
}
40+
41+
@Test
42+
public void testDelete() {
43+
InMemoryTaskStore store = new InMemoryTaskStore();
44+
Task task = sampleTask("task-abc");
45+
46+
store.save(task, false);
47+
store.delete(task.id());
48+
49+
Task retrieved = store.get(task.id());
50+
assertNull(retrieved);
51+
}
52+
53+
@Test
54+
public void testDeleteNonExistent() {
55+
InMemoryTaskStore store = new InMemoryTaskStore();
56+
57+
store.delete("non-existent");
58+
}
59+
60+
@Test
61+
public void testListTransformsHistoryAndArtifacts() {
62+
InMemoryTaskStore store = new InMemoryTaskStore();
63+
Task task = Task.builder()
64+
.id("task-abc")
65+
.contextId("session-xyz")
66+
.status(new TaskStatus(TaskState.TASK_STATE_WORKING))
67+
.history(List.of(sampleMessage("msg-1"), sampleMessage("msg-2")))
68+
.artifacts(List.of(sampleArtifact("artifact-1")))
69+
.metadata(Map.of("origin", "test"))
70+
.build();
71+
72+
store.save(task, false);
73+
74+
ListTasksParams params = ListTasksParams.builder().build();
75+
List<Task> tasks = store.list(params, null).tasks();
76+
77+
assertEquals(1, tasks.size());
78+
Task listed = tasks.get(0);
79+
assertEquals(task.id(), listed.id());
80+
assertEquals(task.contextId(), listed.contextId());
81+
assertTrue(listed.history().isEmpty(), "Default list() should omit history");
82+
assertTrue(listed.artifacts().isEmpty(), "Default list() should omit artifacts");
83+
assertEquals(task.metadata(), listed.metadata());
84+
}
85+
86+
private static Task sampleTask(String id) {
87+
return Task.builder()
88+
.id(id)
89+
.contextId("session-xyz")
90+
.status(new TaskStatus(TaskState.TASK_STATE_SUBMITTED))
91+
.build();
92+
}
93+
94+
private static Message sampleMessage(String messageId) {
95+
return Message.builder()
96+
.role(Message.Role.ROLE_USER)
97+
.parts(List.of(new TextPart("content")))
98+
.messageId(messageId)
99+
.build();
100+
}
101+
102+
private static Artifact sampleArtifact(String artifactId) {
103+
return Artifact.builder()
104+
.artifactId(artifactId)
105+
.parts(List.of(new TextPart("artifact content")))
106+
.build();
107+
}
108+
}
109+

server-common/src/test/java/org/a2aproject/sdk/server/tasks/TaskManagerTest.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,6 @@ public void testSaveTaskEventStatusUpdate() throws A2AServerException {
9595

9696
assertEquals(initialTask.id(), updated.id());
9797
assertEquals(initialTask.contextId(), updated.contextId());
98-
// TODO type does not get unmarshalled
99-
//assertEquals(initialTask.getType(), updated.getType());
10098
assertSame(newStatus, updated.status());
10199
}
102100

0 commit comments

Comments
 (0)