a2a-test-framework 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. checksums.yaml +7 -0
  2. data/a2a.json +1961 -0
  3. data/a2a.proto +796 -0
  4. data/endpoints/grpc/cancel_task.json +10 -0
  5. data/endpoints/grpc/create_task_push_notification_config.json +10 -0
  6. data/endpoints/grpc/delete_task_push_notification_config.json +10 -0
  7. data/endpoints/grpc/get_extended_agent_card.json +10 -0
  8. data/endpoints/grpc/get_task.json +10 -0
  9. data/endpoints/grpc/get_task_push_notification_config.json +10 -0
  10. data/endpoints/grpc/list_task_push_notification_configs.json +10 -0
  11. data/endpoints/grpc/list_tasks.json +10 -0
  12. data/endpoints/grpc/send_message.json +10 -0
  13. data/endpoints/grpc/send_streaming_message.json +10 -0
  14. data/endpoints/grpc/subscribe_to_task.json +10 -0
  15. data/endpoints/rest/cancel_task.json +85 -0
  16. data/endpoints/rest/create_task_push_notification_config.json +104 -0
  17. data/endpoints/rest/delete_task_push_notification_config.json +46 -0
  18. data/endpoints/rest/get_extended_agent_card.json +168 -0
  19. data/endpoints/rest/get_task.json +111 -0
  20. data/endpoints/rest/get_task_push_notification_config.json +90 -0
  21. data/endpoints/rest/list_task_push_notification_configs.json +108 -0
  22. data/endpoints/rest/list_tasks.json +239 -0
  23. data/endpoints/rest/send_message.json +57 -0
  24. data/endpoints/rest/send_streaming_message.json +75 -0
  25. data/endpoints/rest/subscribe_to_task.json +68 -0
  26. data/exe/a2a-test +6 -0
  27. data/lib/a2a_test_framework/cli.rb +190 -0
  28. data/lib/a2a_test_framework/sse_client.rb +104 -0
  29. data/lib/a2a_test_framework/test_helper.rb +146 -0
  30. data/lib/a2a_test_framework/version.rb +5 -0
  31. data/lib/a2a_test_framework.rb +17 -0
  32. data/tests/grpc/cancel_task_test.rb +69 -0
  33. data/tests/grpc/create_task_push_notification_config_test.rb +79 -0
  34. data/tests/grpc/delete_task_push_notification_config_test.rb +54 -0
  35. data/tests/grpc/error_code_mappings_test.rb +39 -0
  36. data/tests/grpc/error_handling_test.rb +175 -0
  37. data/tests/grpc/get_extended_agent_card_test.rb +83 -0
  38. data/tests/grpc/get_task_push_notification_config_test.rb +39 -0
  39. data/tests/grpc/get_task_test.rb +76 -0
  40. data/tests/grpc/grpc_binding_test.rb +74 -0
  41. data/tests/grpc/list_task_push_notification_configs_test.rb +53 -0
  42. data/tests/grpc/list_tasks_test.rb +117 -0
  43. data/tests/grpc/protocol_data_model_test.rb +14 -0
  44. data/tests/grpc/send_message_test.rb +141 -0
  45. data/tests/grpc/send_streaming_message_test.rb +122 -0
  46. data/tests/grpc/streaming_event_delivery_test.rb +48 -0
  47. data/tests/grpc/subscribe_to_task_test.rb +92 -0
  48. data/tests/grpc/versioning_test.rb +32 -0
  49. data/tests/rest/agent_card_caching_test.rb +39 -0
  50. data/tests/rest/agent_card_signing_test.rb +74 -0
  51. data/tests/rest/agent_discovery_test.rb +117 -0
  52. data/tests/rest/authentication_authorization_test.rb +62 -0
  53. data/tests/rest/cancel_task_test.rb +110 -0
  54. data/tests/rest/capability_validation_test.rb +78 -0
  55. data/tests/rest/context_identifier_semantics_test.rb +75 -0
  56. data/tests/rest/create_task_push_notification_config_test.rb +122 -0
  57. data/tests/rest/custom_binding_test.rb +96 -0
  58. data/tests/rest/delete_task_push_notification_config_test.rb +103 -0
  59. data/tests/rest/error_code_mappings_test.rb +45 -0
  60. data/tests/rest/error_handling_test.rb +178 -0
  61. data/tests/rest/extension_versioning_test.rb +44 -0
  62. data/tests/rest/field_presence_optionality_test.rb +64 -0
  63. data/tests/rest/functional_equivalence_test.rb +23 -0
  64. data/tests/rest/get_extended_agent_card_test.rb +67 -0
  65. data/tests/rest/get_task_push_notification_config_test.rb +75 -0
  66. data/tests/rest/get_task_test.rb +134 -0
  67. data/tests/rest/history_length_semantics_test.rb +91 -0
  68. data/tests/rest/http_rest_binding_test.rb +114 -0
  69. data/tests/rest/iana_registrations_test.rb +47 -0
  70. data/tests/rest/idempotency_test.rb +69 -0
  71. data/tests/rest/in_task_authorization_test.rb +45 -0
  72. data/tests/rest/json_field_naming_test.rb +89 -0
  73. data/tests/rest/json_rpc_binding_test.rb +102 -0
  74. data/tests/rest/list_task_push_notification_configs_test.rb +92 -0
  75. data/tests/rest/list_tasks_test.rb +162 -0
  76. data/tests/rest/messages_and_artifacts_test.rb +101 -0
  77. data/tests/rest/multi_turn_conversation_test.rb +94 -0
  78. data/tests/rest/protocol_data_model_test.rb +99 -0
  79. data/tests/rest/protocol_security_test.rb +25 -0
  80. data/tests/rest/protocol_selection_negotiation_test.rb +24 -0
  81. data/tests/rest/push_notification_delivery_test.rb +115 -0
  82. data/tests/rest/security_considerations_test.rb +101 -0
  83. data/tests/rest/send_message_test.rb +230 -0
  84. data/tests/rest/send_streaming_message_test.rb +129 -0
  85. data/tests/rest/service_parameters_test.rb +52 -0
  86. data/tests/rest/streaming_event_delivery_test.rb +58 -0
  87. data/tests/rest/subscribe_to_task_test.rb +99 -0
  88. data/tests/rest/task_identifier_semantics_test.rb +67 -0
  89. data/tests/rest/timestamps_test.rb +70 -0
  90. data/tests/rest/versioning_responsibilities_test.rb +46 -0
  91. data/tests/rest/versioning_test.rb +44 -0
  92. metadata +159 -0
@@ -0,0 +1,62 @@
1
+ require "a2a_test_framework/test_helper"
2
+
3
+ # NOTE: All tests commented out -- Reference server does not implement authentication
4
+
5
+ # require "a2a_test_framework/test_helper"
6
+
7
+ # # NOTE: All tests commented out -- Reference server does not implement authentication
8
+
9
+ # # describe "Authentication and Authorization (REST)" do
10
+ # # # --- Server Identity Verification ---
11
+ # #
12
+ # # describe "when a client establishes a connection" do
13
+ # # it "should verify the server TLS certificate against trusted CAs" do
14
+ # # end
15
+ # # end
16
+ # #
17
+ # # # --- Server Authentication Responsibilities ---
18
+ # #
19
+ # # describe "when any request is received by the server" do
20
+ # # it "should authenticate the request based on provided credentials" do
21
+ # # end
22
+ # # end
23
+ # #
24
+ # # describe "when a request fails authentication" do
25
+ # # it "should use appropriate binding-specific error codes" do
26
+ # # end
27
+ # #
28
+ # # it "should provide authentication challenge information with the error response" do
29
+ # # end
30
+ # # end
31
+ # #
32
+ # # # --- In-Task Authorization ---
33
+ # #
34
+ # # describe "when the agent requires authorization during task processing" do
35
+ # # it "should use a Task to track the operation" do
36
+ # # end
37
+ # #
38
+ # # it "should transition TaskState to TASK_STATE_AUTH_REQUIRED" do
39
+ # # end
40
+ # #
41
+ # # it "should include a TaskStatus message explaining the required authorization" do
42
+ # # end
43
+ # #
44
+ # # it "should arrange to receive credentials via an out-of-band means" do
45
+ # # end
46
+ # # end
47
+ # #
48
+ # # describe "when the agent transitions to TASK_STATE_AUTH_REQUIRED on a streamed task" do
49
+ # # it "should maintain the active response stream with the client" do
50
+ # # end
51
+ # # end
52
+ # #
53
+ # # describe "when credentials are received out-of-band for an auth_required task" do
54
+ # # it "should MAY immediately continue task processing without client follow-up message" do
55
+ # # end
56
+ # # end
57
+ # #
58
+ # # describe "when a client sends a message to a task in auth_required state" do
59
+ # # it "should accept and process the message to enable negotiation" do
60
+ # # end
61
+ # # end
62
+ # # end
@@ -0,0 +1,110 @@
1
+ require "a2a_test_framework/test_helper"
2
+
3
+ # REST endpoint: POST /tasks/{id}:cancel
4
+ # Request: CancelTaskRequest (id, metadata, tenant)
5
+ # Response: Task
6
+
7
+ describe "POST /tasks/{id}:cancel" do
8
+ # --- Successful Cancellation ---
9
+ # NOTE: The reference server completes tasks synchronously, so we cannot
10
+ # easily get a task in a non-terminal state to cancel. These are commented out.
11
+
12
+ # describe "when a client sends a CancelTask request for a working task" do
13
+ # it "should respond with an updated Task object" do
14
+ # end
15
+ #
16
+ # it "should reflect the cancellation in the Task status" do
17
+ # end
18
+ # end
19
+
20
+ # describe "when a client sends a CancelTask request for a task in input_required state" do
21
+ # it "should respond with an updated Task object" do
22
+ # end
23
+ #
24
+ # it "should reflect the cancellation in the Task status" do
25
+ # end
26
+ # end
27
+
28
+ # describe "when a client sends a CancelTask request for a cancelable task" do
29
+ # it "should return an updated Task object with cancellation status" do
30
+ # end
31
+ # end
32
+
33
+ # --- Error Cases ---
34
+
35
+ describe "when a client sends a CancelTask request for a completed task" do
36
+ it "should respond with a TaskNotCancelableError" do
37
+ task = create_task!(text: "Complete then cancel")
38
+ task["status"]["state"].should.equal "TASK_STATE_COMPLETED"
39
+
40
+ response = http_post("/tasks/#{task["id"]}:cancel", {})
41
+
42
+ if response.code.to_i >= 400
43
+ true.should.equal true
44
+ else
45
+ data = parse_json(response)
46
+ data.key?("error").should.equal true
47
+ end
48
+ end
49
+ end
50
+
51
+ describe "when a client sends a CancelTask request for an already canceled task" do
52
+ it "should respond with a TaskNotCancelableError" do
53
+ task = create_task!(text: "Cancel again")
54
+
55
+ # First cancel attempt (will fail since task is completed)
56
+ response = http_post("/tasks/#{task["id"]}:cancel", {})
57
+
58
+ # Second attempt should also error
59
+ response2 = http_post("/tasks/#{task["id"]}:cancel", {})
60
+ if response2.code.to_i >= 400
61
+ true.should.equal true
62
+ else
63
+ data = parse_json(response2)
64
+ data.key?("error").should.equal true
65
+ end
66
+ end
67
+ end
68
+
69
+ describe "when a client sends a CancelTask request with a non-existent task ID" do
70
+ it "should respond with a TaskNotFoundError" do
71
+ fake_id = "nonexistent-#{SecureRandom.uuid}"
72
+ response = http_post("/tasks/#{fake_id}:cancel", {})
73
+
74
+ if response.code.to_i >= 400
75
+ true.should.equal true
76
+ else
77
+ data = parse_json(response)
78
+ data.key?("error").should.equal true
79
+ end
80
+ end
81
+ end
82
+
83
+ describe "when a client sends a CancelTask request for a task not accessible to the client" do
84
+ it "should respond with a TaskNotFoundError" do
85
+ fake_id = "inaccessible-#{SecureRandom.uuid}"
86
+ response = http_post("/tasks/#{fake_id}:cancel", {})
87
+
88
+ if response.code.to_i >= 400
89
+ true.should.equal true
90
+ else
91
+ data = parse_json(response)
92
+ data.key?("error").should.equal true
93
+ end
94
+ end
95
+ end
96
+
97
+ # --- Idempotency ---
98
+
99
+ describe "when a client sends multiple CancelTask requests for the same task" do
100
+ it "should handle repeated cancellation requests consistently" do
101
+ task = create_task!(text: "Idempotent cancel")
102
+
103
+ response1 = http_post("/tasks/#{task["id"]}:cancel", {})
104
+ response2 = http_post("/tasks/#{task["id"]}:cancel", {})
105
+
106
+ # Both should return same type of error (task is already terminal)
107
+ response1.code.to_i.should.equal response2.code.to_i
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,78 @@
1
+ require "a2a_test_framework/test_helper"
2
+
3
+ # Cross-cutting: Capability validation applies to all optional endpoints
4
+
5
+ describe "Capability Validation (REST)" do
6
+ # --- Push Notifications Capability ---
7
+ # NOTE: Reference server declares pushNotifications=true, so these
8
+ # test the positive case. Negative case (false) is commented out.
9
+
10
+ # describe "when pushNotifications capability is false or not declared" do
11
+ # it "should return PushNotificationNotSupportedError on CreatePushNotificationConfig" do
12
+ # end
13
+ # end
14
+
15
+ # --- Streaming Capability ---
16
+ # NOTE: Reference server declares streaming=true
17
+
18
+ # describe "when streaming capability is false or not declared" do
19
+ # it "should return UnsupportedOperationError on SendStreamingMessage" do
20
+ # end
21
+ #
22
+ # it "should return UnsupportedOperationError on SubscribeToTask" do
23
+ # end
24
+ # end
25
+
26
+ # --- Extended Agent Card Capability ---
27
+
28
+ describe "when extendedAgentCard capability is false or not declared" do
29
+ it "should return an error on GetExtendedAgentCard" do
30
+ # Reference server has extendedAgentCard=false
31
+ response = http_get("/extendedAgentCard")
32
+
33
+ if response.code.to_i >= 400
34
+ true.should.equal true
35
+ else
36
+ data = parse_json(response)
37
+ data.key?("error").should.equal true
38
+ end
39
+ end
40
+ end
41
+
42
+ # --- Capability Declaration in AgentCard ---
43
+
44
+ describe "when examining the AgentCard capabilities" do
45
+ it "should declare streaming capability" do
46
+ response = http_get("/.well-known/agent-card.json")
47
+ data = parse_json(response)
48
+
49
+ data["capabilities"].key?("streaming").should.equal true
50
+ data["capabilities"]["streaming"].should.equal true
51
+ end
52
+
53
+ it "should declare pushNotifications capability" do
54
+ response = http_get("/.well-known/agent-card.json")
55
+ data = parse_json(response)
56
+
57
+ data["capabilities"].key?("pushNotifications").should.equal true
58
+ data["capabilities"]["pushNotifications"].should.equal true
59
+ end
60
+
61
+ it "should declare extendedAgentCard capability" do
62
+ response = http_get("/.well-known/agent-card.json")
63
+ data = parse_json(response)
64
+
65
+ data["capabilities"].key?("extendedAgentCard").should.equal true
66
+ # Reference server sets this to false
67
+ data["capabilities"]["extendedAgentCard"].should.equal false
68
+ end
69
+ end
70
+
71
+ # --- Extensions Capability ---
72
+ # NOTE: Commented out -- reference server doesn't use required extensions
73
+
74
+ # describe "when the AgentCard lists a required extension" do
75
+ # it "should return ExtensionSupportRequiredError if client does not declare support" do
76
+ # end
77
+ # end
78
+ end
@@ -0,0 +1,75 @@
1
+ require "a2a_test_framework/test_helper"
2
+
3
+ # Cross-cutting: Context identifier semantics
4
+
5
+ describe "Context Identifier Semantics (REST)" do
6
+ # --- Generation and Assignment ---
7
+
8
+ describe "when a client sends a message without a contextId" do
9
+ it "should generate a contextId and include it in the response" do
10
+ body = build_send_message_request(text: "No context provided")
11
+ # Don't include contextId in message
12
+ response = http_post("/message:send", body)
13
+ data = parse_json(response)
14
+
15
+ if data["task"]
16
+ data["task"]["contextId"].should.not.be.nil
17
+ data["task"]["contextId"].should.be.kind_of String
18
+ data["task"]["contextId"].length.should.be > 0
19
+ end
20
+ true.should.equal true
21
+ end
22
+ end
23
+
24
+ describe "when a client sends a message with a contextId" do
25
+ it "should preserve that contextId in the response" do
26
+ provided_context = SecureRandom.uuid
27
+ body = build_send_message_request(text: "With context", context_id: provided_context)
28
+ response = http_post("/message:send", body)
29
+ data = parse_json(response)
30
+
31
+ if data["task"]
32
+ data["task"]["contextId"].should.equal provided_context
33
+ end
34
+ true.should.equal true
35
+ end
36
+ end
37
+
38
+ # --- Grouping and Scope ---
39
+
40
+ describe "when multiple tasks share the same contextId" do
41
+ it "should group them under the same conversational session" do
42
+ context = SecureRandom.uuid
43
+ body1 = build_send_message_request(text: "Context group 1", context_id: context)
44
+ body2 = build_send_message_request(text: "Context group 2", context_id: context)
45
+
46
+ http_post("/message:send", body1)
47
+ http_post("/message:send", body2)
48
+
49
+ # Both tasks should appear when listing by contextId
50
+ response = http_get("/tasks?contextId=#{context}")
51
+ data = parse_json(response)
52
+
53
+ data["tasks"].length.should.be >= 2
54
+ data["tasks"].each { |t| t["contextId"].should.equal context }
55
+ end
56
+ end
57
+
58
+ # --- ContextId as opaque string ---
59
+
60
+ describe "when examining contextId format" do
61
+ it "should treat contextId as an opaque string" do
62
+ task = create_task!(text: "Opaque context test")
63
+ task["contextId"].should.be.kind_of String
64
+ task["contextId"].length.should.be > 0
65
+ end
66
+ end
67
+
68
+ # --- Server-Provided Context ---
69
+ # NOTE: Commented out -- behavior depends on whether server accepts client contexts
70
+
71
+ # describe "when the server does not accept client-provided contextIds" do
72
+ # it "should reject the request with an error" do
73
+ # end
74
+ # end
75
+ end
@@ -0,0 +1,122 @@
1
+ require "a2a_test_framework/test_helper"
2
+
3
+ # REST endpoint: POST /tasks/{task_id}/pushNotificationConfigs
4
+ # Request: TaskPushNotificationConfig (taskId, url, token, authentication, tenant)
5
+ # Response: TaskPushNotificationConfig
6
+
7
+ describe "POST /tasks/{task_id}/pushNotificationConfigs" do
8
+ # --- Successful Creation ---
9
+
10
+ describe "when a client creates a push notification config with a valid webhook URL" do
11
+ it "should respond with a PushNotificationConfig object" do
12
+ task = create_task!(text: "Push config creation")
13
+ body = build_push_notification_config(
14
+ task_id: task["id"],
15
+ url: "https://example.com/webhook",
16
+ token: "test-token-123"
17
+ )
18
+
19
+ response = http_post("/tasks/#{task["id"]}/pushNotificationConfigs", body)
20
+ response.code.to_i.should.equal 200
21
+
22
+ data = parse_json(response)
23
+ data["url"].should.equal "https://example.com/webhook"
24
+ data["token"].should.equal "test-token-123"
25
+ end
26
+
27
+ it "should contain an assigned ID in the response" do
28
+ task = create_task!(text: "Push config ID test")
29
+ body = build_push_notification_config(
30
+ task_id: task["id"],
31
+ url: "https://example.com/hook2"
32
+ )
33
+
34
+ response = http_post("/tasks/#{task["id"]}/pushNotificationConfigs", body)
35
+ data = parse_json(response)
36
+
37
+ data["id"].should.not.be.nil
38
+ data["id"].should.be.kind_of String
39
+ data["id"].length.should.be > 0
40
+ end
41
+ end
42
+
43
+ # --- Configuration with Authentication ---
44
+
45
+ describe "when a client creates a config with authentication details" do
46
+ it "should store and return the authentication scheme" do
47
+ task = create_task!(text: "Auth config test")
48
+ body = build_push_notification_config(
49
+ task_id: task["id"],
50
+ url: "https://example.com/authed-hook",
51
+ authentication: { "scheme" => "Bearer", "credentials" => "my-secret" }
52
+ )
53
+
54
+ response = http_post("/tasks/#{task["id"]}/pushNotificationConfigs", body)
55
+ response.code.to_i.should.equal 200
56
+
57
+ data = parse_json(response)
58
+ data["authentication"].should.not.be.nil
59
+ data["authentication"]["scheme"].should.equal "Bearer"
60
+ end
61
+ end
62
+
63
+ # --- Webhook Delivery ---
64
+ # NOTE: Commented out -- requires an actual webhook receiver endpoint
65
+
66
+ # describe "when the task status changes after config creation" do
67
+ # it "should send an HTTP POST request to the configured webhook URL" do
68
+ # end
69
+ #
70
+ # it "should send the payload as a StreamResponse object" do
71
+ # end
72
+ # end
73
+
74
+ # --- Configuration Persistence ---
75
+ # NOTE: Commented out -- requires async task lifecycle
76
+
77
+ # describe "when a push notification config exists for a non-terminal task" do
78
+ # it "should remain active while the task is in a non-terminal state" do
79
+ # end
80
+ # end
81
+
82
+ # --- Error Cases ---
83
+
84
+ describe "when a client sends a request with a non-existent task ID" do
85
+ it "should respond with a TaskNotFoundError" do
86
+ body = build_push_notification_config(
87
+ task_id: "nonexistent-#{SecureRandom.uuid}",
88
+ url: "https://example.com/hook"
89
+ )
90
+
91
+ response = http_post("/tasks/nonexistent-#{SecureRandom.uuid}/pushNotificationConfigs", body)
92
+
93
+ if response.code.to_i >= 400
94
+ true.should.equal true
95
+ else
96
+ data = parse_json(response)
97
+ data.key?("error").should.equal true
98
+ end
99
+ end
100
+ end
101
+
102
+ # --- Capability Validation ---
103
+ # NOTE: Reference server declares pushNotifications=true
104
+
105
+ describe "when the AgentCard declares pushNotifications capability as true" do
106
+ it "should accept and process the request" do
107
+ task = create_task!(text: "Capability check")
108
+ body = build_push_notification_config(
109
+ task_id: task["id"],
110
+ url: "https://example.com/cap-hook"
111
+ )
112
+
113
+ response = http_post("/tasks/#{task["id"]}/pushNotificationConfigs", body)
114
+ response.code.to_i.should.equal 200
115
+ end
116
+ end
117
+
118
+ # describe "when the AgentCard declares pushNotifications capability as false" do
119
+ # it "should respond with a PushNotificationNotSupportedError" do
120
+ # end
121
+ # end
122
+ end
@@ -0,0 +1,96 @@
1
+ require "a2a_test_framework/test_helper"
2
+
3
+ # NOTE: All tests commented out -- Custom binding testing not applicable to conformance suite
4
+
5
+ # require "a2a_test_framework/test_helper"
6
+
7
+ # # NOTE: All tests commented out -- Custom binding testing not applicable to conformance suite
8
+
9
+ # # describe "Custom Binding Guidelines" do
10
+ # # # --- Binding Requirements ---
11
+ # #
12
+ # # describe "when a custom binding is implemented" do
13
+ # # it "should implement all core operations defined in Section 3" do
14
+ # # end
15
+ # #
16
+ # # it "should use functionally equivalent data structures" do
17
+ # # end
18
+ # #
19
+ # # it "should maintain operation semantics consistent with abstract definitions" do
20
+ # # end
21
+ # #
22
+ # # it "should provide comprehensive documentation" do
23
+ # # end
24
+ # # end
25
+ # #
26
+ # # # --- Data Type Mappings ---
27
+ # #
28
+ # # describe "when mapping data types in a custom binding" do
29
+ # # it "should define how each Protocol Buffer message type is represented" do
30
+ # # end
31
+ # #
32
+ # # it "should follow timestamp conventions from Section 5.6.1" do
33
+ # # end
34
+ # # end
35
+ # #
36
+ # # # --- Service Parameter Transmission ---
37
+ # #
38
+ # # describe "when documenting service parameter transmission" do
39
+ # # it "should document how service parameters are transmitted" do
40
+ # # end
41
+ # #
42
+ # # it "should address the protocol-specific transmission mechanism" do
43
+ # # end
44
+ # # end
45
+ # #
46
+ # # # --- Error Mapping ---
47
+ # #
48
+ # # describe "when mapping errors in a custom binding" do
49
+ # # it "should provide mappings for all A2A-specific error types" do
50
+ # # end
51
+ # #
52
+ # # it "should ensure error details are accessible to clients" do
53
+ # # end
54
+ # # end
55
+ # #
56
+ # # # --- Streaming Support ---
57
+ # #
58
+ # # describe "when a custom binding does not support streaming" do
59
+ # # it "should clearly document this limitation in the Agent Card" do
60
+ # # end
61
+ # # end
62
+ # #
63
+ # # # --- Authentication ---
64
+ # #
65
+ # # describe "when implementing authentication" do
66
+ # # it "should support authentication schemes declared in the Agent Card" do
67
+ # # end
68
+ # # end
69
+ # #
70
+ # # # --- Agent Card Declaration ---
71
+ # #
72
+ # # describe "when declaring a custom binding in the Agent Card" do
73
+ # # it "should use a URI to identify the binding" do
74
+ # # end
75
+ # #
76
+ # # it "should provide the full URL where the binding is available" do
77
+ # # end
78
+ # # end
79
+ # # end
80
+ # #
81
+ # # describe "Custom Binding Identification" do
82
+ # # describe "when identifying a custom protocol binding" do
83
+ # # it "should use a URI as the protocolBinding field" do
84
+ # # end
85
+ # # end
86
+ # #
87
+ # # describe "when a breaking change is introduced to a custom binding" do
88
+ # # it "should use a new URI to distinguish incompatible versions" do
89
+ # # end
90
+ # # end
91
+ # #
92
+ # # describe "when multiple implementers have custom bindings" do
93
+ # # it "should use URIs for globally unique identification" do
94
+ # # end
95
+ # # end
96
+ # # end
@@ -0,0 +1,103 @@
1
+ require "a2a_test_framework/test_helper"
2
+
3
+ # REST endpoint: DELETE /tasks/{task_id}/pushNotificationConfigs/{id}
4
+ # Request: DeleteTaskPushNotificationConfigRequest (taskId, id, tenant)
5
+ # Response: google.protobuf.Empty
6
+
7
+ describe "DELETE /tasks/{task_id}/pushNotificationConfigs/{id}" do
8
+ # --- Successful Deletion ---
9
+
10
+ describe "when a client deletes an existing push notification config" do
11
+ it "should respond with a successful status" do
12
+ task = create_task!(text: "Delete config test")
13
+ body = build_push_notification_config(task_id: task["id"], url: "https://example.com/to-delete")
14
+ create_resp = http_post("/tasks/#{task["id"]}/pushNotificationConfigs", body)
15
+ config = parse_json(create_resp)
16
+
17
+ response = http_delete("/tasks/#{task["id"]}/pushNotificationConfigs/#{config["id"]}")
18
+ response.code.to_i.should.be < 400
19
+ end
20
+
21
+ it "should cause subsequent GetPushNotificationConfig requests to fail" do
22
+ task = create_task!(text: "Delete then get test")
23
+ body = build_push_notification_config(task_id: task["id"], url: "https://example.com/delete-verify")
24
+ create_resp = http_post("/tasks/#{task["id"]}/pushNotificationConfigs", body)
25
+ config = parse_json(create_resp)
26
+
27
+ # Delete it
28
+ http_delete("/tasks/#{task["id"]}/pushNotificationConfigs/#{config["id"]}")
29
+
30
+ # Try to get it -- should fail
31
+ get_response = http_get("/tasks/#{task["id"]}/pushNotificationConfigs/#{config["id"]}")
32
+ if get_response.code.to_i >= 400
33
+ true.should.equal true
34
+ else
35
+ data = parse_json(get_response)
36
+ data.key?("error").should.equal true
37
+ end
38
+ end
39
+
40
+ it "should remove config from the list" do
41
+ task = create_task!(text: "Delete from list test")
42
+ body = build_push_notification_config(task_id: task["id"], url: "https://example.com/list-delete")
43
+ create_resp = http_post("/tasks/#{task["id"]}/pushNotificationConfigs", body)
44
+ config = parse_json(create_resp)
45
+
46
+ # Delete
47
+ http_delete("/tasks/#{task["id"]}/pushNotificationConfigs/#{config["id"]}")
48
+
49
+ # List should be empty
50
+ list_resp = http_get("/tasks/#{task["id"]}/pushNotificationConfigs")
51
+ list_data = parse_json(list_resp)
52
+ list_data["configs"].length.should.equal 0
53
+ end
54
+ end
55
+
56
+ # --- Idempotency ---
57
+
58
+ describe "when a client sends multiple delete requests for the same config" do
59
+ it "should handle repeated deletion without error" do
60
+ task = create_task!(text: "Idempotent delete test")
61
+ body = build_push_notification_config(task_id: task["id"], url: "https://example.com/idem-delete")
62
+ create_resp = http_post("/tasks/#{task["id"]}/pushNotificationConfigs", body)
63
+ config = parse_json(create_resp)
64
+
65
+ # Delete twice
66
+ response1 = http_delete("/tasks/#{task["id"]}/pushNotificationConfigs/#{config["id"]}")
67
+ response2 = http_delete("/tasks/#{task["id"]}/pushNotificationConfigs/#{config["id"]}")
68
+
69
+ # Both should succeed (or second returns not-found gracefully)
70
+ response1.code.to_i.should.be < 500
71
+ response2.code.to_i.should.be < 500
72
+ end
73
+ end
74
+
75
+ # --- Error Cases ---
76
+
77
+ describe "when a client sends a request with a non-existent task ID" do
78
+ it "should respond with an error" do
79
+ response = http_delete("/tasks/nonexistent-#{SecureRandom.uuid}/pushNotificationConfigs/fake-id")
80
+
81
+ if response.code.to_i >= 400
82
+ true.should.equal true
83
+ else
84
+ data = parse_json(response)
85
+ data.key?("error").should.equal true
86
+ end
87
+ end
88
+ end
89
+
90
+ # NOTE: Commented out -- server supports push notifications
91
+
92
+ # describe "when the server does not support push notifications" do
93
+ # it "should respond with a PushNotificationNotSupportedError" do
94
+ # end
95
+ # end
96
+
97
+ # NOTE: Commented out -- requires webhook delivery verification
98
+
99
+ # describe "when a task changes status after config deletion" do
100
+ # it "should not send notifications to the previously configured webhook URL" do
101
+ # end
102
+ # end
103
+ end
@@ -0,0 +1,45 @@
1
+ require "a2a_test_framework/test_helper"
2
+
3
+ # Cross-cutting: HTTP status code mappings for REST binding
4
+
5
+ describe "Error Code Mappings (REST - HTTP Status)" do
6
+ describe "when a TaskNotFoundError occurs" do
7
+ it "should return HTTP 404 Not Found" do
8
+ response = http_get("/tasks/nonexistent-#{SecureRandom.uuid}")
9
+ response.code.to_i.should.equal 404
10
+ end
11
+ end
12
+
13
+ describe "when a TaskNotCancelableError occurs" do
14
+ it "should return HTTP 400 Bad Request" do
15
+ task = create_task!(text: "Cancel error code test")
16
+ response = http_post("/tasks/#{task["id"]}:cancel", {})
17
+ response.code.to_i.should.equal 400
18
+ end
19
+ end
20
+
21
+ describe "when an UnsupportedOperationError occurs" do
22
+ it "should return HTTP 400 Bad Request" do
23
+ response = http_get("/extendedAgentCard")
24
+ # Extended agent card returns unsupported operation
25
+ response.code.to_i.should.equal 400
26
+ end
27
+ end
28
+
29
+ # NOTE: Commented out -- difficult to trigger these specific errors
30
+
31
+ # describe "when a ContentTypeNotSupportedError occurs" do
32
+ # it "should return HTTP 400 Bad Request" do
33
+ # end
34
+ # end
35
+
36
+ # describe "when an InvalidAgentResponseError occurs" do
37
+ # it "should return HTTP 500 Internal Server Error" do
38
+ # end
39
+ # end
40
+
41
+ # describe "when a PushNotificationNotSupportedError occurs" do
42
+ # it "should return HTTP 400 Bad Request" do
43
+ # end
44
+ # end
45
+ end