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 mutable = list == null ? new ArrayList<>() : new ArrayList<>(list); + boolean defaultConfigAlreadyExists = configIdIsMissing + && mutable.stream().anyMatch(existing -> existing.id() != null && existing.id().equals(configId)); + if (defaultConfigAlreadyExists) { + throw new InvalidParamsError("A push notification config with the default ID already exists for task " + + taskId + "; specify the config ID explicitly to update it"); + } boolean isExistingConfig = mutable.removeIf( existing -> existing.id() != null && existing.id().equals(configId)); if (!isExistingConfig && mutable.size() >= maxPerTask) { diff --git a/server-common/src/main/java/org/a2aproject/sdk/server/tasks/PushNotificationConfigStore.java b/server-common/src/main/java/org/a2aproject/sdk/server/tasks/PushNotificationConfigStore.java index 64138f3aa..c76028f8b 100644 --- a/server-common/src/main/java/org/a2aproject/sdk/server/tasks/PushNotificationConfigStore.java +++ b/server-common/src/main/java/org/a2aproject/sdk/server/tasks/PushNotificationConfigStore.java @@ -88,8 +88,11 @@ public interface PushNotificationConfigStore { /** * Sets or updates the push notification configuration for a task. *

- * 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. *

* * @param notificationConfig the task push notification configuration 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 7adbe6e31..744ff8b7a 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 @@ -166,18 +166,17 @@ public void testSetInfoWithEmptyConfigId() { 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 @@ -191,6 +190,13 @@ public void testSetInfoWithNullConfigId() { TaskPushNotificationConfig result = configStore.setInfo(config); assertEquals(taskId, result.id(), "Config ID should default to taskId when null"); + + TaskPushNotificationConfig duplicateConfig = TaskPushNotificationConfig.builder() + .url("http://updated.url/callback") + .taskId(taskId) + .build(); + + assertThrows(InvalidParamsError.class, () -> configStore.setInfo(duplicateConfig)); } @Test From bd875377aed28559e48afd4f48a803d85dd70978 Mon Sep 17 00:00:00 2001 From: hutiefang76 <137664623+hutiefang76@users.noreply.github.com> Date: Sun, 30 Aug 2026 20:57:13 +0800 Subject: [PATCH 5/6] Preserve v0.3 push config update semantics Signed-off-by: hutiefang76 <137664623+hutiefang76@users.noreply.github.com> --- .../conversion/TestUtilsBean_v0_3.java | 3 ++- ...paDatabasePushNotificationConfigStore.java | 2 +- .../JpaPushNotificationConfigStoreTest.java | 23 +++++++++++++++++++ .../InMemoryPushNotificationConfigStore.java | 14 ++++++++--- .../tasks/PushNotificationConfigStore.java | 1 + ...MemoryPushNotificationConfigStoreTest.java | 22 ++++++++++++++++++ 6 files changed, 60 insertions(+), 5 deletions(-) diff --git a/compat-0.3/server-conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/TestUtilsBean_v0_3.java b/compat-0.3/server-conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/TestUtilsBean_v0_3.java index 6a43e444c..3800880d4 100644 --- a/compat-0.3/server-conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/TestUtilsBean_v0_3.java +++ b/compat-0.3/server-conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/TestUtilsBean_v0_3.java @@ -64,7 +64,8 @@ public void deleteTaskPushNotificationConfig(String taskId, String configId) { } public void saveTaskPushNotificationConfig(String taskId, TaskPushNotificationConfig notificationConfig) { - pushNotificationConfigStore.setInfo(TaskPushNotificationConfig.builder(notificationConfig).taskId(taskId).build()); + pushNotificationConfigStore.setInfo( + TaskPushNotificationConfig.builder(notificationConfig).taskId(taskId).build(), "0.3"); } /** 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 0fa606211..424291974 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 @@ -64,7 +64,7 @@ public TaskPushNotificationConfig setInfo(TaskPushNotificationConfig notificatio // Check if entity already exists JpaPushNotificationConfig existingJpaConfig = em.find(JpaPushNotificationConfig.class, configId); - if (configIdIsMissing && existingJpaConfig != null) { + if (configIdIsMissing && existingJpaConfig != null && !"0.3".equals(resolvedVersion)) { throw new InvalidParamsError("A push notification config with the default ID already exists for task " + taskId + "; specify the config ID explicitly to update it"); } 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 3e9a5b207..4c96f50f7 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 @@ -189,6 +189,29 @@ public void testSetInfoWithNullConfigId() { assertThrows(InvalidParamsError.class, () -> configStore.setInfo(duplicateConfig)); } + @Test + @Transactional + public void testSetInfoAllowsV03DefaultConfigUpdate() { + String taskId = "task_v03_default_update"; + TaskPushNotificationConfig initialConfig = TaskPushNotificationConfig.builder() + .taskId(taskId) + .url("http://initial.url/callback") + .build(); + TaskPushNotificationConfig updatedConfig = TaskPushNotificationConfig.builder() + .taskId(taskId) + .url("http://updated.url/callback") + .build(); + + configStore.setInfo(initialConfig, "0.3"); + TaskPushNotificationConfig result = configStore.setInfo(updatedConfig, "0.3"); + + assertEquals(taskId, result.id()); + ListTaskPushNotificationConfigsResult stored = + configStore.getInfo(new ListTaskPushNotificationConfigsParams(taskId)); + assertEquals(1, stored.configs().size()); + assertEquals(updatedConfig.url(), stored.configs().get(0).url()); + } + @Test @Transactional public void testGetInfoExistingConfig() { 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 c90d33014..acbe29b1c 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 @@ -46,6 +46,12 @@ public InMemoryPushNotificationConfigStore() { @Override public TaskPushNotificationConfig setInfo(TaskPushNotificationConfig notificationConfig) { + return setInfo(notificationConfig, false); + } + + private TaskPushNotificationConfig setInfo( + TaskPushNotificationConfig notificationConfig, + boolean allowDefaultConfigUpdate) { String taskId = Assert.checkNotNullParam("taskId", notificationConfig.taskId()); TaskPushNotificationConfig.Builder builder = TaskPushNotificationConfig.builder(notificationConfig); String requestedConfigId = notificationConfig.id(); @@ -61,7 +67,7 @@ public TaskPushNotificationConfig setInfo(TaskPushNotificationConfig notificatio List mutable = list == null ? new ArrayList<>() : new ArrayList<>(list); boolean defaultConfigAlreadyExists = configIdIsMissing && mutable.stream().anyMatch(existing -> existing.id() != null && existing.id().equals(configId)); - if (defaultConfigAlreadyExists) { + if (defaultConfigAlreadyExists && !allowDefaultConfigUpdate) { throw new InvalidParamsError("A push notification config with the default ID already exists for task " + taskId + "; specify the config ID explicitly to update it"); } @@ -79,8 +85,10 @@ public TaskPushNotificationConfig setInfo(TaskPushNotificationConfig notificatio @Override public TaskPushNotificationConfig setInfo(TaskPushNotificationConfig config, @Nullable String protocolVersion) { - TaskPushNotificationConfig result = setInfo(config); - protocolVersions.put(result.taskId() + ":" + result.id(), PushNotificationConfigStore.resolveProtocolVersion(protocolVersion)); + TaskPushNotificationConfig result = setInfo(config, "0.3".equals(protocolVersion)); + protocolVersions.put( + result.taskId() + ":" + result.id(), + PushNotificationConfigStore.resolveProtocolVersion(protocolVersion)); return result; } diff --git a/server-common/src/main/java/org/a2aproject/sdk/server/tasks/PushNotificationConfigStore.java b/server-common/src/main/java/org/a2aproject/sdk/server/tasks/PushNotificationConfigStore.java index c76028f8b..e70e246e6 100644 --- a/server-common/src/main/java/org/a2aproject/sdk/server/tasks/PushNotificationConfigStore.java +++ b/server-common/src/main/java/org/a2aproject/sdk/server/tasks/PushNotificationConfigStore.java @@ -93,6 +93,7 @@ public interface PushNotificationConfigStore { * 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. + * The v0.3 compatibility path keeps its historical single-config update behavior. *

* * @param notificationConfig the task push notification configuration 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 744ff8b7a..5217fa889 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 @@ -199,6 +199,28 @@ public void testSetInfoWithNullConfigId() { assertThrows(InvalidParamsError.class, () -> configStore.setInfo(duplicateConfig)); } + @Test + public void testSetInfoAllowsV03DefaultConfigUpdate() { + String taskId = "task_v03_default_update"; + TaskPushNotificationConfig initialConfig = TaskPushNotificationConfig.builder() + .taskId(taskId) + .url("http://initial.url/callback") + .build(); + TaskPushNotificationConfig updatedConfig = TaskPushNotificationConfig.builder() + .taskId(taskId) + .url("http://updated.url/callback") + .build(); + + configStore.setInfo(initialConfig, "0.3"); + TaskPushNotificationConfig result = configStore.setInfo(updatedConfig, "0.3"); + + assertEquals(taskId, result.id()); + ListTaskPushNotificationConfigsResult stored = + configStore.getInfo(new ListTaskPushNotificationConfigsParams(taskId)); + assertEquals(1, stored.configs().size()); + assertEquals(updatedConfig.url(), stored.configs().get(0).url()); + } + @Test public void testGetInfoExistingConfig() { String taskId = "task_get_exist"; From 05422c57037f94f10008d2e2c48d06d306c45aae Mon Sep 17 00:00:00 2001 From: hutiefang76 <137664623+hutiefang76@users.noreply.github.com> Date: Sun, 30 Aug 2026 21:40:44 +0800 Subject: [PATCH 6/6] Test v0.3 push config setup in multiversion servers Signed-off-by: hutiefang76 <137664623+hutiefang76@users.noreply.github.com> --- .../conversion/AbstractA2AServerServerTest_v0_3.java | 1 + .../sdk/tests/multiversion/grpc/A2ATestResource.java | 8 ++++++-- .../sdk/tests/multiversion/jsonrpc/A2ATestRoutes.java | 3 ++- .../sdk/tests/multiversion/rest/A2ATestRoutes.java | 3 ++- .../sdk/server/apps/common/TestUtilsBean.java | 11 ++++++++++- 5 files changed, 21 insertions(+), 5 deletions(-) diff --git a/compat-0.3/server-conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/AbstractA2AServerServerTest_v0_3.java b/compat-0.3/server-conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/AbstractA2AServerServerTest_v0_3.java index d1f622402..6cf582458 100644 --- a/compat-0.3/server-conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/AbstractA2AServerServerTest_v0_3.java +++ b/compat-0.3/server-conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/AbstractA2AServerServerTest_v0_3.java @@ -1740,6 +1740,7 @@ protected void savePushNotificationConfigInStore(String taskId, PushNotification .uri(URI.create("http://localhost:" + serverPort + "/test/task/" + taskId)) .POST(HttpRequest.BodyPublishers.ofString(org.a2aproject.sdk.jsonrpc.common.json.JsonUtil.toJson(v10Config))) .header("Content-Type", APPLICATION_JSON) + .header("A2A-Version", A2AProtocol_v0_3.PROTOCOL_VERSION) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); diff --git a/tests/multiversion/grpc/src/test/java/org/a2aproject/sdk/tests/multiversion/grpc/A2ATestResource.java b/tests/multiversion/grpc/src/test/java/org/a2aproject/sdk/tests/multiversion/grpc/A2ATestResource.java index b5ac86735..d30fa6e9f 100644 --- a/tests/multiversion/grpc/src/test/java/org/a2aproject/sdk/tests/multiversion/grpc/A2ATestResource.java +++ b/tests/multiversion/grpc/src/test/java/org/a2aproject/sdk/tests/multiversion/grpc/A2ATestResource.java @@ -10,6 +10,7 @@ import jakarta.ws.rs.Consumes; import jakarta.ws.rs.DELETE; import jakarta.ws.rs.GET; +import jakarta.ws.rs.HeaderParam; import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; import jakarta.ws.rs.PathParam; @@ -131,12 +132,15 @@ public Response deleteTaskPushNotificationConfig(@PathParam("taskId") String tas @POST @Path("/task/{taskId}") @Consumes(MediaType.APPLICATION_JSON) - public Response savePushNotificationConfigInStore(@PathParam("taskId") String taskId, String body) throws Exception { + public Response savePushNotificationConfigInStore( + @PathParam("taskId") String taskId, + @HeaderParam("A2A-Version") String protocolVersion, + String body) throws Exception { TaskPushNotificationConfig notificationConfig = JsonUtil.fromJson(body, TaskPushNotificationConfig.class); if (notificationConfig == null) { return Response.status(404).build(); } - testUtilsBean.saveTaskPushNotificationConfig(taskId, notificationConfig); + testUtilsBean.saveTaskPushNotificationConfig(taskId, notificationConfig, protocolVersion); return Response.ok().build(); } diff --git a/tests/multiversion/jsonrpc/src/test/java/org/a2aproject/sdk/tests/multiversion/jsonrpc/A2ATestRoutes.java b/tests/multiversion/jsonrpc/src/test/java/org/a2aproject/sdk/tests/multiversion/jsonrpc/A2ATestRoutes.java index 82870a693..1c06793c6 100644 --- a/tests/multiversion/jsonrpc/src/test/java/org/a2aproject/sdk/tests/multiversion/jsonrpc/A2ATestRoutes.java +++ b/tests/multiversion/jsonrpc/src/test/java/org/a2aproject/sdk/tests/multiversion/jsonrpc/A2ATestRoutes.java @@ -221,7 +221,8 @@ public void saveTaskPushNotificationConfig(String taskId, String body, RoutingCo rc.response().setStatusCode(404).end(); return; } - testUtilsBean.saveTaskPushNotificationConfig(taskId, notificationConfig); + testUtilsBean.saveTaskPushNotificationConfig( + taskId, notificationConfig, rc.request().getHeader("A2A-Version")); rc.response().setStatusCode(200).end(); } catch (Throwable t) { errorResponse(t, rc); diff --git a/tests/multiversion/rest/src/test/java/org/a2aproject/sdk/tests/multiversion/rest/A2ATestRoutes.java b/tests/multiversion/rest/src/test/java/org/a2aproject/sdk/tests/multiversion/rest/A2ATestRoutes.java index 950699473..038ac6e96 100644 --- a/tests/multiversion/rest/src/test/java/org/a2aproject/sdk/tests/multiversion/rest/A2ATestRoutes.java +++ b/tests/multiversion/rest/src/test/java/org/a2aproject/sdk/tests/multiversion/rest/A2ATestRoutes.java @@ -289,7 +289,8 @@ public void saveTaskPushNotificationConfig(String taskId, String body, RoutingCo .end(); return; } - testUtilsBean.saveTaskPushNotificationConfig(taskId, notificationConfig); + testUtilsBean.saveTaskPushNotificationConfig( + taskId, notificationConfig, rc.request().getHeader("A2A-Version")); rc.response() .setStatusCode(200) .end(); diff --git a/tests/server-common/src/test/java/org/a2aproject/sdk/server/apps/common/TestUtilsBean.java b/tests/server-common/src/test/java/org/a2aproject/sdk/server/apps/common/TestUtilsBean.java index 7e771a32f..a7360bbd3 100644 --- a/tests/server-common/src/test/java/org/a2aproject/sdk/server/apps/common/TestUtilsBean.java +++ b/tests/server-common/src/test/java/org/a2aproject/sdk/server/apps/common/TestUtilsBean.java @@ -59,7 +59,16 @@ public void deleteTaskPushNotificationConfig(String taskId, String configId) { } public void saveTaskPushNotificationConfig(String taskId, TaskPushNotificationConfig notificationConfig) { - pushNotificationConfigStore.setInfo(TaskPushNotificationConfig.builder(notificationConfig).taskId(taskId).build()); + saveTaskPushNotificationConfig(taskId, notificationConfig, null); + } + + public void saveTaskPushNotificationConfig( + String taskId, + TaskPushNotificationConfig notificationConfig, + String protocolVersion) { + pushNotificationConfigStore.setInfo( + TaskPushNotificationConfig.builder(notificationConfig).taskId(taskId).build(), + protocolVersion); } /**