From 78494f1ca332cfc664843cdc5aa6918661af4130 Mon Sep 17 00:00:00 2001
From: hutiefang76 <137664623+hutiefang76@users.noreply.github.com>
Date: Wed, 26 Aug 2026 00:15:03 +0800
Subject: [PATCH 1/6] fix: allow omitted task push config id
Fixes #1081
---
.../InMemoryPushNotificationConfigStore.java | 6 ++++--
...MemoryPushNotificationConfigStoreTest.java | 2 --
.../sdk/spec/TaskPushNotificationConfig.java | 16 +++++++--------
.../spec/TaskPushNotificationConfigTest.java | 20 +++++++++++++++++++
4 files changed, 32 insertions(+), 12 deletions(-)
create mode 100644 spec/src/test/java/org/a2aproject/sdk/spec/TaskPushNotificationConfigTest.java
diff --git a/server-common/src/main/java/org/a2aproject/sdk/server/tasks/InMemoryPushNotificationConfigStore.java b/server-common/src/main/java/org/a2aproject/sdk/server/tasks/InMemoryPushNotificationConfigStore.java
index 854bd7fbd..403b99c0e 100644
--- a/server-common/src/main/java/org/a2aproject/sdk/server/tasks/InMemoryPushNotificationConfigStore.java
+++ b/server-common/src/main/java/org/a2aproject/sdk/server/tasks/InMemoryPushNotificationConfigStore.java
@@ -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()) {
+ 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) -> {
diff --git a/server-common/src/test/java/org/a2aproject/sdk/server/tasks/InMemoryPushNotificationConfigStoreTest.java b/server-common/src/test/java/org/a2aproject/sdk/server/tasks/InMemoryPushNotificationConfigStoreTest.java
index 9a1a0f706..11adac3c6 100644
--- a/server-common/src/test/java/org/a2aproject/sdk/server/tasks/InMemoryPushNotificationConfigStoreTest.java
+++ b/server-common/src/test/java/org/a2aproject/sdk/server/tasks/InMemoryPushNotificationConfigStoreTest.java
@@ -154,7 +154,6 @@ public void testSetInfoAppendsToExistingConfig() {
public void testSetInfoWithoutConfigId() {
String taskId = "task1";
TaskPushNotificationConfig initialConfig = TaskPushNotificationConfig.builder()
- .id("") // No ID set
.url("http://initial.url/callback")
.taskId(taskId)
.build();
@@ -167,7 +166,6 @@ public void testSetInfoWithoutConfigId() {
assertEquals(taskId, configResult.configs().get(0).id());
TaskPushNotificationConfig updatedConfig = TaskPushNotificationConfig.builder()
- .id("") // No ID set
.url("http://initial.url/callback_new")
.taskId(taskId)
.build();
diff --git a/spec/src/main/java/org/a2aproject/sdk/spec/TaskPushNotificationConfig.java b/spec/src/main/java/org/a2aproject/sdk/spec/TaskPushNotificationConfig.java
index dc3fa7a0f..e98924bee 100644
--- a/spec/src/main/java/org/a2aproject/sdk/spec/TaskPushNotificationConfig.java
+++ b/spec/src/main/java/org/a2aproject/sdk/spec/TaskPushNotificationConfig.java
@@ -21,7 +21,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
@@ -31,14 +32,14 @@
* @see MessageSendConfiguration for configuring push notifications on message send
* @see A2A Protocol Specification
*/
-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
@@ -46,7 +47,6 @@ public record TaskPushNotificationConfig(String id, @Nullable String taskId, Str
* @param tenant the tenant identifier
*/
public TaskPushNotificationConfig {
- Assert.checkNotNullParam("id", id);
Assert.checkNotNullParam("url", url);
Utils.validateTenant(tenant);
}
@@ -105,10 +105,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;
}
@@ -172,11 +172,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,
diff --git a/spec/src/test/java/org/a2aproject/sdk/spec/TaskPushNotificationConfigTest.java b/spec/src/test/java/org/a2aproject/sdk/spec/TaskPushNotificationConfigTest.java
new file mode 100644
index 000000000..bf5216182
--- /dev/null
+++ b/spec/src/test/java/org/a2aproject/sdk/spec/TaskPushNotificationConfigTest.java
@@ -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());
+ }
+}
From 483868d7c075649c4b195e733b10f7cb07e68779 Mon Sep 17 00:00:00 2001
From: hutiefang76 <137664623+hutiefang76@users.noreply.github.com>
Date: Wed, 26 Aug 2026 20:44:59 +0800
Subject: [PATCH 2/6] fix: complete optional push config id handling
---
...paDatabasePushNotificationConfigStore.java | 6 ++---
.../JpaPushNotificationConfigStoreTest.java | 18 +++++++++++++
.../DefaultRequestHandler.java | 3 ++-
.../DefaultRequestHandlerTest.java | 21 +++++++++++++++
.../GetTaskPushNotificationConfigParams.java | 22 +++++++++++-----
...tTaskPushNotificationConfigParamsTest.java | 26 +++++++++++++++++++
6 files changed, 85 insertions(+), 11 deletions(-)
create mode 100644 spec/src/test/java/org/a2aproject/sdk/spec/GetTaskPushNotificationConfigParamsTest.java
diff --git a/extras/push-notification-config-store-database-jpa/src/main/java/org/a2aproject/sdk/extras/pushnotificationconfigstore/database/jpa/JpaDatabasePushNotificationConfigStore.java b/extras/push-notification-config-store-database-jpa/src/main/java/org/a2aproject/sdk/extras/pushnotificationconfigstore/database/jpa/JpaDatabasePushNotificationConfigStore.java
index dee737d4b..2b2f3eb2e 100644
--- a/extras/push-notification-config-store-database-jpa/src/main/java/org/a2aproject/sdk/extras/pushnotificationconfigstore/database/jpa/JpaDatabasePushNotificationConfigStore.java
+++ b/extras/push-notification-config-store-database-jpa/src/main/java/org/a2aproject/sdk/extras/pushnotificationconfigstore/database/jpa/JpaDatabasePushNotificationConfigStore.java
@@ -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();
}
diff --git a/extras/push-notification-config-store-database-jpa/src/test/java/org/a2aproject/sdk/extras/pushnotificationconfigstore/database/jpa/JpaPushNotificationConfigStoreTest.java b/extras/push-notification-config-store-database-jpa/src/test/java/org/a2aproject/sdk/extras/pushnotificationconfigstore/database/jpa/JpaPushNotificationConfigStoreTest.java
index 089a418b7..9da2b36bf 100644
--- a/extras/push-notification-config-store-database-jpa/src/test/java/org/a2aproject/sdk/extras/pushnotificationconfigstore/database/jpa/JpaPushNotificationConfigStoreTest.java
+++ b/extras/push-notification-config-store-database-jpa/src/test/java/org/a2aproject/sdk/extras/pushnotificationconfigstore/database/jpa/JpaPushNotificationConfigStoreTest.java
@@ -163,6 +163,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() {
diff --git a/server-common/src/main/java/org/a2aproject/sdk/server/requesthandlers/DefaultRequestHandler.java b/server-common/src/main/java/org/a2aproject/sdk/server/requesthandlers/DefaultRequestHandler.java
index d234e7764..bdb540000 100644
--- a/server-common/src/main/java/org/a2aproject/sdk/server/requesthandlers/DefaultRequestHandler.java
+++ b/server-common/src/main/java/org/a2aproject/sdk/server/requesthandlers/DefaultRequestHandler.java
@@ -1126,7 +1126,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);
}
diff --git a/server-common/src/test/java/org/a2aproject/sdk/server/requesthandlers/DefaultRequestHandlerTest.java b/server-common/src/test/java/org/a2aproject/sdk/server/requesthandlers/DefaultRequestHandlerTest.java
index da0603961..fa9bf6814 100644
--- a/server-common/src/test/java/org/a2aproject/sdk/server/requesthandlers/DefaultRequestHandlerTest.java
+++ b/server-common/src/test/java/org/a2aproject/sdk/server/requesthandlers/DefaultRequestHandlerTest.java
@@ -45,6 +45,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.ListTasksParams;
import org.a2aproject.sdk.spec.ListTaskPushNotificationConfigsParams;
@@ -989,6 +990,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).
diff --git a/spec/src/main/java/org/a2aproject/sdk/spec/GetTaskPushNotificationConfigParams.java b/spec/src/main/java/org/a2aproject/sdk/spec/GetTaskPushNotificationConfigParams.java
index eeb950cd8..38d0c31cb 100644
--- a/spec/src/main/java/org/a2aproject/sdk/spec/GetTaskPushNotificationConfigParams.java
+++ b/spec/src/main/java/org/a2aproject/sdk/spec/GetTaskPushNotificationConfigParams.java
@@ -18,7 +18,7 @@
* @see TaskPushNotificationConfig for the returned configuration structure
* @see A2A Protocol Specification
*/
-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.
@@ -26,21 +26,31 @@ public record GetTaskPushNotificationConfigParams(String taskId, String id, @Nul
* @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);
Utils.validateTenant(tenant);
}
+ }
+
+ /**
+ * 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);
+ }
+
/**
* Convenience constructor for creating parameters without tenant.
*
* @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);
}
@@ -84,7 +94,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;
}
@@ -108,7 +118,7 @@ public Builder tenant(@Nullable String tenant) {
public GetTaskPushNotificationConfigParams build() {
return new GetTaskPushNotificationConfigParams(
Assert.checkNotNullParam("taskId", taskId),
- Assert.checkNotNullParam("id", id),
+ id,
tenant);
}
}
diff --git a/spec/src/test/java/org/a2aproject/sdk/spec/GetTaskPushNotificationConfigParamsTest.java b/spec/src/test/java/org/a2aproject/sdk/spec/GetTaskPushNotificationConfigParamsTest.java
new file mode 100644
index 000000000..030bf209e
--- /dev/null
+++ b/spec/src/test/java/org/a2aproject/sdk/spec/GetTaskPushNotificationConfigParamsTest.java
@@ -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());
+ }
+}
From e4a3d3711ca91cbae1fa1744482faa408187d370 Mon Sep 17 00:00:00 2001
From: hutiefang76 <137664623+hutiefang76@users.noreply.github.com>
Date: Wed, 26 Aug 2026 23:07:39 +0800
Subject: [PATCH 3/6] test: cover empty and null push config ids
---
...InMemoryPushNotificationConfigStoreTest.java | 17 ++++++++++++++++-
.../GetTaskPushNotificationConfigParams.java | 2 --
2 files changed, 16 insertions(+), 3 deletions(-)
diff --git a/server-common/src/test/java/org/a2aproject/sdk/server/tasks/InMemoryPushNotificationConfigStoreTest.java b/server-common/src/test/java/org/a2aproject/sdk/server/tasks/InMemoryPushNotificationConfigStoreTest.java
index 11adac3c6..7adbe6e31 100644
--- a/server-common/src/test/java/org/a2aproject/sdk/server/tasks/InMemoryPushNotificationConfigStoreTest.java
+++ b/server-common/src/test/java/org/a2aproject/sdk/server/tasks/InMemoryPushNotificationConfigStoreTest.java
@@ -151,9 +151,10 @@ public void testSetInfoAppendsToExistingConfig() {
}
@Test
- public void testSetInfoWithoutConfigId() {
+ public void testSetInfoWithEmptyConfigId() {
String taskId = "task1";
TaskPushNotificationConfig initialConfig = TaskPushNotificationConfig.builder()
+ .id("")
.url("http://initial.url/callback")
.taskId(taskId)
.build();
@@ -166,6 +167,7 @@ public void testSetInfoWithoutConfigId() {
assertEquals(taskId, configResult.configs().get(0).id());
TaskPushNotificationConfig updatedConfig = TaskPushNotificationConfig.builder()
+ .id("")
.url("http://initial.url/callback_new")
.taskId(taskId)
.build();
@@ -178,6 +180,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";
diff --git a/spec/src/main/java/org/a2aproject/sdk/spec/GetTaskPushNotificationConfigParams.java b/spec/src/main/java/org/a2aproject/sdk/spec/GetTaskPushNotificationConfigParams.java
index 38d0c31cb..584b7de23 100644
--- a/spec/src/main/java/org/a2aproject/sdk/spec/GetTaskPushNotificationConfigParams.java
+++ b/spec/src/main/java/org/a2aproject/sdk/spec/GetTaskPushNotificationConfigParams.java
@@ -33,8 +33,6 @@ public record GetTaskPushNotificationConfigParams(String taskId, @Nullable Strin
Utils.validateTenant(tenant);
}
- }
-
/**
* Convenience constructor for retrieving the configuration that uses the task ID as its default ID.
*
From 1c21dcd4df582d68a70e688dc0cbfa382b9f1d17 Mon Sep 17 00:00:00 2001
From: hutiefang76 <137664623+hutiefang76@users.noreply.github.com>
Date: Sat, 29 Aug 2026 08:58:37 +0800
Subject: [PATCH 4/6] Reject duplicate push configs with omitted IDs
Signed-off-by: hutiefang76 <137664623+hutiefang76@users.noreply.github.com>
---
...JpaDatabasePushNotificationConfigStore.java | 8 +++++++-
.../JpaPushNotificationConfigStoreTest.java | 18 +++++++++++++-----
.../InMemoryPushNotificationConfigStore.java | 6 ++++++
.../tasks/PushNotificationConfigStore.java | 7 +++++--
...nMemoryPushNotificationConfigStoreTest.java | 16 +++++++++++-----
5 files changed, 42 insertions(+), 13 deletions(-)
diff --git a/extras/push-notification-config-store-database-jpa/src/main/java/org/a2aproject/sdk/extras/pushnotificationconfigstore/database/jpa/JpaDatabasePushNotificationConfigStore.java b/extras/push-notification-config-store-database-jpa/src/main/java/org/a2aproject/sdk/extras/pushnotificationconfigstore/database/jpa/JpaDatabasePushNotificationConfigStore.java
index 2b2f3eb2e..0fa606211 100644
--- a/extras/push-notification-config-store-database-jpa/src/main/java/org/a2aproject/sdk/extras/pushnotificationconfigstore/database/jpa/JpaDatabasePushNotificationConfigStore.java
+++ b/extras/push-notification-config-store-database-jpa/src/main/java/org/a2aproject/sdk/extras/pushnotificationconfigstore/database/jpa/JpaDatabasePushNotificationConfigStore.java
@@ -51,7 +51,8 @@ public TaskPushNotificationConfig setInfo(TaskPushNotificationConfig notificatio
public TaskPushNotificationConfig setInfo(TaskPushNotificationConfig notificationConfig, @Nullable String protocolVersion) {
String taskId = Assert.checkNotNullParam("taskId", notificationConfig.taskId());
// Default missing config IDs to the task ID, matching the in-memory store.
- if (notificationConfig.id() == null || notificationConfig.id().isEmpty()) {
+ boolean configIdIsMissing = notificationConfig.id() == null || notificationConfig.id().isEmpty();
+ if (configIdIsMissing) {
notificationConfig = TaskPushNotificationConfig.builder(notificationConfig).id(taskId).build();
}
@@ -63,6 +64,11 @@ public TaskPushNotificationConfig setInfo(TaskPushNotificationConfig notificatio
// Check if entity already exists
JpaPushNotificationConfig existingJpaConfig = em.find(JpaPushNotificationConfig.class, configId);
+ if (configIdIsMissing && existingJpaConfig != null) {
+ throw new InvalidParamsError("A push notification config with the default ID already exists for task "
+ + taskId + "; specify the config ID explicitly to update it");
+ }
+
if (existingJpaConfig != null) {
// Update existing entity
existingJpaConfig.setConfig(notificationConfig);
diff --git a/extras/push-notification-config-store-database-jpa/src/test/java/org/a2aproject/sdk/extras/pushnotificationconfigstore/database/jpa/JpaPushNotificationConfigStoreTest.java b/extras/push-notification-config-store-database-jpa/src/test/java/org/a2aproject/sdk/extras/pushnotificationconfigstore/database/jpa/JpaPushNotificationConfigStoreTest.java
index 9da2b36bf..3e9a5b207 100644
--- a/extras/push-notification-config-store-database-jpa/src/test/java/org/a2aproject/sdk/extras/pushnotificationconfigstore/database/jpa/JpaPushNotificationConfigStoreTest.java
+++ b/extras/push-notification-config-store-database-jpa/src/test/java/org/a2aproject/sdk/extras/pushnotificationconfigstore/database/jpa/JpaPushNotificationConfigStoreTest.java
@@ -5,6 +5,7 @@
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.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.never;
@@ -21,6 +22,7 @@
import org.a2aproject.sdk.server.tasks.BasePushNotificationSender;
import org.a2aproject.sdk.server.tasks.PushNotificationConfigStore;
import org.a2aproject.sdk.server.tasks.PushNotificationUrlValidator;
+import org.a2aproject.sdk.spec.InvalidParamsError;
import org.a2aproject.sdk.spec.ListTaskPushNotificationConfigsParams;
import org.a2aproject.sdk.spec.ListTaskPushNotificationConfigsResult;
import org.a2aproject.sdk.spec.Task;
@@ -149,18 +151,17 @@ public void testSetInfoWithoutConfigId() {
assertEquals(1, configResult.configs().size());
assertEquals(taskId, configResult.configs().get(0).id());
- TaskPushNotificationConfig updatedConfig = TaskPushNotificationConfig.builder()
+ TaskPushNotificationConfig duplicateConfig = TaskPushNotificationConfig.builder()
.id("")
.url("http://initial.url/callback_new")
.taskId(taskId)
.build();
- TaskPushNotificationConfig updatedResult = configStore.setInfo(updatedConfig);
- assertEquals(taskId, updatedResult.id());
+ assertThrows(InvalidParamsError.class, () -> configStore.setInfo(duplicateConfig));
configResult = configStore.getInfo(new ListTaskPushNotificationConfigsParams(taskId));
- assertEquals(1, configResult.configs().size(), "Should replace existing config with same ID rather than adding new one");
- assertEquals(updatedConfig.url(), configResult.configs().get(0).url());
+ assertEquals(1, configResult.configs().size());
+ assertEquals(initialConfig.url(), configResult.configs().get(0).url());
}
@Test
@@ -179,6 +180,13 @@ public void testSetInfoWithNullConfigId() {
new ListTaskPushNotificationConfigsParams(taskId));
assertEquals(1, configResult.configs().size());
assertEquals(taskId, configResult.configs().get(0).id());
+
+ TaskPushNotificationConfig duplicateConfig = TaskPushNotificationConfig.builder()
+ .url("http://updated.url/callback")
+ .taskId(taskId)
+ .build();
+
+ assertThrows(InvalidParamsError.class, () -> configStore.setInfo(duplicateConfig));
}
@Test
diff --git a/server-common/src/main/java/org/a2aproject/sdk/server/tasks/InMemoryPushNotificationConfigStore.java b/server-common/src/main/java/org/a2aproject/sdk/server/tasks/InMemoryPushNotificationConfigStore.java
index 403b99c0e..c90d33014 100644
--- a/server-common/src/main/java/org/a2aproject/sdk/server/tasks/InMemoryPushNotificationConfigStore.java
+++ b/server-common/src/main/java/org/a2aproject/sdk/server/tasks/InMemoryPushNotificationConfigStore.java
@@ -59,6 +59,12 @@ public TaskPushNotificationConfig setInfo(TaskPushNotificationConfig notificatio
pushNotificationInfos.compute(taskId, (key, list) -> {
List
- * If {@code notificationConfig.id()} is null or empty, it's set to the task ID.
- * If a config with the same ID already exists for this task, it's replaced.
+ * If {@code notificationConfig.id()} is null or empty, the store creates the
+ * default config with the task ID. Omitting the ID is a create-only shorthand:
+ * if the default config already exists, the store rejects the request instead
+ * of silently replacing it. To update the default config, provide the task ID
+ * explicitly. Configurations beyond the default one must always provide an ID.
*