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,178 @@
|
|
|
1
|
+
require "a2a_test_framework/test_helper"
|
|
2
|
+
|
|
3
|
+
# Cross-cutting: Error handling applies to all REST endpoints
|
|
4
|
+
|
|
5
|
+
describe "Error Handling (REST)" do
|
|
6
|
+
# --- Authentication Errors ---
|
|
7
|
+
# NOTE: Commented out -- reference server does not implement authentication
|
|
8
|
+
|
|
9
|
+
# describe "when a client sends a request without authentication credentials" do
|
|
10
|
+
# it "should reject the request with an authentication error" do
|
|
11
|
+
# end
|
|
12
|
+
#
|
|
13
|
+
# it "should include authentication challenge information in the error response" do
|
|
14
|
+
# end
|
|
15
|
+
#
|
|
16
|
+
# it "should specify which authentication scheme is required" do
|
|
17
|
+
# end
|
|
18
|
+
# end
|
|
19
|
+
|
|
20
|
+
# describe "when a client sends a request with invalid authentication credentials" do
|
|
21
|
+
# it "should reject the request with an authentication error" do
|
|
22
|
+
# end
|
|
23
|
+
# end
|
|
24
|
+
|
|
25
|
+
# describe "when a client sends a request with an expired bearer token" do
|
|
26
|
+
# it "should reject the request with an authentication error" do
|
|
27
|
+
# end
|
|
28
|
+
# end
|
|
29
|
+
|
|
30
|
+
# --- Authorization Errors ---
|
|
31
|
+
# NOTE: Commented out -- reference server does not implement authorization
|
|
32
|
+
|
|
33
|
+
# describe "when an authenticated client lacks required permissions for an operation" do
|
|
34
|
+
# it "should return an authorization error" do
|
|
35
|
+
# end
|
|
36
|
+
# end
|
|
37
|
+
|
|
38
|
+
# describe "when an authenticated client attempts to access another client's task" do
|
|
39
|
+
# it "should not reveal the existence of that task" do
|
|
40
|
+
# end
|
|
41
|
+
#
|
|
42
|
+
# it "should not distinguish between does-not-exist and not-authorized" do
|
|
43
|
+
# end
|
|
44
|
+
# end
|
|
45
|
+
|
|
46
|
+
# --- Validation Errors ---
|
|
47
|
+
|
|
48
|
+
describe "when a client sends a request with an invalid request body" do
|
|
49
|
+
it "should return an error response" do
|
|
50
|
+
# Send invalid JSON
|
|
51
|
+
uri = URI("#{BASE_URL}/message:send")
|
|
52
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
53
|
+
request = Net::HTTP::Post.new(uri.path)
|
|
54
|
+
request["Content-Type"] = "application/json"
|
|
55
|
+
request.body = "not valid json{{"
|
|
56
|
+
response = http.request(request)
|
|
57
|
+
|
|
58
|
+
response.code.to_i.should.be >= 400
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# --- Resource Errors ---
|
|
63
|
+
|
|
64
|
+
describe "when a client requests a resource that does not exist" do
|
|
65
|
+
it "should return a not found error" do
|
|
66
|
+
response = http_get("/tasks/nonexistent-#{SecureRandom.uuid}")
|
|
67
|
+
|
|
68
|
+
if response.code.to_i >= 400
|
|
69
|
+
true.should.equal true
|
|
70
|
+
else
|
|
71
|
+
data = parse_json(response)
|
|
72
|
+
data.key?("error").should.equal true
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# --- Error Payload Structure ---
|
|
78
|
+
|
|
79
|
+
describe "when any error response is returned" do
|
|
80
|
+
it "should contain an error with a code field" do
|
|
81
|
+
response = http_get("/tasks/nonexistent-#{SecureRandom.uuid}")
|
|
82
|
+
|
|
83
|
+
if response.code.to_i >= 400
|
|
84
|
+
data = parse_json(response)
|
|
85
|
+
if data.key?("error")
|
|
86
|
+
data["error"]["code"].should.not.be.nil
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
true.should.equal true
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
it "should contain a human-readable error message" do
|
|
93
|
+
response = http_get("/tasks/nonexistent-#{SecureRandom.uuid}")
|
|
94
|
+
|
|
95
|
+
if response.code.to_i >= 400
|
|
96
|
+
data = parse_json(response)
|
|
97
|
+
if data.key?("error")
|
|
98
|
+
data["error"]["message"].should.not.be.nil
|
|
99
|
+
data["error"]["message"].should.be.kind_of String
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
true.should.equal true
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# --- A2A-Specific Errors ---
|
|
107
|
+
|
|
108
|
+
describe "when a task ID does not correspond to an existing task" do
|
|
109
|
+
it "should return a TaskNotFoundError" do
|
|
110
|
+
response = http_get("/tasks/does-not-exist-#{SecureRandom.uuid}")
|
|
111
|
+
response.code.to_i.should.be >= 400
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
describe "when a CancelTask request targets a completed task" do
|
|
116
|
+
it "should return a TaskNotCancelableError" do
|
|
117
|
+
task = create_task!(text: "Error handling cancel test")
|
|
118
|
+
response = http_post("/tasks/#{task["id"]}:cancel", {})
|
|
119
|
+
response.code.to_i.should.be >= 400
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# --- System Errors ---
|
|
124
|
+
# NOTE: Commented out -- cannot easily trigger internal server errors
|
|
125
|
+
|
|
126
|
+
# describe "when the server experiences a temporary internal failure" do
|
|
127
|
+
# it "should return an appropriate error code indicating temporary unavailability" do
|
|
128
|
+
# end
|
|
129
|
+
# end
|
|
130
|
+
|
|
131
|
+
# --- Unsupported Operations ---
|
|
132
|
+
|
|
133
|
+
describe "when a client requests an unsupported operation" do
|
|
134
|
+
it "should return an error for GetExtendedAgentCard (not supported)" do
|
|
135
|
+
response = http_get("/extendedAgentCard")
|
|
136
|
+
|
|
137
|
+
if response.code.to_i >= 400
|
|
138
|
+
true.should.equal true
|
|
139
|
+
else
|
|
140
|
+
data = parse_json(response)
|
|
141
|
+
data.key?("error").should.equal true
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# --- Remaining A2A Errors ---
|
|
147
|
+
# NOTE: Commented out -- require specific server configurations
|
|
148
|
+
|
|
149
|
+
# describe "when a client attempts push notification features on a non-supporting server" do
|
|
150
|
+
# it "should return a PushNotificationNotSupportedError" do
|
|
151
|
+
# end
|
|
152
|
+
# end
|
|
153
|
+
|
|
154
|
+
# describe "when a client sends a message with an unsupported media type" do
|
|
155
|
+
# it "should return a ContentTypeNotSupportedError" do
|
|
156
|
+
# end
|
|
157
|
+
# end
|
|
158
|
+
|
|
159
|
+
# describe "when an agent returns a malformed response" do
|
|
160
|
+
# it "should be classified as InvalidAgentResponseError" do
|
|
161
|
+
# end
|
|
162
|
+
# end
|
|
163
|
+
|
|
164
|
+
# describe "when the extended agent card is not configured despite capability being declared" do
|
|
165
|
+
# it "should return an ExtendedAgentCardNotConfiguredError" do
|
|
166
|
+
# end
|
|
167
|
+
# end
|
|
168
|
+
|
|
169
|
+
# describe "when a client does not declare support for a required extension" do
|
|
170
|
+
# it "should return an ExtensionSupportRequiredError" do
|
|
171
|
+
# end
|
|
172
|
+
# end
|
|
173
|
+
|
|
174
|
+
# describe "when a client sends a request with an unsupported protocol version" do
|
|
175
|
+
# it "should return a VersionNotSupportedError" do
|
|
176
|
+
# end
|
|
177
|
+
# end
|
|
178
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
require "a2a_test_framework/test_helper"
|
|
2
|
+
|
|
3
|
+
# NOTE: All tests commented out -- Reference server does not implement extensions
|
|
4
|
+
|
|
5
|
+
# require "a2a_test_framework/test_helper"
|
|
6
|
+
|
|
7
|
+
# # NOTE: All tests commented out -- Reference server does not implement extensions
|
|
8
|
+
|
|
9
|
+
# # describe "Extension Versioning and Compatibility (REST)" do
|
|
10
|
+
# # # --- Version in URI ---
|
|
11
|
+
# #
|
|
12
|
+
# # describe "when extensions are declared in the AgentCard" do
|
|
13
|
+
# # it "should include version information in each extension URI" do
|
|
14
|
+
# # end
|
|
15
|
+
# # end
|
|
16
|
+
# #
|
|
17
|
+
# # describe "when a breaking change is made to an extension" do
|
|
18
|
+
# # it "should create a new URI for the incompatible version" do
|
|
19
|
+
# # end
|
|
20
|
+
# #
|
|
21
|
+
# # it "should not reuse the old URI for the incompatible version" do
|
|
22
|
+
# # end
|
|
23
|
+
# # end
|
|
24
|
+
# #
|
|
25
|
+
# # # --- Unsupported Extension Version Handling ---
|
|
26
|
+
# #
|
|
27
|
+
# # describe "when a client requests a non-required extension version the agent does not support" do
|
|
28
|
+
# # it "should ignore the extension for that interaction" do
|
|
29
|
+
# # end
|
|
30
|
+
# #
|
|
31
|
+
# # it "should proceed without it" do
|
|
32
|
+
# # end
|
|
33
|
+
# # end
|
|
34
|
+
# #
|
|
35
|
+
# # describe "when a client requests a required extension version the agent does not support" do
|
|
36
|
+
# # it "should return an error indicating unsupported extension" do
|
|
37
|
+
# # end
|
|
38
|
+
# # end
|
|
39
|
+
# #
|
|
40
|
+
# # describe "when a client requests a newer extension version" do
|
|
41
|
+
# # it "should not fall back to a previous extension version automatically" do
|
|
42
|
+
# # end
|
|
43
|
+
# # end
|
|
44
|
+
# # end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
require "a2a_test_framework/test_helper"
|
|
2
|
+
|
|
3
|
+
# Cross-cutting: Field presence and optionality applies to all REST endpoints
|
|
4
|
+
|
|
5
|
+
describe "Field Presence and Optionality (REST)" do
|
|
6
|
+
# --- Required Fields ---
|
|
7
|
+
|
|
8
|
+
describe "when a client sends a message with all required fields present" do
|
|
9
|
+
it "should accept the message as valid" do
|
|
10
|
+
body = build_send_message_request(text: "All fields present")
|
|
11
|
+
response = http_post("/message:send", body)
|
|
12
|
+
response.code.to_i.should.equal 200
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
describe "when a client sends a message with a required field missing" do
|
|
17
|
+
it "should reject the message with an error" do
|
|
18
|
+
# Send request without message field (required)
|
|
19
|
+
response = http_post("/message:send", {})
|
|
20
|
+
response.code.to_i.should.be >= 400
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
describe "when a client sends a message with parts as empty array" do
|
|
25
|
+
it "should reject or handle gracefully" do
|
|
26
|
+
body = {
|
|
27
|
+
"message" => {
|
|
28
|
+
"messageId" => SecureRandom.uuid,
|
|
29
|
+
"role" => "ROLE_USER",
|
|
30
|
+
"parts" => []
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
response = http_post("/message:send", body)
|
|
34
|
+
# Server should either reject (400) or handle gracefully
|
|
35
|
+
(response.code.to_i >= 200 && response.code.to_i < 500).should.equal true
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# --- Unrecognized Fields ---
|
|
40
|
+
|
|
41
|
+
describe "when a client sends a request with an unrecognized extra field" do
|
|
42
|
+
it "should ignore the unrecognized field and process normally" do
|
|
43
|
+
body = build_send_message_request(text: "Extra field test")
|
|
44
|
+
body["unknownExtraField"] = "should be ignored"
|
|
45
|
+
body["anotherUnknown"] = 42
|
|
46
|
+
|
|
47
|
+
response = http_post("/message:send", body)
|
|
48
|
+
response.code.to_i.should.equal 200
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
describe "when a client sends fields from a future protocol version" do
|
|
53
|
+
it "should process the request based on known fields" do
|
|
54
|
+
body = build_send_message_request(text: "Future compat test")
|
|
55
|
+
body["futureField_v99"] = { "data" => "from the future" }
|
|
56
|
+
|
|
57
|
+
response = http_post("/message:send", body)
|
|
58
|
+
response.code.to_i.should.equal 200
|
|
59
|
+
|
|
60
|
+
data = parse_json(response)
|
|
61
|
+
(data.key?("task") || data.key?("message")).should.equal true
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
require "a2a_test_framework/test_helper"
|
|
2
|
+
|
|
3
|
+
# NOTE: All tests commented out -- Requires testing across multiple protocol bindings simultaneously
|
|
4
|
+
|
|
5
|
+
# require "a2a_test_framework/test_helper"
|
|
6
|
+
|
|
7
|
+
# # NOTE: All tests commented out -- Requires testing across multiple protocol bindings simultaneously
|
|
8
|
+
|
|
9
|
+
# # describe "Functional Equivalence Requirements (REST)" do
|
|
10
|
+
# # describe "when the agent supports multiple protocol bindings" do
|
|
11
|
+
# # it "should provide the same set of operations and capabilities across all bindings" do
|
|
12
|
+
# # end
|
|
13
|
+
# #
|
|
14
|
+
# # it "should return semantically equivalent results for the same request across bindings" do
|
|
15
|
+
# # end
|
|
16
|
+
# #
|
|
17
|
+
# # it "should map errors consistently using appropriate protocol-specific codes" do
|
|
18
|
+
# # end
|
|
19
|
+
# #
|
|
20
|
+
# # it "should support the same authentication schemes across all bindings" do
|
|
21
|
+
# # end
|
|
22
|
+
# # end
|
|
23
|
+
# # end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
require "a2a_test_framework/test_helper"
|
|
2
|
+
|
|
3
|
+
# REST endpoint: GET /extendedAgentCard
|
|
4
|
+
# Request: GetExtendedAgentCardRequest (tenant)
|
|
5
|
+
# Response: AgentCard
|
|
6
|
+
#
|
|
7
|
+
# NOTE: The reference server declares extendedAgentCard=false, so this
|
|
8
|
+
# endpoint returns an UnsupportedOperationError.
|
|
9
|
+
|
|
10
|
+
describe "GET /extendedAgentCard" do
|
|
11
|
+
# --- Authentication Requirement ---
|
|
12
|
+
# NOTE: Commented out -- reference server does not implement auth
|
|
13
|
+
|
|
14
|
+
# describe "when a client sends an authenticated request using a declared security scheme" do
|
|
15
|
+
# it "should accept the request" do
|
|
16
|
+
# end
|
|
17
|
+
# end
|
|
18
|
+
|
|
19
|
+
# describe "when a client sends a request without authentication" do
|
|
20
|
+
# it "should reject the request with an authentication error" do
|
|
21
|
+
# end
|
|
22
|
+
# end
|
|
23
|
+
|
|
24
|
+
# describe "when a client sends a request using an undeclared security scheme" do
|
|
25
|
+
# it "should reject the request" do
|
|
26
|
+
# end
|
|
27
|
+
# end
|
|
28
|
+
|
|
29
|
+
# --- Successful Retrieval ---
|
|
30
|
+
# NOTE: Commented out -- reference server doesn't support extended card
|
|
31
|
+
|
|
32
|
+
# describe "when an authenticated client retrieves the extended agent card" do
|
|
33
|
+
# it "should respond with a complete AgentCard object" do
|
|
34
|
+
# end
|
|
35
|
+
# end
|
|
36
|
+
|
|
37
|
+
# --- Availability / Capability Validation ---
|
|
38
|
+
|
|
39
|
+
describe "when the AgentCard declares extendedAgentCard capability as false" do
|
|
40
|
+
it "should respond with an error" do
|
|
41
|
+
response = http_get("/extendedAgentCard")
|
|
42
|
+
|
|
43
|
+
if response.code.to_i >= 400
|
|
44
|
+
true.should.equal true
|
|
45
|
+
else
|
|
46
|
+
data = parse_json(response)
|
|
47
|
+
data.key?("error").should.equal true
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# --- Card Replacement Behavior ---
|
|
53
|
+
# NOTE: Commented out -- not testable without extended card support
|
|
54
|
+
|
|
55
|
+
# describe "when a client retrieves the extended agent card after caching the public card" do
|
|
56
|
+
# it "should replace the cached public AgentCard with the extended card" do
|
|
57
|
+
# end
|
|
58
|
+
# end
|
|
59
|
+
|
|
60
|
+
# --- Error Cases ---
|
|
61
|
+
# NOTE: Commented out -- reference server responds with unsupported
|
|
62
|
+
|
|
63
|
+
# describe "when the agent declares support but has no extended card configured" do
|
|
64
|
+
# it "should respond with an ExtendedAgentCardNotConfiguredError" do
|
|
65
|
+
# end
|
|
66
|
+
# end
|
|
67
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
require "a2a_test_framework/test_helper"
|
|
2
|
+
|
|
3
|
+
# REST endpoint: GET /tasks/{task_id}/pushNotificationConfigs/{id}
|
|
4
|
+
# Request: GetTaskPushNotificationConfigRequest (taskId, id, tenant)
|
|
5
|
+
# Response: TaskPushNotificationConfig
|
|
6
|
+
|
|
7
|
+
describe "GET /tasks/{task_id}/pushNotificationConfigs/{id}" do
|
|
8
|
+
# --- Successful Retrieval ---
|
|
9
|
+
|
|
10
|
+
describe "when a client retrieves an existing push notification config" do
|
|
11
|
+
it "should respond with a PushNotificationConfig object" do
|
|
12
|
+
task = create_task!(text: "Get config test")
|
|
13
|
+
# Create a config first
|
|
14
|
+
body = build_push_notification_config(
|
|
15
|
+
task_id: task["id"],
|
|
16
|
+
url: "https://example.com/get-hook",
|
|
17
|
+
token: "get-token"
|
|
18
|
+
)
|
|
19
|
+
create_response = http_post("/tasks/#{task["id"]}/pushNotificationConfigs", body)
|
|
20
|
+
created = parse_json(create_response)
|
|
21
|
+
config_id = created["id"]
|
|
22
|
+
|
|
23
|
+
# Now retrieve it
|
|
24
|
+
response = http_get("/tasks/#{task["id"]}/pushNotificationConfigs/#{config_id}")
|
|
25
|
+
response.code.to_i.should.equal 200
|
|
26
|
+
|
|
27
|
+
data = parse_json(response)
|
|
28
|
+
data["id"].should.equal config_id
|
|
29
|
+
data["url"].should.equal "https://example.com/get-hook"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it "should include the webhook URL in the configuration details" do
|
|
33
|
+
task = create_task!(text: "URL check test")
|
|
34
|
+
body = build_push_notification_config(
|
|
35
|
+
task_id: task["id"],
|
|
36
|
+
url: "https://example.com/url-check"
|
|
37
|
+
)
|
|
38
|
+
create_response = http_post("/tasks/#{task["id"]}/pushNotificationConfigs", body)
|
|
39
|
+
created = parse_json(create_response)
|
|
40
|
+
|
|
41
|
+
response = http_get("/tasks/#{task["id"]}/pushNotificationConfigs/#{created["id"]}")
|
|
42
|
+
data = parse_json(response)
|
|
43
|
+
|
|
44
|
+
data["url"].should.equal "https://example.com/url-check"
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# --- Error Cases ---
|
|
49
|
+
|
|
50
|
+
describe "when a client requests a non-existent config ID" do
|
|
51
|
+
it "should respond with an error" do
|
|
52
|
+
task = create_task!(text: "Missing config test")
|
|
53
|
+
response = http_get("/tasks/#{task["id"]}/pushNotificationConfigs/nonexistent-#{SecureRandom.uuid}")
|
|
54
|
+
|
|
55
|
+
if response.code.to_i >= 400
|
|
56
|
+
true.should.equal true
|
|
57
|
+
else
|
|
58
|
+
data = parse_json(response)
|
|
59
|
+
data.key?("error").should.equal true
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# NOTE: Commented out -- reference server doesn't implement auth
|
|
65
|
+
|
|
66
|
+
# describe "when the server does not support push notifications" do
|
|
67
|
+
# it "should respond with a PushNotificationNotSupportedError" do
|
|
68
|
+
# end
|
|
69
|
+
# end
|
|
70
|
+
|
|
71
|
+
# describe "when a client lacks access to the configuration" do
|
|
72
|
+
# it "should respond with an error" do
|
|
73
|
+
# end
|
|
74
|
+
# end
|
|
75
|
+
end
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
require "a2a_test_framework/test_helper"
|
|
2
|
+
|
|
3
|
+
# REST endpoint: GET /tasks/{id}
|
|
4
|
+
# Request: GetTaskRequest (id, historyLength, tenant)
|
|
5
|
+
# Response: Task
|
|
6
|
+
|
|
7
|
+
describe "GET /tasks/{id}" do
|
|
8
|
+
# --- Successful Retrieval ---
|
|
9
|
+
|
|
10
|
+
describe "when a client retrieves an existing task" do
|
|
11
|
+
it "should respond with HTTP 200 and a Task object" do
|
|
12
|
+
task = create_task!(text: "Task for retrieval")
|
|
13
|
+
response = http_get("/tasks/#{task["id"]}")
|
|
14
|
+
response.code.to_i.should.equal 200
|
|
15
|
+
|
|
16
|
+
data = parse_json(response)
|
|
17
|
+
data["id"].should.equal task["id"]
|
|
18
|
+
data.should.be.kind_of Hash
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it "should include the current status in the Task object" do
|
|
22
|
+
task = create_task!(text: "Status check")
|
|
23
|
+
response = http_get("/tasks/#{task["id"]}")
|
|
24
|
+
data = parse_json(response)
|
|
25
|
+
|
|
26
|
+
data["status"].should.not.be.nil
|
|
27
|
+
data["status"].should.be.kind_of Hash
|
|
28
|
+
data["status"]["state"].should.not.be.nil
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it "should include any generated artifacts in the Task object" do
|
|
32
|
+
task = create_task!(text: "Artifact check")
|
|
33
|
+
response = http_get("/tasks/#{task["id"]}")
|
|
34
|
+
data = parse_json(response)
|
|
35
|
+
|
|
36
|
+
if data["artifacts"]
|
|
37
|
+
data["artifacts"].should.be.kind_of Array
|
|
38
|
+
data["artifacts"].each do |artifact|
|
|
39
|
+
artifact["artifactId"].should.not.be.nil
|
|
40
|
+
artifact["parts"].should.be.kind_of Array
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
true.should.equal true
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
it "should include contextId in the Task object" do
|
|
47
|
+
task = create_task!(text: "Context check")
|
|
48
|
+
response = http_get("/tasks/#{task["id"]}")
|
|
49
|
+
data = parse_json(response)
|
|
50
|
+
|
|
51
|
+
data["contextId"].should.not.be.nil
|
|
52
|
+
data["contextId"].should.be.kind_of String
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
describe "when a task has completed" do
|
|
57
|
+
it "should return a status with state TASK_STATE_COMPLETED" do
|
|
58
|
+
task = create_task!(text: "Completed task")
|
|
59
|
+
response = http_get("/tasks/#{task["id"]}")
|
|
60
|
+
data = parse_json(response)
|
|
61
|
+
|
|
62
|
+
data["status"]["state"].should.equal "TASK_STATE_COMPLETED"
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# --- History Length Parameter ---
|
|
67
|
+
|
|
68
|
+
describe "when a client sends a GetTask request with historyLength" do
|
|
69
|
+
it "should include history messages when historyLength is positive" do
|
|
70
|
+
task = create_task!(text: "History test")
|
|
71
|
+
response = http_get("/tasks/#{task["id"]}?historyLength=10")
|
|
72
|
+
data = parse_json(response)
|
|
73
|
+
|
|
74
|
+
if data["history"]
|
|
75
|
+
data["history"].should.be.kind_of Array
|
|
76
|
+
data["history"].length.should.be > 0
|
|
77
|
+
end
|
|
78
|
+
true.should.equal true
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
it "should omit history when historyLength is 0" do
|
|
82
|
+
task = create_task!(text: "No history test")
|
|
83
|
+
response = http_get("/tasks/#{task["id"]}?historyLength=0")
|
|
84
|
+
data = parse_json(response)
|
|
85
|
+
|
|
86
|
+
# When historyLength=0, history should be omitted or empty
|
|
87
|
+
if data.key?("history")
|
|
88
|
+
(data["history"].nil? || data["history"].empty?).should.equal true
|
|
89
|
+
else
|
|
90
|
+
true.should.equal true
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
it "should limit history to the specified number of messages" do
|
|
95
|
+
# Create a task with some history
|
|
96
|
+
task = create_task!(text: "Multi-message task")
|
|
97
|
+
response = http_get("/tasks/#{task["id"]}?historyLength=1")
|
|
98
|
+
data = parse_json(response)
|
|
99
|
+
|
|
100
|
+
if data["history"]
|
|
101
|
+
data["history"].length.should.be <= 1
|
|
102
|
+
end
|
|
103
|
+
true.should.equal true
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# --- Error Cases ---
|
|
108
|
+
|
|
109
|
+
describe "when a client sends a GetTask request with a non-existent task ID" do
|
|
110
|
+
it "should respond with an error (404 or error body)" do
|
|
111
|
+
response = http_get("/tasks/nonexistent-task-id-#{SecureRandom.uuid}")
|
|
112
|
+
|
|
113
|
+
if response.code.to_i >= 400
|
|
114
|
+
true.should.equal true
|
|
115
|
+
else
|
|
116
|
+
data = parse_json(response)
|
|
117
|
+
data.key?("error").should.equal true
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
describe "when a client sends a GetTask request for a task not accessible to the client" do
|
|
123
|
+
it "should respond with an error indistinguishable from not-found" do
|
|
124
|
+
response = http_get("/tasks/inaccessible-task-#{SecureRandom.uuid}")
|
|
125
|
+
|
|
126
|
+
if response.code.to_i >= 400
|
|
127
|
+
true.should.equal true
|
|
128
|
+
else
|
|
129
|
+
data = parse_json(response)
|
|
130
|
+
data.key?("error").should.equal true
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
require "a2a_test_framework/test_helper"
|
|
2
|
+
|
|
3
|
+
# Cross-cutting: historyLength semantics apply to:
|
|
4
|
+
# REST: GET /tasks/{id}, GET /tasks, POST /message:send
|
|
5
|
+
|
|
6
|
+
describe "History Length Semantics (REST)" do
|
|
7
|
+
# --- Unset/Undefined ---
|
|
8
|
+
|
|
9
|
+
describe "when a client sends a request without specifying historyLength" do
|
|
10
|
+
it "should return the server default amount of history" do
|
|
11
|
+
task = create_task!(text: "Default history test")
|
|
12
|
+
response = http_get("/tasks/#{task["id"]}")
|
|
13
|
+
data = parse_json(response)
|
|
14
|
+
|
|
15
|
+
# Server should return history by default
|
|
16
|
+
if data["history"]
|
|
17
|
+
data["history"].should.be.kind_of Array
|
|
18
|
+
end
|
|
19
|
+
true.should.equal true
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# --- Zero Value ---
|
|
24
|
+
|
|
25
|
+
describe "when a client sends a request with historyLength set to 0" do
|
|
26
|
+
it "should omit or empty the history field" do
|
|
27
|
+
task = create_task!(text: "Zero history test")
|
|
28
|
+
response = http_get("/tasks/#{task["id"]}?historyLength=0")
|
|
29
|
+
data = parse_json(response)
|
|
30
|
+
|
|
31
|
+
if data.key?("history")
|
|
32
|
+
(data["history"].nil? || data["history"].empty?).should.equal true
|
|
33
|
+
else
|
|
34
|
+
true.should.equal true
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# --- Positive Value ---
|
|
40
|
+
|
|
41
|
+
describe "when a client sends a request with historyLength set to a positive number" do
|
|
42
|
+
it "should return at most that many recent messages" do
|
|
43
|
+
task = create_task!(text: "Limited history test")
|
|
44
|
+
response = http_get("/tasks/#{task["id"]}?historyLength=1")
|
|
45
|
+
data = parse_json(response)
|
|
46
|
+
|
|
47
|
+
if data["history"]
|
|
48
|
+
data["history"].length.should.be <= 1
|
|
49
|
+
end
|
|
50
|
+
true.should.equal true
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
describe "when historyLength is greater than total available messages" do
|
|
55
|
+
it "should return all available messages" do
|
|
56
|
+
task = create_task!(text: "Large history test")
|
|
57
|
+
response = http_get("/tasks/#{task["id"]}?historyLength=1000")
|
|
58
|
+
data = parse_json(response)
|
|
59
|
+
|
|
60
|
+
if data["history"]
|
|
61
|
+
data["history"].should.be.kind_of Array
|
|
62
|
+
# Should return whatever is available, not error
|
|
63
|
+
end
|
|
64
|
+
true.should.equal true
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# --- Consistency Across Operations ---
|
|
69
|
+
|
|
70
|
+
describe "when historyLength is used in GetTask vs ListTasks" do
|
|
71
|
+
it "should apply the same truncation semantics in both operations" do
|
|
72
|
+
task = create_task!(text: "Consistency test")
|
|
73
|
+
|
|
74
|
+
# GetTask with historyLength=1
|
|
75
|
+
get_resp = http_get("/tasks/#{task["id"]}?historyLength=1")
|
|
76
|
+
get_data = parse_json(get_resp)
|
|
77
|
+
|
|
78
|
+
# ListTasks with historyLength=1
|
|
79
|
+
list_resp = http_get("/tasks?historyLength=1&contextId=#{task["contextId"]}")
|
|
80
|
+
list_data = parse_json(list_resp)
|
|
81
|
+
|
|
82
|
+
if get_data["history"] && list_data["tasks"].length > 0
|
|
83
|
+
list_task = list_data["tasks"].find { |t| t["id"] == task["id"] }
|
|
84
|
+
if list_task && list_task["history"]
|
|
85
|
+
list_task["history"].length.should.be <= 1
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
true.should.equal true
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|