Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package org.a2aproject.sdk.compat03.conversion.mappers.domain;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

import java.util.HashMap;
import java.util.Map;

import org.a2aproject.sdk.compat03.spec.TaskState_v0_3;
import org.a2aproject.sdk.compat03.spec.TaskStatusUpdateEvent_v0_3;
import org.a2aproject.sdk.compat03.spec.TaskStatus_v0_3;
import org.a2aproject.sdk.spec.TaskState;
import org.a2aproject.sdk.spec.TaskStatus;
import org.a2aproject.sdk.spec.TaskStatusUpdateEvent;
import org.junit.jupiter.api.Test;

class TaskStatusUpdateEventMapper_v0_3_Test {

@Test
void toV10_preservesMetadataAndDefensiveCopy() {
Map<String, Object> mutableMetadata = new HashMap<>();
mutableMetadata.put("source", "sensor");

TaskStatusUpdateEvent_v0_3 v03 = new TaskStatusUpdateEvent_v0_3.Builder()
.taskId("task-123")
.status(new TaskStatus_v0_3(TaskState_v0_3.WORKING))
.contextId("context-456")
.isFinal(false)
.metadata(mutableMetadata)
.build();

TaskStatusUpdateEvent v10 = TaskStatusUpdateEventMapper_v0_3.INSTANCE.toV10(v03);
mutableMetadata.put("write", "should-not-leak");

assertEquals("task-123", v10.taskId());
assertEquals(TaskState.TASK_STATE_WORKING, v10.status().state());
assertEquals("context-456", v10.contextId());
assertEquals(Map.of("source", "sensor"), v10.metadata());
assertThrows(UnsupportedOperationException.class, () -> v10.metadata().put("another", "value"));
}

@Test
void fromV10_preservesMetadata() {
TaskStatusUpdateEvent v10 = new TaskStatusUpdateEvent(
"task-123",
new TaskStatus(TaskState.TASK_STATE_WORKING),
"context-456",
Map.of("source", "sensor"));

TaskStatusUpdateEvent_v0_3 v03 = TaskStatusUpdateEventMapper_v0_3.INSTANCE.fromV10(v10);

assertEquals("task-123", v03.taskId());
assertEquals(TaskState_v0_3.WORKING, v03.status().state());
assertEquals("context-456", v03.contextId());
assertEquals(Map.of("source", "sensor"), v03.metadata());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package org.a2aproject.sdk.server.tasks;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this test related to the spec hardening?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, this test was accidentally deleted in feat: Implement MainEventBus architecture for event queue processing (#611). I just rollback it
image


import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.List;
import java.util.Map;

import org.a2aproject.sdk.spec.Artifact;
import org.a2aproject.sdk.spec.ListTasksParams;
import org.a2aproject.sdk.spec.Message;
import org.a2aproject.sdk.spec.Task;
import org.a2aproject.sdk.spec.TaskState;
import org.a2aproject.sdk.spec.TaskStatus;
import org.a2aproject.sdk.spec.TextPart;
import org.junit.jupiter.api.Test;

public class InMemoryTaskStoreTest {

@Test
public void testSaveAndGet() {
InMemoryTaskStore store = new InMemoryTaskStore();
Task task = sampleTask("task-abc");

store.save(task, false);

Task retrieved = store.get(task.id());
assertSame(task, retrieved);
}

@Test
public void testGetNonExistent() {
InMemoryTaskStore store = new InMemoryTaskStore();

Task retrieved = store.get("nonexistent");
assertNull(retrieved);
}

@Test
public void testDelete() {
InMemoryTaskStore store = new InMemoryTaskStore();
Task task = sampleTask("task-abc");

store.save(task, false);
store.delete(task.id());

Task retrieved = store.get(task.id());
assertNull(retrieved);
}

@Test
public void testDeleteNonExistent() {
InMemoryTaskStore store = new InMemoryTaskStore();

store.delete("non-existent");
}

@Test
public void testListTransformsHistoryAndArtifacts() {
InMemoryTaskStore store = new InMemoryTaskStore();
Task task = Task.builder()
.id("task-abc")
.contextId("session-xyz")
.status(new TaskStatus(TaskState.TASK_STATE_WORKING))
.history(List.of(sampleMessage("msg-1"), sampleMessage("msg-2")))
.artifacts(List.of(sampleArtifact("artifact-1")))
.metadata(Map.of("origin", "test"))
.build();

store.save(task, false);

ListTasksParams params = ListTasksParams.builder().build();
List<Task> tasks = store.list(params, null).tasks();

assertEquals(1, tasks.size());
Task listed = tasks.get(0);
assertEquals(task.id(), listed.id());
assertEquals(task.contextId(), listed.contextId());
assertTrue(listed.history().isEmpty(), "Default list() should omit history");
assertTrue(listed.artifacts().isEmpty(), "Default list() should omit artifacts");
assertEquals(task.metadata(), listed.metadata());
}

private static Task sampleTask(String id) {
return Task.builder()
.id(id)
.contextId("session-xyz")
.status(new TaskStatus(TaskState.TASK_STATE_SUBMITTED))
.build();
}

private static Message sampleMessage(String messageId) {
return Message.builder()
.role(Message.Role.ROLE_USER)
.parts(List.of(new TextPart("content")))
.messageId(messageId)
.build();
}

private static Artifact sampleArtifact(String artifactId) {
return Artifact.builder()
.artifactId(artifactId)
.parts(List.of(new TextPart("artifact content")))
.build();
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,6 @@ public void testSaveTaskEventStatusUpdate() throws A2AServerException {

assertEquals(initialTask.id(), updated.id());
assertEquals(initialTask.contextId(), updated.contextId());
// TODO type does not get unmarshalled
//assertEquals(initialTask.getType(), updated.getType());
assertSame(newStatus, updated.status());
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package org.a2aproject.sdk.spec;

import org.a2aproject.sdk.util.CollectionCopies;
import java.util.List;

import org.jspecify.annotations.Nullable;

/**
Expand Down Expand Up @@ -35,6 +37,10 @@ public record AgentCapabilities(boolean streaming,
boolean extendedAgentCard,
@Nullable List<AgentExtension> extensions) {

public AgentCapabilities {
extensions = CollectionCopies.immutableNullableList(extensions);
}

/**
* Create a new Builder
*
Expand Down
28 changes: 18 additions & 10 deletions spec/src/main/java/org/a2aproject/sdk/spec/AgentCard.java
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
package org.a2aproject.sdk.spec;

import org.a2aproject.sdk.util.Assert;
import org.a2aproject.sdk.util.CollectionCopies;
import org.jspecify.annotations.Nullable;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;

import org.a2aproject.sdk.util.Assert;
import org.jspecify.annotations.Nullable;

/**
* The AgentCard is a self-describing manifest for an agent in the A2A Protocol.
* <p>
Expand Down Expand Up @@ -89,6 +89,14 @@ public record AgentCard(
Assert.checkNotNullParam("skills", skills);
Assert.checkNotNullParam("supportedInterfaces", supportedInterfaces);
Assert.checkNotNullParam("version", version);
defaultInputModes = CollectionCopies.immutableList(defaultInputModes);
defaultOutputModes = CollectionCopies.immutableList(defaultOutputModes);
skills = CollectionCopies.immutableList(skills);
securitySchemes = CollectionCopies.immutableNullableMap(securitySchemes);
securityRequirements = CollectionCopies.immutableNullableList(securityRequirements);
supportedInterfaces = CollectionCopies.immutableList(supportedInterfaces);
signatures = CollectionCopies.immutableNullableList(signatures);
additionalInterfaces = CollectionCopies.immutableNullableList(additionalInterfaces);
}

/**
Expand Down Expand Up @@ -184,13 +192,13 @@ private Builder(AgentCard card) {
this.version = card.version();
this.documentationUrl = card.documentationUrl();
this.capabilities = card.capabilities();
this.defaultInputModes = card.defaultInputModes() != null ? new ArrayList<>(card.defaultInputModes()) : Collections.emptyList();
this.defaultOutputModes = card.defaultOutputModes() != null ? new ArrayList<>(card.defaultOutputModes()) : Collections.emptyList();
this.skills = card.skills() != null ? new ArrayList<>(card.skills()) : Collections.emptyList();
this.securitySchemes = card.securitySchemes() != null ? Map.copyOf(card.securitySchemes()) : Collections.emptyMap();
this.securityRequirements = card.securityRequirements() != null ? new ArrayList<>(card.securityRequirements()) : Collections.emptyList();
this.defaultInputModes = CollectionCopies.mutableListCopyOrEmpty(card.defaultInputModes());
this.defaultOutputModes = CollectionCopies.mutableListCopyOrEmpty(card.defaultOutputModes());
this.skills = CollectionCopies.mutableListCopyOrEmpty(card.skills());
this.securitySchemes = CollectionCopies.mutableMapCopyOrEmpty(card.securitySchemes());
this.securityRequirements = CollectionCopies.mutableListCopyOrEmpty(card.securityRequirements());
this.iconUrl = card.iconUrl();
this.supportedInterfaces = card.supportedInterfaces() != null ? new ArrayList<>(card.supportedInterfaces()) : Collections.emptyList();
this.supportedInterfaces = CollectionCopies.mutableListCopyOrEmpty(card.supportedInterfaces());
this.signatures = card.signatures() != null ? new ArrayList<>(card.signatures()) : null;
this.url = card.url();
this.preferredTransport= card.preferredTransport();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import com.google.gson.annotations.SerializedName;
import org.a2aproject.sdk.util.Assert;
import org.a2aproject.sdk.util.CollectionCopies;
import org.jspecify.annotations.Nullable;

/**
Expand Down Expand Up @@ -46,6 +47,7 @@ public record AgentCardSignature(@Nullable Map<String, Object> header, @Serializ
public AgentCardSignature {
Assert.checkNotNullParam("protectedHeader", protectedHeader);
Assert.checkNotNullParam("signature", signature);
header = CollectionCopies.unmodifiableNullableShallowMap(header);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import java.util.Map;

import org.a2aproject.sdk.util.Assert;
import org.a2aproject.sdk.util.CollectionCopies;
import org.jspecify.annotations.Nullable;

/**
Expand Down Expand Up @@ -37,6 +38,7 @@ public record AgentExtension (@Nullable String description, @Nullable Map<String
*/
public AgentExtension {
Assert.checkNotNullParam("uri", uri);
params = CollectionCopies.unmodifiableNullableShallowMap(params);
}

/**
Expand Down
5 changes: 5 additions & 0 deletions spec/src/main/java/org/a2aproject/sdk/spec/AgentSkill.java
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ public record AgentSkill(String id, String name, String description, List<String
Assert.checkNotNullParam("name", name);
Assert.checkNotNullParam("description", description);
Assert.checkNotNullParam("tags", tags);
tags = List.copyOf(tags);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's use the CollectionCopies here too.

examples = examples == null ? null : List.copyOf(examples);
inputModes = inputModes == null ? null : List.copyOf(inputModes);
outputModes = outputModes == null ? null : List.copyOf(outputModes);
securityRequirements = securityRequirements == null ? null : List.copyOf(securityRequirements);
}

/**
Expand Down
8 changes: 6 additions & 2 deletions spec/src/main/java/org/a2aproject/sdk/spec/Artifact.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import java.util.Map;

import org.a2aproject.sdk.util.Assert;
import org.a2aproject.sdk.util.CollectionCopies;
import org.jspecify.annotations.Nullable;

/**
Expand Down Expand Up @@ -50,6 +51,9 @@ public record Artifact(String artifactId, @Nullable String name, @Nullable Strin
if (parts.isEmpty()) {
throw new IllegalArgumentException("Parts cannot be empty");
}
parts = CollectionCopies.immutableList(parts);
metadata = CollectionCopies.unmodifiableNullableShallowMap(metadata);
extensions = CollectionCopies.immutableNullableList(extensions);
}

/**
Expand Down Expand Up @@ -169,7 +173,7 @@ public Builder parts(List<Part<?>> parts) {
* @return this builder for method chaining
*/
public Builder parts(Part<?>... parts) {
this.parts = List.of(parts);
this.parts = CollectionCopies.immutableList(List.of(parts));
return this;
}

Expand All @@ -191,7 +195,7 @@ public Builder metadata(@Nullable Map<String, Object> metadata) {
* @return this builder for method chaining
*/
public Builder extensions(@Nullable List<String> extensions) {
this.extensions = (extensions == null) ? null : List.copyOf(extensions);
this.extensions = CollectionCopies.immutableNullableList(extensions);
return this;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import java.util.Map;

import org.a2aproject.sdk.util.Assert;
import org.a2aproject.sdk.util.CollectionCopies;

/**
* Configuration for the OAuth 2.0 Authorization Code flow.
Expand Down Expand Up @@ -42,5 +43,6 @@ public record AuthorizationCodeOAuthFlow(String authorizationUrl, String refresh
Assert.checkNotNullParam("authorizationUrl", authorizationUrl);
Assert.checkNotNullParam("scopes", scopes);
Assert.checkNotNullParam("tokenUrl", tokenUrl);
scopes = CollectionCopies.immutableMap(scopes);
}
}
12 changes: 7 additions & 5 deletions spec/src/main/java/org/a2aproject/sdk/spec/CancelTaskParams.java
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
package org.a2aproject.sdk.spec;

import org.a2aproject.sdk.util.Assert;
import java.util.Collections;
import java.util.Map;

import org.a2aproject.sdk.util.Assert;
import org.a2aproject.sdk.util.CollectionCopies;
import org.jspecify.annotations.Nullable;

/**
Expand All @@ -27,6 +28,7 @@ public record CancelTaskParams(String id, @Nullable String tenant, Map<String, O
*/
public CancelTaskParams {
Assert.checkNotNullParam("id", id);
metadata = CollectionCopies.unmodifiableShallowMap(metadata);
}

/**
Expand All @@ -35,7 +37,7 @@ public record CancelTaskParams(String id, @Nullable String tenant, Map<String, O
* @param id the task identifier (required)
*/
public CancelTaskParams(String id) {
this(id, null, Collections.emptyMap());
this(id, null, Map.of());
}

/**
Expand All @@ -53,7 +55,7 @@ public static Builder builder() {
public static class Builder {
private @Nullable String id;
private @Nullable String tenant;
private Map<String, Object> metadata = Collections.emptyMap();
private Map<String, Object> metadata = Map.of();

/**
* Creates a new Builder with all fields unset.
Expand Down Expand Up @@ -90,7 +92,7 @@ public Builder tenant(@Nullable String tenant) {
* @return this builder
*/
public Builder metadata(Map<String, Object> metadata) {
this.metadata = Map.copyOf(metadata);
this.metadata = CollectionCopies.unmodifiableShallowMap(metadata);
return this;
}
Comment thread
kabir marked this conversation as resolved.

Expand Down
Loading
Loading