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,101 @@
|
|
|
1
|
+
require "a2a_test_framework/test_helper"
|
|
2
|
+
|
|
3
|
+
# NOTE: All tests commented out -- Reference server does not implement security features
|
|
4
|
+
|
|
5
|
+
# require "a2a_test_framework/test_helper"
|
|
6
|
+
|
|
7
|
+
# # NOTE: All tests commented out -- Reference server does not implement security features
|
|
8
|
+
|
|
9
|
+
# # describe "Security Considerations (REST)" do
|
|
10
|
+
# # # --- Data Access and Authorization Scoping ---
|
|
11
|
+
# #
|
|
12
|
+
# # describe "when any request is received" do
|
|
13
|
+
# # it "should implement authorization checks on every operation request" do
|
|
14
|
+
# # end
|
|
15
|
+
# #
|
|
16
|
+
# # it "should scope results to the caller's authorized access boundaries" do
|
|
17
|
+
# # end
|
|
18
|
+
# #
|
|
19
|
+
# # it "should scope results even without filter parameters" do
|
|
20
|
+
# # end
|
|
21
|
+
# # end
|
|
22
|
+
# #
|
|
23
|
+
# # describe "when a client sends a ListTasks request" do
|
|
24
|
+
# # it "should only return tasks visible to the authenticated client" do
|
|
25
|
+
# # end
|
|
26
|
+
# # end
|
|
27
|
+
# #
|
|
28
|
+
# # describe "when a client sends a GetTask request" do
|
|
29
|
+
# # it "should verify the client has access to the requested task" do
|
|
30
|
+
# # end
|
|
31
|
+
# # end
|
|
32
|
+
# #
|
|
33
|
+
# # describe "when authorization checks are performed" do
|
|
34
|
+
# # it "should occur before any database queries" do
|
|
35
|
+
# # end
|
|
36
|
+
# #
|
|
37
|
+
# # it "should not leak information about resources outside the caller's scope" do
|
|
38
|
+
# # end
|
|
39
|
+
# # end
|
|
40
|
+
# #
|
|
41
|
+
# # # --- Push Notification Security ---
|
|
42
|
+
# #
|
|
43
|
+
# # describe "when the agent sends webhook notifications" do
|
|
44
|
+
# # it "should include authentication credentials as specified in PushNotificationConfig" do
|
|
45
|
+
# # end
|
|
46
|
+
# # end
|
|
47
|
+
# #
|
|
48
|
+
# # describe "when a client creates a push notification config with a webhook URL" do
|
|
49
|
+
# # it "should validate the URL to prevent SSRF attacks" do
|
|
50
|
+
# # end
|
|
51
|
+
# #
|
|
52
|
+
# # it "should reject private IP ranges for webhooks" do
|
|
53
|
+
# # end
|
|
54
|
+
# # end
|
|
55
|
+
# #
|
|
56
|
+
# # describe "when a client receives a webhook request" do
|
|
57
|
+
# # it "should validate webhook authenticity using authentication credentials" do
|
|
58
|
+
# # end
|
|
59
|
+
# #
|
|
60
|
+
# # it "should respond with HTTP 2xx for successful receipt" do
|
|
61
|
+
# # end
|
|
62
|
+
# # end
|
|
63
|
+
# #
|
|
64
|
+
# # describe "when configuring webhook URLs" do
|
|
65
|
+
# # it "should use HTTPS to protect payload confidentiality" do
|
|
66
|
+
# # end
|
|
67
|
+
# # end
|
|
68
|
+
# #
|
|
69
|
+
# # # --- Extended Agent Card Access Control ---
|
|
70
|
+
# #
|
|
71
|
+
# # describe "when a client requests the extended agent card" do
|
|
72
|
+
# # it "should require authentication" do
|
|
73
|
+
# # end
|
|
74
|
+
# #
|
|
75
|
+
# # it "should validate client permissions before returning privileged info" do
|
|
76
|
+
# # end
|
|
77
|
+
# # end
|
|
78
|
+
# #
|
|
79
|
+
# # # --- General Security Best Practices ---
|
|
80
|
+
# #
|
|
81
|
+
# # describe "when handling requests in production" do
|
|
82
|
+
# # it "should use encrypted communication" do
|
|
83
|
+
# # end
|
|
84
|
+
# #
|
|
85
|
+
# # it "should validate all input parameters before processing" do
|
|
86
|
+
# # end
|
|
87
|
+
# # end
|
|
88
|
+
# #
|
|
89
|
+
# # describe "when handling credentials" do
|
|
90
|
+
# # it "should treat API keys, tokens, and credentials as secrets" do
|
|
91
|
+
# # end
|
|
92
|
+
# #
|
|
93
|
+
# # it "should not include sensitive information in logs" do
|
|
94
|
+
# # end
|
|
95
|
+
# # end
|
|
96
|
+
# #
|
|
97
|
+
# # describe "when handling personal data" do
|
|
98
|
+
# # it "should comply with applicable data protection regulations" do
|
|
99
|
+
# # end
|
|
100
|
+
# # end
|
|
101
|
+
# # end
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
require "a2a_test_framework/test_helper"
|
|
2
|
+
|
|
3
|
+
# REST endpoint: POST /message:send
|
|
4
|
+
# Request: SendMessageRequest (message, configuration, metadata)
|
|
5
|
+
# Response: SendMessageResponse (task | message)
|
|
6
|
+
|
|
7
|
+
describe "POST /message:send" do
|
|
8
|
+
# --- Response Type Behavior ---
|
|
9
|
+
|
|
10
|
+
describe "when a client sends a SendMessageRequest with a valid message" do
|
|
11
|
+
it "should respond with HTTP 200" do
|
|
12
|
+
body = build_send_message_request(text: "Hello")
|
|
13
|
+
response = http_post("/message:send", body)
|
|
14
|
+
response.code.to_i.should.equal 200
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it "should respond with a Task object containing a valid task ID and status" do
|
|
18
|
+
body = build_send_message_request(text: "Hello")
|
|
19
|
+
response = http_post("/message:send", body)
|
|
20
|
+
data = parse_json(response)
|
|
21
|
+
|
|
22
|
+
if data.key?("task")
|
|
23
|
+
data["task"].should.be.kind_of Hash
|
|
24
|
+
data["task"]["id"].should.not.be.nil
|
|
25
|
+
data["task"]["id"].should.be.kind_of String
|
|
26
|
+
data["task"]["id"].length.should.be > 0
|
|
27
|
+
data["task"]["status"].should.not.be.nil
|
|
28
|
+
data["task"]["status"].should.be.kind_of Hash
|
|
29
|
+
data["task"]["status"]["state"].should.not.be.nil
|
|
30
|
+
end
|
|
31
|
+
true.should.equal true
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it "should return either a Task or a Message, never both" do
|
|
35
|
+
body = build_send_message_request(text: "Hello")
|
|
36
|
+
response = http_post("/message:send", body)
|
|
37
|
+
data = parse_json(response)
|
|
38
|
+
|
|
39
|
+
has_task = data.key?("task") && !data["task"].nil?
|
|
40
|
+
has_message = data.key?("message") && !data["message"].nil?
|
|
41
|
+
|
|
42
|
+
(has_task || has_message).should.equal true
|
|
43
|
+
(has_task && has_message).should.equal false
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
it "should return immediately without blocking indefinitely" do
|
|
47
|
+
body = build_send_message_request(text: "Quick test")
|
|
48
|
+
start_time = Time.now
|
|
49
|
+
response = http_post("/message:send", body)
|
|
50
|
+
elapsed = Time.now - start_time
|
|
51
|
+
|
|
52
|
+
response.code.to_i.should.equal 200
|
|
53
|
+
elapsed.should.be < 30
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
describe "when the server creates a Task to process the message" do
|
|
58
|
+
it "should return a Task with a valid state" do
|
|
59
|
+
body = build_send_message_request(text: "Test task creation")
|
|
60
|
+
response = http_post("/message:send", body)
|
|
61
|
+
data = parse_json(response)
|
|
62
|
+
|
|
63
|
+
if data.key?("task")
|
|
64
|
+
state = data["task"]["status"]["state"]
|
|
65
|
+
valid_states = %w[
|
|
66
|
+
TASK_STATE_SUBMITTED TASK_STATE_WORKING TASK_STATE_COMPLETED
|
|
67
|
+
TASK_STATE_FAILED TASK_STATE_CANCELED TASK_STATE_INPUT_REQUIRED
|
|
68
|
+
TASK_STATE_AUTH_REQUIRED TASK_STATE_REJECTED
|
|
69
|
+
]
|
|
70
|
+
valid_states.should.include state
|
|
71
|
+
end
|
|
72
|
+
true.should.equal true
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
it "should include a contextId in the Task" do
|
|
76
|
+
body = build_send_message_request(text: "Context test")
|
|
77
|
+
response = http_post("/message:send", body)
|
|
78
|
+
data = parse_json(response)
|
|
79
|
+
|
|
80
|
+
if data.key?("task")
|
|
81
|
+
data["task"]["contextId"].should.not.be.nil
|
|
82
|
+
data["task"]["contextId"].should.be.kind_of String
|
|
83
|
+
data["task"]["contextId"].length.should.be > 0
|
|
84
|
+
end
|
|
85
|
+
true.should.equal true
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# --- Error Cases ---
|
|
90
|
+
|
|
91
|
+
describe "when a client sends a SendMessageRequest referencing a non-existent task ID" do
|
|
92
|
+
it "should respond with an error" do
|
|
93
|
+
body = build_send_message_request(text: "Hello")
|
|
94
|
+
body["message"]["taskId"] = "nonexistent-task-id-#{SecureRandom.uuid}"
|
|
95
|
+
response = http_post("/message:send", body)
|
|
96
|
+
|
|
97
|
+
# Should get an error (4xx or error in body)
|
|
98
|
+
if response.code.to_i >= 400
|
|
99
|
+
true.should.equal true
|
|
100
|
+
else
|
|
101
|
+
data = parse_json(response)
|
|
102
|
+
data.key?("error").should.equal true
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
describe "when a client sends a SendMessageRequest referencing a completed task" do
|
|
108
|
+
it "should respond with an error indicating unsupported operation" do
|
|
109
|
+
# Create and complete a task first
|
|
110
|
+
task = create_task!(text: "Complete me")
|
|
111
|
+
|
|
112
|
+
if task["status"]["state"] == "TASK_STATE_COMPLETED"
|
|
113
|
+
# Send another message referencing the completed task
|
|
114
|
+
body = build_send_message_request(text: "Follow up")
|
|
115
|
+
body["message"]["taskId"] = task["id"]
|
|
116
|
+
response = http_post("/message:send", body)
|
|
117
|
+
|
|
118
|
+
if response.code.to_i >= 400
|
|
119
|
+
true.should.equal true
|
|
120
|
+
else
|
|
121
|
+
data = parse_json(response)
|
|
122
|
+
data.key?("error").should.equal true
|
|
123
|
+
end
|
|
124
|
+
else
|
|
125
|
+
true.should.equal true
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# --- Request Structure ---
|
|
131
|
+
|
|
132
|
+
describe "when validating request structure" do
|
|
133
|
+
it "should accept a request with only a message field" do
|
|
134
|
+
body = build_send_message_request(text: "Minimal request")
|
|
135
|
+
response = http_post("/message:send", body)
|
|
136
|
+
response.code.to_i.should.equal 200
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
it "should accept a request with message and metadata" do
|
|
140
|
+
body = build_send_message_request(text: "With metadata", metadata: { "key" => "value" })
|
|
141
|
+
response = http_post("/message:send", body)
|
|
142
|
+
response.code.to_i.should.equal 200
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# --- HTTP Method and Path ---
|
|
147
|
+
|
|
148
|
+
describe "when verifying HTTP method and path" do
|
|
149
|
+
it "should respond to POST on /message:send" do
|
|
150
|
+
body = build_send_message_request(text: "POST test")
|
|
151
|
+
response = http_post("/message:send", body)
|
|
152
|
+
response.code.to_i.should.not.equal 404
|
|
153
|
+
response.code.to_i.should.not.equal 405
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
it "should reject GET requests on /message:send" do
|
|
157
|
+
response = http_get("/message:send")
|
|
158
|
+
response.code.to_i.should.equal 405
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# --- Content-Type Handling ---
|
|
163
|
+
|
|
164
|
+
describe "when verifying content-type handling" do
|
|
165
|
+
it "should return application/json or application/a2a+json Content-Type in response" do
|
|
166
|
+
body = build_send_message_request(text: "Content-Type test")
|
|
167
|
+
response = http_post("/message:send", body)
|
|
168
|
+
content_type = response["Content-Type"].to_s
|
|
169
|
+
(content_type.include?("application/json") || content_type.include?("application/a2a+json")).should.equal true
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# --- SendMessageConfiguration: Blocking Mode ---
|
|
174
|
+
# NOTE: The reference server completes tasks synchronously, so blocking/non-blocking
|
|
175
|
+
# behavior is effectively the same. These tests verify the response is valid.
|
|
176
|
+
|
|
177
|
+
describe "when return_immediately is not set (blocking mode default)" do
|
|
178
|
+
it "should return a task in a terminal or actionable state" do
|
|
179
|
+
body = build_send_message_request(text: "Blocking test")
|
|
180
|
+
response = http_post("/message:send", body)
|
|
181
|
+
data = parse_json(response)
|
|
182
|
+
|
|
183
|
+
if data.key?("task")
|
|
184
|
+
state = data["task"]["status"]["state"]
|
|
185
|
+
# In blocking mode, should return terminal or actionable state
|
|
186
|
+
actionable_states = %w[
|
|
187
|
+
TASK_STATE_COMPLETED TASK_STATE_FAILED TASK_STATE_CANCELED
|
|
188
|
+
TASK_STATE_REJECTED TASK_STATE_INPUT_REQUIRED TASK_STATE_AUTH_REQUIRED
|
|
189
|
+
]
|
|
190
|
+
actionable_states.should.include state
|
|
191
|
+
end
|
|
192
|
+
true.should.equal true
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# --- SendMessageConfiguration: Non-Blocking Mode ---
|
|
197
|
+
# NOTE: Commented out -- reference server completes synchronously
|
|
198
|
+
|
|
199
|
+
# describe "when a client sends a SendMessageRequest with return_immediately set to true" do
|
|
200
|
+
# it "should return immediately after creating the task" do
|
|
201
|
+
# end
|
|
202
|
+
#
|
|
203
|
+
# it "should not wait for the task to reach a terminal state" do
|
|
204
|
+
# end
|
|
205
|
+
#
|
|
206
|
+
# it "should return a task in an in-progress state" do
|
|
207
|
+
# end
|
|
208
|
+
#
|
|
209
|
+
# it "should require the client to poll for updates using GetTask or subscribe" do
|
|
210
|
+
# end
|
|
211
|
+
# end
|
|
212
|
+
|
|
213
|
+
# --- SendMessageConfiguration: No Effect Cases ---
|
|
214
|
+
# NOTE: Commented out -- requires async server behavior
|
|
215
|
+
|
|
216
|
+
# describe "when return_immediately is set but the agent returns a direct Message" do
|
|
217
|
+
# it "should have no effect on the response behavior" do
|
|
218
|
+
# end
|
|
219
|
+
# end
|
|
220
|
+
|
|
221
|
+
# describe "when return_immediately is set on a streaming operation" do
|
|
222
|
+
# it "should have no effect on the streaming behavior" do
|
|
223
|
+
# end
|
|
224
|
+
# end
|
|
225
|
+
|
|
226
|
+
# describe "when return_immediately is set and push notifications are configured" do
|
|
227
|
+
# it "should operate push notification delivery independently of execution mode" do
|
|
228
|
+
# end
|
|
229
|
+
# end
|
|
230
|
+
end
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
require "a2a_test_framework/test_helper"
|
|
2
|
+
require "a2a_test_framework/sse_client"
|
|
3
|
+
|
|
4
|
+
# REST endpoint: POST /message:stream
|
|
5
|
+
# Request: SendMessageRequest
|
|
6
|
+
# Response: StreamResponse (SSE stream containing Task, Message, TaskStatusUpdateEvent, TaskArtifactUpdateEvent)
|
|
7
|
+
|
|
8
|
+
describe "POST /message:stream" do
|
|
9
|
+
# --- Streaming Connection Establishment ---
|
|
10
|
+
|
|
11
|
+
describe "when a client sends a SendStreamingMessage request with a valid message" do
|
|
12
|
+
it "should establish a streaming connection and return events" do
|
|
13
|
+
body = build_send_message_request(text: "Stream me!")
|
|
14
|
+
events = SSEClient.post_stream("/message:stream", body, timeout_seconds: 10)
|
|
15
|
+
|
|
16
|
+
events.length.should.be > 0
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# --- Task Lifecycle Stream Pattern ---
|
|
21
|
+
|
|
22
|
+
describe "when the agent returns a Task response via stream" do
|
|
23
|
+
it "should send a Task object as the first item in the stream" do
|
|
24
|
+
body = build_send_message_request(text: "First event test")
|
|
25
|
+
events = SSEClient.post_stream("/message:stream", body, timeout_seconds: 10)
|
|
26
|
+
|
|
27
|
+
events.length.should.be > 0
|
|
28
|
+
first = events.first.data
|
|
29
|
+
first.should.be.kind_of Hash
|
|
30
|
+
# First event should contain a task snapshot
|
|
31
|
+
first.key?("task").should.equal true
|
|
32
|
+
first["task"]["id"].should.not.be.nil
|
|
33
|
+
first["task"]["status"].should.not.be.nil
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
it "should include TaskArtifactUpdateEvent in the stream" do
|
|
37
|
+
body = build_send_message_request(text: "Artifact event test")
|
|
38
|
+
events = SSEClient.post_stream("/message:stream", body, timeout_seconds: 10)
|
|
39
|
+
|
|
40
|
+
artifact_events = events.select { |e| e.data.is_a?(Hash) && e.data.key?("artifactUpdate") }
|
|
41
|
+
artifact_events.length.should.be > 0
|
|
42
|
+
|
|
43
|
+
ae = artifact_events.first.data["artifactUpdate"]
|
|
44
|
+
ae["taskId"].should.not.be.nil
|
|
45
|
+
ae["artifact"].should.not.be.nil
|
|
46
|
+
ae["artifact"]["parts"].should.be.kind_of Array
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
it "should include TaskStatusUpdateEvent with terminal state" do
|
|
50
|
+
body = build_send_message_request(text: "Status event test")
|
|
51
|
+
events = SSEClient.post_stream("/message:stream", body, timeout_seconds: 10)
|
|
52
|
+
|
|
53
|
+
status_events = events.select { |e| e.data.is_a?(Hash) && e.data.key?("statusUpdate") }
|
|
54
|
+
status_events.length.should.be > 0
|
|
55
|
+
|
|
56
|
+
# Last status event should have a terminal state
|
|
57
|
+
last_status = status_events.last.data["statusUpdate"]
|
|
58
|
+
last_status["status"]["state"].should.equal "TASK_STATE_COMPLETED"
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
describe "when the task reaches a terminal state during streaming" do
|
|
63
|
+
it "should close the stream when task reaches completed state" do
|
|
64
|
+
body = build_send_message_request(text: "Stream close test")
|
|
65
|
+
events = SSEClient.post_stream("/message:stream", body, timeout_seconds: 10)
|
|
66
|
+
|
|
67
|
+
# Stream should have ended (we got events back, not a timeout with no data)
|
|
68
|
+
events.length.should.be > 0
|
|
69
|
+
|
|
70
|
+
# Last event should indicate terminal state
|
|
71
|
+
last_event = events.last.data
|
|
72
|
+
if last_event.is_a?(Hash) && last_event.key?("statusUpdate")
|
|
73
|
+
last_event["statusUpdate"]["status"]["state"].should.equal "TASK_STATE_COMPLETED"
|
|
74
|
+
end
|
|
75
|
+
true.should.equal true
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# --- Event Ordering ---
|
|
80
|
+
|
|
81
|
+
describe "when validating event ordering" do
|
|
82
|
+
it "should deliver events in correct order: task, artifact, status(completed)" do
|
|
83
|
+
body = build_send_message_request(text: "Order test")
|
|
84
|
+
events = SSEClient.post_stream("/message:stream", body, timeout_seconds: 10)
|
|
85
|
+
|
|
86
|
+
events.length.should.be >= 3
|
|
87
|
+
|
|
88
|
+
# First should be task snapshot
|
|
89
|
+
events[0].data.key?("task").should.equal true
|
|
90
|
+
# Second should be artifact update
|
|
91
|
+
events[1].data.key?("artifactUpdate").should.equal true
|
|
92
|
+
# Third should be status completed
|
|
93
|
+
events[2].data.key?("statusUpdate").should.equal true
|
|
94
|
+
events[2].data["statusUpdate"]["status"]["state"].should.equal "TASK_STATE_COMPLETED"
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# --- Error Cases ---
|
|
99
|
+
# NOTE: Commented out -- server supports streaming, cannot easily test unsupported case
|
|
100
|
+
|
|
101
|
+
# describe "when the server does not support streaming" do
|
|
102
|
+
# it "should respond with an UnsupportedOperationError" do
|
|
103
|
+
# end
|
|
104
|
+
# end
|
|
105
|
+
|
|
106
|
+
# --- Message-Only Stream Pattern ---
|
|
107
|
+
# NOTE: Commented out -- reference server always returns Task-based streams
|
|
108
|
+
|
|
109
|
+
# describe "when the agent returns a Message response via stream" do
|
|
110
|
+
# it "should contain exactly one Message object in the stream" do
|
|
111
|
+
# end
|
|
112
|
+
#
|
|
113
|
+
# it "should close the stream immediately after the Message" do
|
|
114
|
+
# end
|
|
115
|
+
# end
|
|
116
|
+
|
|
117
|
+
# --- Error cases for referencing terminal tasks ---
|
|
118
|
+
# NOTE: Commented out -- streaming endpoint creates new tasks in reference server
|
|
119
|
+
|
|
120
|
+
# describe "when a client sends a streaming request referencing a completed task" do
|
|
121
|
+
# it "should respond with an UnsupportedOperationError" do
|
|
122
|
+
# end
|
|
123
|
+
# end
|
|
124
|
+
|
|
125
|
+
# describe "when a client sends a streaming request referencing a non-existent task ID" do
|
|
126
|
+
# it "should respond with a TaskNotFoundError" do
|
|
127
|
+
# end
|
|
128
|
+
# end
|
|
129
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
require "a2a_test_framework/test_helper"
|
|
2
|
+
|
|
3
|
+
# NOTE: All tests commented out -- Reference server does not implement A2A service parameters
|
|
4
|
+
|
|
5
|
+
# require "a2a_test_framework/test_helper"
|
|
6
|
+
|
|
7
|
+
# # NOTE: All tests commented out -- Reference server does not implement A2A service parameters
|
|
8
|
+
|
|
9
|
+
# # describe "Service Parameters (REST)" do
|
|
10
|
+
# # # --- Key Case Insensitivity ---
|
|
11
|
+
# #
|
|
12
|
+
# # describe "when a client sends service parameter keys in different cases" do
|
|
13
|
+
# # it "should treat A2A-Version and a2a-version as the same parameter" do
|
|
14
|
+
# # end
|
|
15
|
+
# #
|
|
16
|
+
# # it "should treat A2A-Extensions and a2a-extensions as the same parameter" do
|
|
17
|
+
# # end
|
|
18
|
+
# # end
|
|
19
|
+
# #
|
|
20
|
+
# # # --- Value Case Sensitivity ---
|
|
21
|
+
# #
|
|
22
|
+
# # describe "when a client sends service parameter values" do
|
|
23
|
+
# # it "should treat values as case-sensitive" do
|
|
24
|
+
# # end
|
|
25
|
+
# # end
|
|
26
|
+
# #
|
|
27
|
+
# # # --- A2A-Version Parameter ---
|
|
28
|
+
# #
|
|
29
|
+
# # describe "when A2A-Version is set to an unsupported version" do
|
|
30
|
+
# # it "should respond with a VersionNotSupportedError" do
|
|
31
|
+
# # end
|
|
32
|
+
# # end
|
|
33
|
+
# #
|
|
34
|
+
# # describe "when A2A-Version is set to a supported version" do
|
|
35
|
+
# # it "should process the request normally" do
|
|
36
|
+
# # end
|
|
37
|
+
# # end
|
|
38
|
+
# #
|
|
39
|
+
# # # --- A2A-Extensions Parameter ---
|
|
40
|
+
# #
|
|
41
|
+
# # describe "when A2A-Extensions contains multiple extension URIs" do
|
|
42
|
+
# # it "should interpret the value as a comma-separated list of extension URIs" do
|
|
43
|
+
# # end
|
|
44
|
+
# # end
|
|
45
|
+
# #
|
|
46
|
+
# # # --- Prefix Convention ---
|
|
47
|
+
# #
|
|
48
|
+
# # describe "when examining specification-defined service parameters" do
|
|
49
|
+
# # it "should all be prefixed with a2a-" do
|
|
50
|
+
# # end
|
|
51
|
+
# # end
|
|
52
|
+
# # end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
require "a2a_test_framework/test_helper"
|
|
2
|
+
require "a2a_test_framework/sse_client"
|
|
3
|
+
|
|
4
|
+
# Cross-cutting: Streaming event delivery applies to:
|
|
5
|
+
# REST: POST /message:stream, GET /tasks/{id}:subscribe
|
|
6
|
+
|
|
7
|
+
describe "Streaming Event Delivery (REST)" do
|
|
8
|
+
# --- Event Ordering ---
|
|
9
|
+
|
|
10
|
+
describe "when a task generates multiple events in sequence" do
|
|
11
|
+
it "should deliver all events in the order they were generated" do
|
|
12
|
+
body = build_send_message_request(text: "Event order test")
|
|
13
|
+
events = SSEClient.post_stream("/message:stream", body, timeout_seconds: 10)
|
|
14
|
+
|
|
15
|
+
events.length.should.be >= 3
|
|
16
|
+
|
|
17
|
+
# Verify ordering: task first, then artifact, then status completed
|
|
18
|
+
events[0].data.key?("task").should.equal true
|
|
19
|
+
events[1].data.key?("artifactUpdate").should.equal true
|
|
20
|
+
events[2].data.key?("statusUpdate").should.equal true
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it "should not reorder events during transmission" do
|
|
24
|
+
body = build_send_message_request(text: "No reorder test")
|
|
25
|
+
events = SSEClient.post_stream("/message:stream", body, timeout_seconds: 10)
|
|
26
|
+
|
|
27
|
+
# The last status event should be terminal
|
|
28
|
+
status_events = events.select { |e| e.data.is_a?(Hash) && e.data.key?("statusUpdate") }
|
|
29
|
+
if status_events.length > 0
|
|
30
|
+
last_status = status_events.last.data["statusUpdate"]["status"]["state"]
|
|
31
|
+
last_status.should.equal "TASK_STATE_COMPLETED"
|
|
32
|
+
end
|
|
33
|
+
true.should.equal true
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# --- Multiple Streams Per Task ---
|
|
38
|
+
# NOTE: Commented out -- requires tasks in non-terminal state for subscribe
|
|
39
|
+
|
|
40
|
+
# describe "when multiple clients subscribe to the same task" do
|
|
41
|
+
# it "should serve multiple concurrent streams simultaneously" do
|
|
42
|
+
# end
|
|
43
|
+
#
|
|
44
|
+
# it "should broadcast events to all active streams for a task" do
|
|
45
|
+
# end
|
|
46
|
+
#
|
|
47
|
+
# it "should deliver the same events in the same order to each stream" do
|
|
48
|
+
# end
|
|
49
|
+
# end
|
|
50
|
+
|
|
51
|
+
# describe "when one client closes their stream" do
|
|
52
|
+
# it "should keep other active streams open and receiving events" do
|
|
53
|
+
# end
|
|
54
|
+
#
|
|
55
|
+
# it "should not affect the task lifecycle" do
|
|
56
|
+
# end
|
|
57
|
+
# end
|
|
58
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
require "a2a_test_framework/test_helper"
|
|
2
|
+
require "a2a_test_framework/sse_client"
|
|
3
|
+
|
|
4
|
+
# REST endpoint: GET /tasks/{id}:subscribe
|
|
5
|
+
# Request: SubscribeToTaskRequest (id, tenant)
|
|
6
|
+
# Response: stream of StreamResponse (SSE)
|
|
7
|
+
|
|
8
|
+
describe "GET /tasks/{id}:subscribe" do
|
|
9
|
+
# --- Stream Initialization ---
|
|
10
|
+
# NOTE: The reference server completes tasks synchronously, so subscribing
|
|
11
|
+
# to a task after SendMessage will always find it in a terminal state.
|
|
12
|
+
# We can only test error cases here.
|
|
13
|
+
|
|
14
|
+
# describe "when a client subscribes to a working task" do
|
|
15
|
+
# it "should send a Task object as the first event in the stream" do
|
|
16
|
+
# end
|
|
17
|
+
#
|
|
18
|
+
# it "should represent the current state of the task at time of subscription" do
|
|
19
|
+
# end
|
|
20
|
+
# end
|
|
21
|
+
|
|
22
|
+
# --- Error Cases ---
|
|
23
|
+
|
|
24
|
+
describe "when a client subscribes to a non-existent task ID" do
|
|
25
|
+
it "should respond with an error" do
|
|
26
|
+
response = http_get("/tasks/nonexistent-#{SecureRandom.uuid}:subscribe")
|
|
27
|
+
|
|
28
|
+
if response.code.to_i >= 400
|
|
29
|
+
true.should.equal true
|
|
30
|
+
else
|
|
31
|
+
data = parse_json(response)
|
|
32
|
+
data.key?("error").should.equal true
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
describe "when a client subscribes to a task in completed state" do
|
|
38
|
+
it "should respond with an error indicating unsupported operation" do
|
|
39
|
+
task = create_task!(text: "Subscribe to completed")
|
|
40
|
+
task["status"]["state"].should.equal "TASK_STATE_COMPLETED"
|
|
41
|
+
|
|
42
|
+
response = http_get("/tasks/#{task["id"]}:subscribe")
|
|
43
|
+
|
|
44
|
+
if response.code.to_i >= 400
|
|
45
|
+
true.should.equal true
|
|
46
|
+
else
|
|
47
|
+
data = parse_json(response)
|
|
48
|
+
data.key?("error").should.equal true
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
describe "when a client subscribes to a task not accessible to the client" do
|
|
54
|
+
it "should respond with an error" do
|
|
55
|
+
response = http_get("/tasks/inaccessible-#{SecureRandom.uuid}:subscribe")
|
|
56
|
+
|
|
57
|
+
if response.code.to_i >= 400
|
|
58
|
+
true.should.equal true
|
|
59
|
+
else
|
|
60
|
+
data = parse_json(response)
|
|
61
|
+
data.key?("error").should.equal true
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# --- Stream Content ---
|
|
67
|
+
# NOTE: Commented out -- requires a task in non-terminal state
|
|
68
|
+
|
|
69
|
+
# describe "when the subscribed task status changes" do
|
|
70
|
+
# it "should deliver a TaskStatusUpdateEvent object" do
|
|
71
|
+
# end
|
|
72
|
+
# end
|
|
73
|
+
|
|
74
|
+
# describe "when the subscribed task generates a new artifact" do
|
|
75
|
+
# it "should deliver a TaskArtifactUpdateEvent object" do
|
|
76
|
+
# end
|
|
77
|
+
# end
|
|
78
|
+
|
|
79
|
+
# --- Stream Termination ---
|
|
80
|
+
# NOTE: Commented out -- requires a task in non-terminal state
|
|
81
|
+
|
|
82
|
+
# describe "when the subscribed task reaches a terminal state" do
|
|
83
|
+
# it "should terminate the stream when task reaches completed state" do
|
|
84
|
+
# end
|
|
85
|
+
# end
|
|
86
|
+
|
|
87
|
+
# describe "when the subscribed task is in a non-terminal state" do
|
|
88
|
+
# it "should keep the stream open while task is in working state" do
|
|
89
|
+
# end
|
|
90
|
+
# end
|
|
91
|
+
|
|
92
|
+
# --- Server Not Supporting Streaming ---
|
|
93
|
+
# NOTE: Commented out -- reference server supports streaming
|
|
94
|
+
|
|
95
|
+
# describe "when the server does not support streaming" do
|
|
96
|
+
# it "should respond with an UnsupportedOperationError" do
|
|
97
|
+
# end
|
|
98
|
+
# end
|
|
99
|
+
end
|