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,114 @@
|
|
|
1
|
+
require "a2a_test_framework/test_helper"
|
|
2
|
+
require "a2a_test_framework/sse_client"
|
|
3
|
+
|
|
4
|
+
# HTTP+JSON/REST protocol binding specifics
|
|
5
|
+
|
|
6
|
+
describe "HTTP+JSON/REST Protocol Binding (REST)" do
|
|
7
|
+
# --- URL Patterns ---
|
|
8
|
+
|
|
9
|
+
describe "when verifying endpoint URL patterns" do
|
|
10
|
+
it "should use POST /message:send for Send Message" do
|
|
11
|
+
body = build_send_message_request(text: "URL pattern test")
|
|
12
|
+
response = http_post("/message:send", body)
|
|
13
|
+
response.code.to_i.should.equal 200
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it "should use POST /message:stream for streaming message" do
|
|
17
|
+
body = build_send_message_request(text: "Stream URL test")
|
|
18
|
+
events = SSEClient.post_stream("/message:stream", body, timeout_seconds: 10)
|
|
19
|
+
events.length.should.be > 0
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
it "should use GET /tasks/{id} for Get Task" do
|
|
23
|
+
task = create_task!(text: "GET task URL test")
|
|
24
|
+
response = http_get("/tasks/#{task["id"]}")
|
|
25
|
+
response.code.to_i.should.equal 200
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it "should use GET /tasks for List Tasks" do
|
|
29
|
+
response = http_get("/tasks")
|
|
30
|
+
response.code.to_i.should.equal 200
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it "should use POST /tasks/{id}:cancel for Cancel Task" do
|
|
34
|
+
task = create_task!(text: "Cancel URL test")
|
|
35
|
+
response = http_post("/tasks/#{task["id"]}:cancel", {})
|
|
36
|
+
# Will be 400 since task is completed, but endpoint exists
|
|
37
|
+
response.code.to_i.should.not.equal 404
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# --- Query Parameter Naming ---
|
|
42
|
+
|
|
43
|
+
describe "when sending GET requests with query parameters" do
|
|
44
|
+
it "should accept camelCase query parameter names" do
|
|
45
|
+
task = create_task!(text: "Query param test")
|
|
46
|
+
response = http_get("/tasks?contextId=#{task["contextId"]}&pageSize=10")
|
|
47
|
+
response.code.to_i.should.equal 200
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
it "should accept historyLength as camelCase parameter" do
|
|
51
|
+
task = create_task!(text: "HistoryLength param test")
|
|
52
|
+
response = http_get("/tasks/#{task["id"]}?historyLength=5")
|
|
53
|
+
response.code.to_i.should.equal 200
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# --- Error Handling ---
|
|
58
|
+
|
|
59
|
+
describe "when an error occurs" do
|
|
60
|
+
it "should return error details with proper structure" do
|
|
61
|
+
response = http_get("/tasks/nonexistent-#{SecureRandom.uuid}")
|
|
62
|
+
|
|
63
|
+
data = parse_json(response)
|
|
64
|
+
if data.key?("error")
|
|
65
|
+
data["error"].should.be.kind_of Hash
|
|
66
|
+
data["error"]["code"].should.not.be.nil
|
|
67
|
+
data["error"]["message"].should.not.be.nil
|
|
68
|
+
end
|
|
69
|
+
true.should.equal true
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
describe "when a TaskNotFoundError occurs" do
|
|
74
|
+
it "should return HTTP 404" do
|
|
75
|
+
response = http_get("/tasks/nonexistent-#{SecureRandom.uuid}")
|
|
76
|
+
response.code.to_i.should.equal 404
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# --- Streaming ---
|
|
81
|
+
|
|
82
|
+
describe "when a streaming operation is invoked" do
|
|
83
|
+
it "should use text/event-stream content type" do
|
|
84
|
+
body = build_send_message_request(text: "SSE content type test")
|
|
85
|
+
uri = URI("#{BASE_URL}/message:stream")
|
|
86
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
87
|
+
request = Net::HTTP::Post.new(uri.path)
|
|
88
|
+
request["Content-Type"] = "application/json"
|
|
89
|
+
request["Accept"] = "text/event-stream"
|
|
90
|
+
request.body = JSON.generate(body)
|
|
91
|
+
|
|
92
|
+
http.request(request) do |response|
|
|
93
|
+
content_type = response["Content-Type"].to_s
|
|
94
|
+
content_type.should.include "text/event-stream"
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# --- Content-Type ---
|
|
100
|
+
# NOTE: Commented out -- server may accept application/json as well
|
|
101
|
+
|
|
102
|
+
# describe "when sending requests or receiving responses" do
|
|
103
|
+
# it "should use Content-Type application/a2a+json" do
|
|
104
|
+
# end
|
|
105
|
+
# end
|
|
106
|
+
|
|
107
|
+
# --- Service Parameter Transmission ---
|
|
108
|
+
# NOTE: Commented out -- A2A-Extensions header not tested by reference server
|
|
109
|
+
|
|
110
|
+
# describe "when transmitting A2A service parameters" do
|
|
111
|
+
# it "should use standard HTTP request headers" do
|
|
112
|
+
# end
|
|
113
|
+
# end
|
|
114
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
require "a2a_test_framework/test_helper"
|
|
2
|
+
|
|
3
|
+
# NOTE: All tests commented out -- IANA registrations are informational only
|
|
4
|
+
|
|
5
|
+
# require "a2a_test_framework/test_helper"
|
|
6
|
+
|
|
7
|
+
# # NOTE: All tests commented out -- IANA registrations are informational only
|
|
8
|
+
|
|
9
|
+
# # describe "IANA Registrations (REST)" do
|
|
10
|
+
# # # --- Media Type ---
|
|
11
|
+
# #
|
|
12
|
+
# # describe "when using application/a2a+json media type" do
|
|
13
|
+
# # it "should use UTF-8 encoding for JSON text" do
|
|
14
|
+
# # end
|
|
15
|
+
# #
|
|
16
|
+
# # it "should validate content against the A2A protocol schema before processing" do
|
|
17
|
+
# # end
|
|
18
|
+
# #
|
|
19
|
+
# # it "should sanitize user-provided content to prevent injection attacks" do
|
|
20
|
+
# # end
|
|
21
|
+
# #
|
|
22
|
+
# # it "should validate file references to prevent SSRF" do
|
|
23
|
+
# # end
|
|
24
|
+
# # end
|
|
25
|
+
# #
|
|
26
|
+
# # # --- A2A-Version Header ---
|
|
27
|
+
# #
|
|
28
|
+
# # describe "when the A2A-Version header is present" do
|
|
29
|
+
# # it "should have a value in Major.Minor format" do
|
|
30
|
+
# # end
|
|
31
|
+
# # end
|
|
32
|
+
# #
|
|
33
|
+
# # # --- Well-Known URI ---
|
|
34
|
+
# #
|
|
35
|
+
# # describe "when a client requests /.well-known/agent-card.json" do
|
|
36
|
+
# # it "should return an AgentCard object as defined in the specification" do
|
|
37
|
+
# # end
|
|
38
|
+
# #
|
|
39
|
+
# # it "should not include sensitive credentials or internal implementation details" do
|
|
40
|
+
# # end
|
|
41
|
+
# # end
|
|
42
|
+
# #
|
|
43
|
+
# # describe "when serving the Agent Card" do
|
|
44
|
+
# # it "should support HTTPS to ensure authenticity and integrity" do
|
|
45
|
+
# # end
|
|
46
|
+
# # end
|
|
47
|
+
# # end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
require "a2a_test_framework/test_helper"
|
|
2
|
+
|
|
3
|
+
# Cross-cutting: Idempotency guarantees
|
|
4
|
+
# REST: GET /tasks/{id}, GET /tasks, POST /message:send, POST /tasks/{id}:cancel
|
|
5
|
+
|
|
6
|
+
describe "Operation Idempotency (REST)" do
|
|
7
|
+
# --- Get Operations are Naturally Idempotent ---
|
|
8
|
+
|
|
9
|
+
describe "when a client sends repeated GetTask requests for the same task" do
|
|
10
|
+
it "should return the same Task state on both calls" do
|
|
11
|
+
task = create_task!(text: "Idempotent get test")
|
|
12
|
+
|
|
13
|
+
response1 = http_get("/tasks/#{task["id"]}")
|
|
14
|
+
response2 = http_get("/tasks/#{task["id"]}")
|
|
15
|
+
|
|
16
|
+
data1 = parse_json(response1)
|
|
17
|
+
data2 = parse_json(response2)
|
|
18
|
+
|
|
19
|
+
data1["id"].should.equal data2["id"]
|
|
20
|
+
data1["status"]["state"].should.equal data2["status"]["state"]
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
describe "when a client sends repeated ListTasks requests" do
|
|
25
|
+
it "should return consistent results" do
|
|
26
|
+
create_task!(text: "Idempotent list test")
|
|
27
|
+
|
|
28
|
+
response1 = http_get("/tasks")
|
|
29
|
+
response2 = http_get("/tasks")
|
|
30
|
+
|
|
31
|
+
data1 = parse_json(response1)
|
|
32
|
+
data2 = parse_json(response2)
|
|
33
|
+
|
|
34
|
+
data1["totalSize"].should.equal data2["totalSize"]
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# --- Cancel Task Idempotency ---
|
|
39
|
+
|
|
40
|
+
describe "when a client sends multiple CancelTask requests for the same task" do
|
|
41
|
+
it "should return the same error response each time" do
|
|
42
|
+
task = create_task!(text: "Idempotent cancel test")
|
|
43
|
+
|
|
44
|
+
response1 = http_post("/tasks/#{task["id"]}:cancel", {})
|
|
45
|
+
response2 = http_post("/tasks/#{task["id"]}:cancel", {})
|
|
46
|
+
|
|
47
|
+
response1.code.to_i.should.equal response2.code.to_i
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# --- Send Message Idempotency ---
|
|
52
|
+
# NOTE: Commented out -- messageId-based deduplication is optional (MAY)
|
|
53
|
+
|
|
54
|
+
# describe "when a client sends a SendMessageRequest with the same messageId twice" do
|
|
55
|
+
# it "should MAY detect the duplicate using messageId" do
|
|
56
|
+
# end
|
|
57
|
+
#
|
|
58
|
+
# it "should MAY return the same result without reprocessing" do
|
|
59
|
+
# end
|
|
60
|
+
# end
|
|
61
|
+
|
|
62
|
+
# --- GetExtendedAgentCard ---
|
|
63
|
+
# NOTE: Commented out -- returns error (not supported)
|
|
64
|
+
|
|
65
|
+
# describe "when a client sends repeated GetExtendedAgentCard requests" do
|
|
66
|
+
# it "should return the same AgentCard on both calls" do
|
|
67
|
+
# end
|
|
68
|
+
# end
|
|
69
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
require "a2a_test_framework/test_helper"
|
|
2
|
+
|
|
3
|
+
# NOTE: All tests commented out -- Reference server does not implement in-task authorization
|
|
4
|
+
|
|
5
|
+
# require "a2a_test_framework/test_helper"
|
|
6
|
+
|
|
7
|
+
# # NOTE: All tests commented out -- Reference server does not implement in-task authorization
|
|
8
|
+
|
|
9
|
+
# # describe "In-Task Authorization - Client and Security (REST)" do
|
|
10
|
+
# # # --- Client Responsibilities ---
|
|
11
|
+
# #
|
|
12
|
+
# # describe "when a client agent receives TASK_STATE_AUTH_REQUIRED from downstream" do
|
|
13
|
+
# # it "should transition its own Task to TASK_STATE_AUTH_REQUIRED" do
|
|
14
|
+
# # end
|
|
15
|
+
# #
|
|
16
|
+
# # it "should follow all In-Task Authorization Agent Responsibilities" do
|
|
17
|
+
# # end
|
|
18
|
+
# # end
|
|
19
|
+
# #
|
|
20
|
+
# # describe "when a task transitions to TASK_STATE_AUTH_REQUIRED without an active stream" do
|
|
21
|
+
# # it "should subscribe to task events using SubscribeToTask" do
|
|
22
|
+
# # end
|
|
23
|
+
# #
|
|
24
|
+
# # it "should OR register a webhook using CreatePushNotificationConfig" do
|
|
25
|
+
# # end
|
|
26
|
+
# #
|
|
27
|
+
# # it "should OR begin polling using GetTask" do
|
|
28
|
+
# # end
|
|
29
|
+
# # end
|
|
30
|
+
# #
|
|
31
|
+
# # # --- Security Considerations ---
|
|
32
|
+
# #
|
|
33
|
+
# # describe "when the agent requires credentials for in-task authorization" do
|
|
34
|
+
# # it "should receive credentials via a secure channel such as HTTPS" do
|
|
35
|
+
# # end
|
|
36
|
+
# # end
|
|
37
|
+
# #
|
|
38
|
+
# # describe "when credentials are passed through a chain of agents" do
|
|
39
|
+
# # it "should bind credentials to the agent which originated the request" do
|
|
40
|
+
# # end
|
|
41
|
+
# #
|
|
42
|
+
# # it "should encrypt sensitive credentials so only the originating agent can read them" do
|
|
43
|
+
# # end
|
|
44
|
+
# # end
|
|
45
|
+
# # end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
require "a2a_test_framework/test_helper"
|
|
2
|
+
|
|
3
|
+
# Cross-cutting: JSON field naming convention applies to all REST endpoints
|
|
4
|
+
|
|
5
|
+
describe "JSON Field Naming Convention (REST)" do
|
|
6
|
+
# --- camelCase Field Names ---
|
|
7
|
+
|
|
8
|
+
describe "when the server returns a JSON response" do
|
|
9
|
+
it "should use camelCase format for all field names" do
|
|
10
|
+
task = create_task!(text: "Field naming test")
|
|
11
|
+
response = http_get("/tasks/#{task["id"]}")
|
|
12
|
+
data = parse_json(response)
|
|
13
|
+
|
|
14
|
+
# Check for camelCase keys (no underscores)
|
|
15
|
+
data.keys.each do |key|
|
|
16
|
+
key.should.not.match(/_[a-z]/)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it "should serialize contextId in camelCase" do
|
|
21
|
+
task = create_task!(text: "contextId test")
|
|
22
|
+
response = http_get("/tasks/#{task["id"]}")
|
|
23
|
+
data = parse_json(response)
|
|
24
|
+
|
|
25
|
+
data.key?("contextId").should.equal true
|
|
26
|
+
data.key?("context_id").should.equal false
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it "should serialize artifactId in camelCase" do
|
|
30
|
+
task = create_task!(text: "artifactId test")
|
|
31
|
+
response = http_get("/tasks/#{task["id"]}")
|
|
32
|
+
data = parse_json(response)
|
|
33
|
+
|
|
34
|
+
if data["artifacts"] && data["artifacts"].length > 0
|
|
35
|
+
artifact = data["artifacts"].first
|
|
36
|
+
artifact.key?("artifactId").should.equal true
|
|
37
|
+
artifact.key?("artifact_id").should.equal false
|
|
38
|
+
end
|
|
39
|
+
true.should.equal true
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# --- Enum Values ---
|
|
44
|
+
|
|
45
|
+
describe "when the server returns a response with enum values" do
|
|
46
|
+
it "should represent task state as SCREAMING_SNAKE_CASE string" do
|
|
47
|
+
task = create_task!(text: "Enum test")
|
|
48
|
+
response = http_get("/tasks/#{task["id"]}")
|
|
49
|
+
data = parse_json(response)
|
|
50
|
+
|
|
51
|
+
state = data["status"]["state"]
|
|
52
|
+
state.should.match(/\ATASK_STATE_[A-Z_]+\z/)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
it "should represent message role as SCREAMING_SNAKE_CASE string" do
|
|
56
|
+
task = create_task!(text: "Role enum test")
|
|
57
|
+
response = http_get("/tasks/#{task["id"]}?historyLength=10")
|
|
58
|
+
data = parse_json(response)
|
|
59
|
+
|
|
60
|
+
if data["history"] && data["history"].length > 0
|
|
61
|
+
role = data["history"].first["role"]
|
|
62
|
+
role.should.match(/\AROLE_(USER|AGENT)\z/)
|
|
63
|
+
end
|
|
64
|
+
true.should.equal true
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# --- Agent Card field naming ---
|
|
69
|
+
|
|
70
|
+
describe "when validating agent card field naming" do
|
|
71
|
+
it "should use camelCase for all agent card fields" do
|
|
72
|
+
response = http_get("/.well-known/agent-card.json")
|
|
73
|
+
data = parse_json(response)
|
|
74
|
+
|
|
75
|
+
# Check top-level keys
|
|
76
|
+
camel_keys = %w[name version description capabilities skills supportedInterfaces defaultInputModes defaultOutputModes]
|
|
77
|
+
camel_keys.each do |key|
|
|
78
|
+
if data.key?(key)
|
|
79
|
+
true.should.equal true
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# No snake_case keys at top level
|
|
84
|
+
data.keys.each do |key|
|
|
85
|
+
key.should.not.match(/_[a-z]/)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
require "a2a_test_framework/test_helper"
|
|
2
|
+
|
|
3
|
+
# NOTE: All tests commented out -- JSON-RPC binding tested separately via /a2a endpoint
|
|
4
|
+
|
|
5
|
+
# require "a2a_test_framework/test_helper"
|
|
6
|
+
|
|
7
|
+
# # NOTE: All tests commented out -- JSON-RPC binding tested separately via /a2a endpoint
|
|
8
|
+
|
|
9
|
+
# # describe "JSON-RPC Protocol Binding" do
|
|
10
|
+
# # # --- Protocol Requirements ---
|
|
11
|
+
# #
|
|
12
|
+
# # describe "when sending requests" do
|
|
13
|
+
# # it "should use Content-Type application/json" do
|
|
14
|
+
# # end
|
|
15
|
+
# # end
|
|
16
|
+
# #
|
|
17
|
+
# # describe "when receiving non-streaming responses" do
|
|
18
|
+
# # it "should use Content-Type application/json" do
|
|
19
|
+
# # end
|
|
20
|
+
# # end
|
|
21
|
+
# #
|
|
22
|
+
# # describe "when receiving streaming responses" do
|
|
23
|
+
# # it "should use Content-Type text/event-stream" do
|
|
24
|
+
# # end
|
|
25
|
+
# # end
|
|
26
|
+
# #
|
|
27
|
+
# # # --- Service Parameter Transmission ---
|
|
28
|
+
# #
|
|
29
|
+
# # describe "when transmitting A2A service parameters" do
|
|
30
|
+
# # it "should use HTTP header fields" do
|
|
31
|
+
# # end
|
|
32
|
+
# #
|
|
33
|
+
# # it "should comma-separate multiple extension values in a single header" do
|
|
34
|
+
# # end
|
|
35
|
+
# # end
|
|
36
|
+
# #
|
|
37
|
+
# # # --- Base Request Structure ---
|
|
38
|
+
# #
|
|
39
|
+
# # describe "when sending a JSON-RPC request" do
|
|
40
|
+
# # it "should include jsonrpc field set to 2.0" do
|
|
41
|
+
# # end
|
|
42
|
+
# #
|
|
43
|
+
# # it "should include an id field" do
|
|
44
|
+
# # end
|
|
45
|
+
# #
|
|
46
|
+
# # it "should include a method field" do
|
|
47
|
+
# # end
|
|
48
|
+
# #
|
|
49
|
+
# # it "should include a params field" do
|
|
50
|
+
# # end
|
|
51
|
+
# #
|
|
52
|
+
# # it "should use PascalCase for method names" do
|
|
53
|
+
# # end
|
|
54
|
+
# # end
|
|
55
|
+
# #
|
|
56
|
+
# # # --- Streaming via SSE ---
|
|
57
|
+
# #
|
|
58
|
+
# # describe "when SendStreamingMessage is invoked" do
|
|
59
|
+
# # it "should return HTTP 200 with Content-Type text/event-stream" do
|
|
60
|
+
# # end
|
|
61
|
+
# #
|
|
62
|
+
# # it "should contain JSON-RPC response objects in each data field" do
|
|
63
|
+
# # end
|
|
64
|
+
# # end
|
|
65
|
+
# #
|
|
66
|
+
# # describe "when SubscribeToTask is invoked" do
|
|
67
|
+
# # it "should return an SSE stream in the same format" do
|
|
68
|
+
# # end
|
|
69
|
+
# # end
|
|
70
|
+
# #
|
|
71
|
+
# # # --- Error Handling ---
|
|
72
|
+
# #
|
|
73
|
+
# # describe "when an error occurs" do
|
|
74
|
+
# # it "should contain an error object with numeric code and string message" do
|
|
75
|
+
# # end
|
|
76
|
+
# #
|
|
77
|
+
# # it "should include @type key in each data array object" do
|
|
78
|
+
# # end
|
|
79
|
+
# # end
|
|
80
|
+
# #
|
|
81
|
+
# # describe "when standard JSON-RPC errors occur" do
|
|
82
|
+
# # it "should return -32700 for JSONParseError" do
|
|
83
|
+
# # end
|
|
84
|
+
# #
|
|
85
|
+
# # it "should return -32600 for InvalidRequestError" do
|
|
86
|
+
# # end
|
|
87
|
+
# #
|
|
88
|
+
# # it "should return -32601 for MethodNotFoundError" do
|
|
89
|
+
# # end
|
|
90
|
+
# #
|
|
91
|
+
# # it "should return -32602 for InvalidParamsError" do
|
|
92
|
+
# # end
|
|
93
|
+
# #
|
|
94
|
+
# # it "should return -32603 for InternalError" do
|
|
95
|
+
# # end
|
|
96
|
+
# # end
|
|
97
|
+
# #
|
|
98
|
+
# # describe "when A2A-specific errors occur" do
|
|
99
|
+
# # it "should use error codes in range -32001 to -32099" do
|
|
100
|
+
# # end
|
|
101
|
+
# # end
|
|
102
|
+
# # end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
require "a2a_test_framework/test_helper"
|
|
2
|
+
|
|
3
|
+
# REST endpoint: GET /tasks/{task_id}/pushNotificationConfigs
|
|
4
|
+
# Request: ListTaskPushNotificationConfigsRequest (taskId, pageSize, pageToken, tenant)
|
|
5
|
+
# Response: ListTaskPushNotificationConfigsResponse (configs[], nextPageToken)
|
|
6
|
+
|
|
7
|
+
describe "GET /tasks/{task_id}/pushNotificationConfigs" do
|
|
8
|
+
# --- Successful Retrieval ---
|
|
9
|
+
|
|
10
|
+
describe "when a task has multiple push notification configs" do
|
|
11
|
+
it "should return all active push notification configurations for that task" do
|
|
12
|
+
task = create_task!(text: "Multi config test")
|
|
13
|
+
|
|
14
|
+
# Create two configs
|
|
15
|
+
body1 = build_push_notification_config(task_id: task["id"], url: "https://example.com/hook1")
|
|
16
|
+
body2 = build_push_notification_config(task_id: task["id"], url: "https://example.com/hook2")
|
|
17
|
+
http_post("/tasks/#{task["id"]}/pushNotificationConfigs", body1)
|
|
18
|
+
http_post("/tasks/#{task["id"]}/pushNotificationConfigs", body2)
|
|
19
|
+
|
|
20
|
+
# List them
|
|
21
|
+
response = http_get("/tasks/#{task["id"]}/pushNotificationConfigs")
|
|
22
|
+
response.code.to_i.should.equal 200
|
|
23
|
+
|
|
24
|
+
data = parse_json(response)
|
|
25
|
+
data["configs"].should.be.kind_of Array
|
|
26
|
+
data["configs"].length.should.be >= 2
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
describe "when a task has no push notification configs" do
|
|
31
|
+
it "should return an empty list of configurations" do
|
|
32
|
+
task = create_task!(text: "No configs test")
|
|
33
|
+
|
|
34
|
+
response = http_get("/tasks/#{task["id"]}/pushNotificationConfigs")
|
|
35
|
+
response.code.to_i.should.equal 200
|
|
36
|
+
|
|
37
|
+
data = parse_json(response)
|
|
38
|
+
data["configs"].should.be.kind_of Array
|
|
39
|
+
data["configs"].length.should.equal 0
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
describe "when a task has both active and deleted push notification configs" do
|
|
44
|
+
it "should only return active configurations" do
|
|
45
|
+
task = create_task!(text: "Active vs deleted test")
|
|
46
|
+
|
|
47
|
+
# Create two, delete one
|
|
48
|
+
body1 = build_push_notification_config(task_id: task["id"], url: "https://example.com/keep")
|
|
49
|
+
body2 = build_push_notification_config(task_id: task["id"], url: "https://example.com/delete")
|
|
50
|
+
http_post("/tasks/#{task["id"]}/pushNotificationConfigs", body1)
|
|
51
|
+
create2_resp = http_post("/tasks/#{task["id"]}/pushNotificationConfigs", body2)
|
|
52
|
+
config2 = parse_json(create2_resp)
|
|
53
|
+
|
|
54
|
+
# Delete the second
|
|
55
|
+
http_delete("/tasks/#{task["id"]}/pushNotificationConfigs/#{config2["id"]}")
|
|
56
|
+
|
|
57
|
+
# List should only show one
|
|
58
|
+
response = http_get("/tasks/#{task["id"]}/pushNotificationConfigs")
|
|
59
|
+
data = parse_json(response)
|
|
60
|
+
|
|
61
|
+
data["configs"].length.should.equal 1
|
|
62
|
+
data["configs"][0]["url"].should.equal "https://example.com/keep"
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# --- Error Cases ---
|
|
67
|
+
|
|
68
|
+
describe "when a client sends a request with a non-existent task ID" do
|
|
69
|
+
it "should respond with a TaskNotFoundError" do
|
|
70
|
+
response = http_get("/tasks/nonexistent-#{SecureRandom.uuid}/pushNotificationConfigs")
|
|
71
|
+
|
|
72
|
+
if response.code.to_i >= 400
|
|
73
|
+
true.should.equal true
|
|
74
|
+
else
|
|
75
|
+
data = parse_json(response)
|
|
76
|
+
data.key?("error").should.equal true
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# NOTE: Commented out -- reference server supports push notifications
|
|
82
|
+
|
|
83
|
+
# describe "when the server does not support push notifications" do
|
|
84
|
+
# it "should respond with a PushNotificationNotSupportedError" do
|
|
85
|
+
# end
|
|
86
|
+
# end
|
|
87
|
+
|
|
88
|
+
# describe "when a client sends a request for a task not accessible to the client" do
|
|
89
|
+
# it "should respond with a TaskNotFoundError" do
|
|
90
|
+
# end
|
|
91
|
+
# end
|
|
92
|
+
end
|