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
Expand Up @@ -50,10 +50,8 @@ public TaskPushNotificationConfig setInfo(TaskPushNotificationConfig notificatio
@Override
public TaskPushNotificationConfig setInfo(TaskPushNotificationConfig notificationConfig, @Nullable String protocolVersion) {
String taskId = Assert.checkNotNullParam("taskId", notificationConfig.taskId());
// Ensure config has an ID - default to taskId if not provided (mirroring InMemoryPushNotificationConfigStore behavior)
if (notificationConfig.id().isEmpty()) {
// This means the taskId and configId are same. This will not allow having multiple configs for a single Task.
// The configId is a required field in the spec and should not be empty
// Default missing config IDs to the task ID, matching the in-memory store.
if (notificationConfig.id() == null || notificationConfig.id().isEmpty()) {
notificationConfig = TaskPushNotificationConfig.builder(notificationConfig).id(taskId).build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,24 @@ public void testSetInfoWithoutConfigId() {
assertEquals(updatedConfig.url(), configResult.configs().get(0).url());
}

@Test
@Transactional
public void testSetInfoWithNullConfigId() {
String taskId = "task_null_config_id";
TaskPushNotificationConfig config = TaskPushNotificationConfig.builder()
.url("http://null-id.url/callback")
.taskId(taskId)
.build();

TaskPushNotificationConfig result = configStore.setInfo(config);

assertEquals(taskId, result.id(), "A missing config ID should default to the task ID");
ListTaskPushNotificationConfigsResult configResult = configStore.getInfo(
new ListTaskPushNotificationConfigsParams(taskId));
assertEquals(1, configResult.configs().size());
assertEquals(taskId, configResult.configs().get(0).id());
}

@Test
@Transactional
public void testGetInfoExistingConfig() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -962,7 +962,8 @@ public TaskPushNotificationConfig onGetTaskPushNotificationConfig(
throw new InternalError("No push notification config found");
}

String configId = params.id();
String requestedConfigId = params.id();
String configId = requestedConfigId == null || requestedConfigId.isEmpty() ? params.taskId() : requestedConfigId;
return getTaskPushNotificationConfig(listTaskPushNotificationConfigsResult, configId);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,13 @@ public InMemoryPushNotificationConfigStore() {
public TaskPushNotificationConfig setInfo(TaskPushNotificationConfig notificationConfig) {
String taskId = Assert.checkNotNullParam("taskId", notificationConfig.taskId());
TaskPushNotificationConfig.Builder builder = TaskPushNotificationConfig.builder(notificationConfig);
if (notificationConfig.id().isEmpty()) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Not a comment about this code, but JpaDatabasePushNotificationConfigStore has the same latent NPE, and needs fixing too.

String requestedConfigId = notificationConfig.id();
boolean configIdIsMissing = requestedConfigId == null || requestedConfigId.isEmpty();
String configId = configIdIsMissing ? taskId : requestedConfigId;
if (configIdIsMissing) {
builder.id(taskId);
}
TaskPushNotificationConfig config = builder.build();
String configId = config.id();
int maxPerTask = PushNotificationConfigStore.maxPushConfigsPerTask(configProvider);

pushNotificationInfos.compute(taskId, (key, list) -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
import org.a2aproject.sdk.spec.CancelTaskParams;
import org.a2aproject.sdk.spec.Event;
import org.a2aproject.sdk.spec.EventKind;
import org.a2aproject.sdk.spec.GetTaskPushNotificationConfigParams;
import org.a2aproject.sdk.spec.InvalidParamsError;
import org.a2aproject.sdk.spec.Message;
import org.a2aproject.sdk.spec.MessageSendConfiguration;
Expand Down Expand Up @@ -983,6 +984,26 @@ void testVersionStored_OnCreateTaskPushNotificationConfig() throws Exception {
"Protocol version should be stored for the push notification config");
}

@Test
void testGetTaskPushNotificationConfigDefaultsMissingIdToTaskId() throws Exception {
String taskId = "get-default-config-id";
taskStore.save(Task.builder()
.id(taskId)
.contextId("ctx-get-default-config-id")
.status(new TaskStatus(TaskState.TASK_STATE_WORKING))
.build(), false);
requestHandler.onCreateTaskPushNotificationConfig(TaskPushNotificationConfig.builder()
.taskId(taskId)
.url("http://example.com/get-default-config-id")
.build(), NULL_CONTEXT);

TaskPushNotificationConfig result = requestHandler.onGetTaskPushNotificationConfig(
new GetTaskPushNotificationConfigParams(taskId), NULL_CONTEXT);

assertEquals(taskId, result.id());
assertEquals("http://example.com/get-default-config-id", result.url());
}

/**
* Verify that onMessageSend stores the protocol version when the request
* includes a push notification config (new task path).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,10 +149,10 @@ public void testSetInfoAppendsToExistingConfig() {
}

@Test
public void testSetInfoWithoutConfigId() {
public void testSetInfoWithEmptyConfigId() {
String taskId = "task1";
TaskPushNotificationConfig initialConfig = TaskPushNotificationConfig.builder()
.id("") // No ID set

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Please create a new test rather than changing this existing one.

Removing .id("") repurposes this test from the empty-string path to the null path rather than covering both.
The empty-string case is the one production actually hits — the mapper doesn't apply emptyToNull to id, so at runtime a missing id arrives as "", never null.
Recommend keeping the "" case here and adding a separate null-id test (or parameterizing over both), so the load-bearing branch stays asserted.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The previous comment should still be addressed.

.id("")
.url("http://initial.url/callback")
.taskId(taskId)
.build();
Expand All @@ -165,7 +165,7 @@ public void testSetInfoWithoutConfigId() {
assertEquals(taskId, configResult.configs().get(0).id());

TaskPushNotificationConfig updatedConfig = TaskPushNotificationConfig.builder()
.id("") // No ID set

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Please create a new test rather than changing this existing one.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The previous comment should still be addressed.

.id("")
.url("http://initial.url/callback_new")
.taskId(taskId)
.build();
Expand All @@ -178,6 +178,19 @@ public void testSetInfoWithoutConfigId() {
assertEquals(updatedConfig.url(), configResult.configs().get(0).url());
}

@Test
public void testSetInfoWithNullConfigId() {
String taskId = "task_with_null_config_id";
TaskPushNotificationConfig config = TaskPushNotificationConfig.builder()
.url("http://initial.url/callback")
.taskId(taskId)
.build();

TaskPushNotificationConfig result = configStore.setInfo(config);

assertEquals(taskId, result.id(), "Config ID should default to taskId when null");
}

@Test
public void testGetInfoExistingConfig() {
String taskId = "task_get_exist";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,27 @@
* @see TaskPushNotificationConfig for the returned configuration structure
* @see <a href="https://a2a-protocol.org/latest/">A2A Protocol Specification</a>
*/
public record GetTaskPushNotificationConfigParams(String taskId, String id, @Nullable String tenant) {
public record GetTaskPushNotificationConfigParams(String taskId, @Nullable String id, @Nullable String tenant) {

/**
* Compact constructor that validates required fields.
*
* @param taskId the taskId parameter (see class-level JavaDoc)
* @param id the id parameter (see class-level JavaDoc)
* @param tenant the tenant parameter (see class-level JavaDoc)
* @throws IllegalArgumentException if taskId or tenant is null
* @throws IllegalArgumentException if taskId is null
*/
public GetTaskPushNotificationConfigParams {
Assert.checkNotNullParam("taskId", taskId);
Assert.checkNotNullParam("id", id);
}

/**
* Convenience constructor for retrieving the configuration that uses the task ID as its default ID.
*
* @param taskId the task identifier (required)
*/
public GetTaskPushNotificationConfigParams(String taskId) {
this(taskId, null, null);
}

/**
Expand All @@ -38,7 +46,7 @@ public record GetTaskPushNotificationConfigParams(String taskId, String id, @Nul
* @param taskId the task identifier (required)
* @param id optional configuration ID to retrieve
*/
public GetTaskPushNotificationConfigParams(String taskId, String id) {
public GetTaskPushNotificationConfigParams(String taskId, @Nullable String id) {
this(taskId, id, null);
}

Expand Down Expand Up @@ -82,7 +90,7 @@ public Builder taskId(String taskId) {
* @param id the configuration ID
* @return this builder for method chaining
*/
public Builder id(String id) {
public Builder id(@Nullable String id) {
this.id = id;
return this;
}
Expand All @@ -106,7 +114,7 @@ public Builder tenant(@Nullable String tenant) {
public GetTaskPushNotificationConfigParams build() {
return new GetTaskPushNotificationConfigParams(
Assert.checkNotNullParam("taskId", taskId),
Assert.checkNotNullParam("id", id),
id,
tenant);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
* Used for managing task-specific push notification settings via the push notification
* management methods ({@code tasks/pushNotificationConfig/set}, {@code tasks/pushNotificationConfig/get}, etc.).
*
* @param id unique identifier (e.g. UUID) for this push notification configuration
* @param id optional unique identifier (e.g. UUID) for this push notification configuration.
* When omitted while creating a configuration, the server assigns one.
* @param taskId the unique identifier of the task to receive push notifications for
* @param url the HTTP/HTTPS endpoint URL to receive push notifications (required)
* @param token optional bearer token for simple authentication
Expand All @@ -30,22 +31,21 @@
* @see MessageSendConfiguration for configuring push notifications on message send
* @see <a href="https://a2a-protocol.org/latest/">A2A Protocol Specification</a>
*/
public record TaskPushNotificationConfig(String id, @Nullable String taskId, String url, @Nullable String token,
public record TaskPushNotificationConfig(@Nullable String id, @Nullable String taskId, String url, @Nullable String token,
@Nullable AuthenticationInfo authentication, @Nullable String tenant) {

/**
* Compact constructor for validation.
* Validates that required parameters are not null.
*
* @param id the configuration identifier
* @param id the optional configuration identifier
* @param taskId the task identifier
* @param url the notification endpoint URL
* @param token optional bearer token
* @param authentication optional authentication info
* @param tenant the tenant identifier
*/
public TaskPushNotificationConfig {
Assert.checkNotNullParam("id", id);
Assert.checkNotNullParam("url", url);
}

Expand Down Expand Up @@ -103,10 +103,10 @@ private Builder(TaskPushNotificationConfig config) {
/**
* Sets the configuration identifier.
*
* @param id the configuration ID
* @param id the optional configuration ID
* @return this builder
*/
public Builder id(String id) {
public Builder id(@Nullable String id) {
this.id = id;
return this;
}
Expand Down Expand Up @@ -170,11 +170,11 @@ public Builder tenant(String tenant) {
* Builds the {@link TaskPushNotificationConfig}.
*
* @return a new push notification configuration
* @throws IllegalArgumentException if id or url is null
* @throws IllegalArgumentException if url is null
*/
public TaskPushNotificationConfig build() {
return new TaskPushNotificationConfig(
Assert.checkNotNullParam("id", id),
id,
taskId,
Assert.checkNotNullParam("url", url),
token,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package org.a2aproject.sdk.spec;

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

import org.junit.jupiter.api.Test;

class GetTaskPushNotificationConfigParamsTest {

@Test
void testConstructionAllowsOmittedConfigurationId() {
GetTaskPushNotificationConfigParams params = new GetTaskPushNotificationConfigParams("task-1");

assertEquals("task-1", params.taskId());
assertNull(params.id());
}

@Test
void testBuilderAllowsOmittedConfigurationId() {
GetTaskPushNotificationConfigParams params = GetTaskPushNotificationConfigParams.builder()
.taskId("task-1")
.build();

assertNull(params.id());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package org.a2aproject.sdk.spec;

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

import org.junit.jupiter.api.Test;

class TaskPushNotificationConfigTest {

@Test
void builderAllowsAnOmittedConfigurationId() {
TaskPushNotificationConfig config = TaskPushNotificationConfig.builder()
.taskId("task-123")
.url("https://example.com/callback")
.build();

assertNull(config.id());
assertEquals("task-123", config.taskId());
}
}