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.
- checksums.yaml +7 -0
- data/a2a.json +1961 -0
- data/a2a.proto +796 -0
- data/endpoints/grpc/cancel_task.json +10 -0
- data/endpoints/grpc/create_task_push_notification_config.json +10 -0
- data/endpoints/grpc/delete_task_push_notification_config.json +10 -0
- data/endpoints/grpc/get_extended_agent_card.json +10 -0
- data/endpoints/grpc/get_task.json +10 -0
- data/endpoints/grpc/get_task_push_notification_config.json +10 -0
- data/endpoints/grpc/list_task_push_notification_configs.json +10 -0
- data/endpoints/grpc/list_tasks.json +10 -0
- data/endpoints/grpc/send_message.json +10 -0
- data/endpoints/grpc/send_streaming_message.json +10 -0
- data/endpoints/grpc/subscribe_to_task.json +10 -0
- data/endpoints/rest/cancel_task.json +85 -0
- data/endpoints/rest/create_task_push_notification_config.json +104 -0
- data/endpoints/rest/delete_task_push_notification_config.json +46 -0
- data/endpoints/rest/get_extended_agent_card.json +168 -0
- data/endpoints/rest/get_task.json +111 -0
- data/endpoints/rest/get_task_push_notification_config.json +90 -0
- data/endpoints/rest/list_task_push_notification_configs.json +108 -0
- data/endpoints/rest/list_tasks.json +239 -0
- data/endpoints/rest/send_message.json +57 -0
- data/endpoints/rest/send_streaming_message.json +75 -0
- data/endpoints/rest/subscribe_to_task.json +68 -0
- data/exe/a2a-test +6 -0
- data/lib/a2a_test_framework/cli.rb +190 -0
- data/lib/a2a_test_framework/sse_client.rb +104 -0
- data/lib/a2a_test_framework/test_helper.rb +146 -0
- data/lib/a2a_test_framework/version.rb +5 -0
- data/lib/a2a_test_framework.rb +17 -0
- data/tests/grpc/cancel_task_test.rb +69 -0
- data/tests/grpc/create_task_push_notification_config_test.rb +79 -0
- data/tests/grpc/delete_task_push_notification_config_test.rb +54 -0
- data/tests/grpc/error_code_mappings_test.rb +39 -0
- data/tests/grpc/error_handling_test.rb +175 -0
- data/tests/grpc/get_extended_agent_card_test.rb +83 -0
- data/tests/grpc/get_task_push_notification_config_test.rb +39 -0
- data/tests/grpc/get_task_test.rb +76 -0
- data/tests/grpc/grpc_binding_test.rb +74 -0
- data/tests/grpc/list_task_push_notification_configs_test.rb +53 -0
- data/tests/grpc/list_tasks_test.rb +117 -0
- data/tests/grpc/protocol_data_model_test.rb +14 -0
- data/tests/grpc/send_message_test.rb +141 -0
- data/tests/grpc/send_streaming_message_test.rb +122 -0
- data/tests/grpc/streaming_event_delivery_test.rb +48 -0
- data/tests/grpc/subscribe_to_task_test.rb +92 -0
- data/tests/grpc/versioning_test.rb +32 -0
- data/tests/rest/agent_card_caching_test.rb +39 -0
- data/tests/rest/agent_card_signing_test.rb +74 -0
- data/tests/rest/agent_discovery_test.rb +117 -0
- data/tests/rest/authentication_authorization_test.rb +62 -0
- data/tests/rest/cancel_task_test.rb +110 -0
- data/tests/rest/capability_validation_test.rb +78 -0
- data/tests/rest/context_identifier_semantics_test.rb +75 -0
- data/tests/rest/create_task_push_notification_config_test.rb +122 -0
- data/tests/rest/custom_binding_test.rb +96 -0
- data/tests/rest/delete_task_push_notification_config_test.rb +103 -0
- data/tests/rest/error_code_mappings_test.rb +45 -0
- data/tests/rest/error_handling_test.rb +178 -0
- data/tests/rest/extension_versioning_test.rb +44 -0
- data/tests/rest/field_presence_optionality_test.rb +64 -0
- data/tests/rest/functional_equivalence_test.rb +23 -0
- data/tests/rest/get_extended_agent_card_test.rb +67 -0
- data/tests/rest/get_task_push_notification_config_test.rb +75 -0
- data/tests/rest/get_task_test.rb +134 -0
- data/tests/rest/history_length_semantics_test.rb +91 -0
- data/tests/rest/http_rest_binding_test.rb +114 -0
- data/tests/rest/iana_registrations_test.rb +47 -0
- data/tests/rest/idempotency_test.rb +69 -0
- data/tests/rest/in_task_authorization_test.rb +45 -0
- data/tests/rest/json_field_naming_test.rb +89 -0
- data/tests/rest/json_rpc_binding_test.rb +102 -0
- data/tests/rest/list_task_push_notification_configs_test.rb +92 -0
- data/tests/rest/list_tasks_test.rb +162 -0
- data/tests/rest/messages_and_artifacts_test.rb +101 -0
- data/tests/rest/multi_turn_conversation_test.rb +94 -0
- data/tests/rest/protocol_data_model_test.rb +99 -0
- data/tests/rest/protocol_security_test.rb +25 -0
- data/tests/rest/protocol_selection_negotiation_test.rb +24 -0
- data/tests/rest/push_notification_delivery_test.rb +115 -0
- data/tests/rest/security_considerations_test.rb +101 -0
- data/tests/rest/send_message_test.rb +230 -0
- data/tests/rest/send_streaming_message_test.rb +129 -0
- data/tests/rest/service_parameters_test.rb +52 -0
- data/tests/rest/streaming_event_delivery_test.rb +58 -0
- data/tests/rest/subscribe_to_task_test.rb +99 -0
- data/tests/rest/task_identifier_semantics_test.rb +67 -0
- data/tests/rest/timestamps_test.rb +70 -0
- data/tests/rest/versioning_responsibilities_test.rb +46 -0
- data/tests/rest/versioning_test.rb +44 -0
- 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
|