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/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/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/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..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,10 +151,10 @@ public void testSetInfoAppendsToExistingConfig() { } @Test - public void testSetInfoWithoutConfigId() { + public void testSetInfoWithEmptyConfigId() { String taskId = "task1"; TaskPushNotificationConfig initialConfig = TaskPushNotificationConfig.builder() - .id("") // No ID set + .id("") .url("http://initial.url/callback") .taskId(taskId) .build(); @@ -167,7 +167,7 @@ public void testSetInfoWithoutConfigId() { assertEquals(taskId, configResult.configs().get(0).id()); TaskPushNotificationConfig updatedConfig = TaskPushNotificationConfig.builder() - .id("") // No ID set + .id("") .url("http://initial.url/callback_new") .taskId(taskId) .build(); @@ -180,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 eeb950cd8..584b7de23 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,29 @@ 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 +92,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 +116,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/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/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()); + } +} 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()); + } +}