a2a-test-framework 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. checksums.yaml +7 -0
  2. data/a2a.json +1961 -0
  3. data/a2a.proto +796 -0
  4. data/endpoints/grpc/cancel_task.json +10 -0
  5. data/endpoints/grpc/create_task_push_notification_config.json +10 -0
  6. data/endpoints/grpc/delete_task_push_notification_config.json +10 -0
  7. data/endpoints/grpc/get_extended_agent_card.json +10 -0
  8. data/endpoints/grpc/get_task.json +10 -0
  9. data/endpoints/grpc/get_task_push_notification_config.json +10 -0
  10. data/endpoints/grpc/list_task_push_notification_configs.json +10 -0
  11. data/endpoints/grpc/list_tasks.json +10 -0
  12. data/endpoints/grpc/send_message.json +10 -0
  13. data/endpoints/grpc/send_streaming_message.json +10 -0
  14. data/endpoints/grpc/subscribe_to_task.json +10 -0
  15. data/endpoints/rest/cancel_task.json +85 -0
  16. data/endpoints/rest/create_task_push_notification_config.json +104 -0
  17. data/endpoints/rest/delete_task_push_notification_config.json +46 -0
  18. data/endpoints/rest/get_extended_agent_card.json +168 -0
  19. data/endpoints/rest/get_task.json +111 -0
  20. data/endpoints/rest/get_task_push_notification_config.json +90 -0
  21. data/endpoints/rest/list_task_push_notification_configs.json +108 -0
  22. data/endpoints/rest/list_tasks.json +239 -0
  23. data/endpoints/rest/send_message.json +57 -0
  24. data/endpoints/rest/send_streaming_message.json +75 -0
  25. data/endpoints/rest/subscribe_to_task.json +68 -0
  26. data/exe/a2a-test +6 -0
  27. data/lib/a2a_test_framework/cli.rb +190 -0
  28. data/lib/a2a_test_framework/sse_client.rb +104 -0
  29. data/lib/a2a_test_framework/test_helper.rb +146 -0
  30. data/lib/a2a_test_framework/version.rb +5 -0
  31. data/lib/a2a_test_framework.rb +17 -0
  32. data/tests/grpc/cancel_task_test.rb +69 -0
  33. data/tests/grpc/create_task_push_notification_config_test.rb +79 -0
  34. data/tests/grpc/delete_task_push_notification_config_test.rb +54 -0
  35. data/tests/grpc/error_code_mappings_test.rb +39 -0
  36. data/tests/grpc/error_handling_test.rb +175 -0
  37. data/tests/grpc/get_extended_agent_card_test.rb +83 -0
  38. data/tests/grpc/get_task_push_notification_config_test.rb +39 -0
  39. data/tests/grpc/get_task_test.rb +76 -0
  40. data/tests/grpc/grpc_binding_test.rb +74 -0
  41. data/tests/grpc/list_task_push_notification_configs_test.rb +53 -0
  42. data/tests/grpc/list_tasks_test.rb +117 -0
  43. data/tests/grpc/protocol_data_model_test.rb +14 -0
  44. data/tests/grpc/send_message_test.rb +141 -0
  45. data/tests/grpc/send_streaming_message_test.rb +122 -0
  46. data/tests/grpc/streaming_event_delivery_test.rb +48 -0
  47. data/tests/grpc/subscribe_to_task_test.rb +92 -0
  48. data/tests/grpc/versioning_test.rb +32 -0
  49. data/tests/rest/agent_card_caching_test.rb +39 -0
  50. data/tests/rest/agent_card_signing_test.rb +74 -0
  51. data/tests/rest/agent_discovery_test.rb +117 -0
  52. data/tests/rest/authentication_authorization_test.rb +62 -0
  53. data/tests/rest/cancel_task_test.rb +110 -0
  54. data/tests/rest/capability_validation_test.rb +78 -0
  55. data/tests/rest/context_identifier_semantics_test.rb +75 -0
  56. data/tests/rest/create_task_push_notification_config_test.rb +122 -0
  57. data/tests/rest/custom_binding_test.rb +96 -0
  58. data/tests/rest/delete_task_push_notification_config_test.rb +103 -0
  59. data/tests/rest/error_code_mappings_test.rb +45 -0
  60. data/tests/rest/error_handling_test.rb +178 -0
  61. data/tests/rest/extension_versioning_test.rb +44 -0
  62. data/tests/rest/field_presence_optionality_test.rb +64 -0
  63. data/tests/rest/functional_equivalence_test.rb +23 -0
  64. data/tests/rest/get_extended_agent_card_test.rb +67 -0
  65. data/tests/rest/get_task_push_notification_config_test.rb +75 -0
  66. data/tests/rest/get_task_test.rb +134 -0
  67. data/tests/rest/history_length_semantics_test.rb +91 -0
  68. data/tests/rest/http_rest_binding_test.rb +114 -0
  69. data/tests/rest/iana_registrations_test.rb +47 -0
  70. data/tests/rest/idempotency_test.rb +69 -0
  71. data/tests/rest/in_task_authorization_test.rb +45 -0
  72. data/tests/rest/json_field_naming_test.rb +89 -0
  73. data/tests/rest/json_rpc_binding_test.rb +102 -0
  74. data/tests/rest/list_task_push_notification_configs_test.rb +92 -0
  75. data/tests/rest/list_tasks_test.rb +162 -0
  76. data/tests/rest/messages_and_artifacts_test.rb +101 -0
  77. data/tests/rest/multi_turn_conversation_test.rb +94 -0
  78. data/tests/rest/protocol_data_model_test.rb +99 -0
  79. data/tests/rest/protocol_security_test.rb +25 -0
  80. data/tests/rest/protocol_selection_negotiation_test.rb +24 -0
  81. data/tests/rest/push_notification_delivery_test.rb +115 -0
  82. data/tests/rest/security_considerations_test.rb +101 -0
  83. data/tests/rest/send_message_test.rb +230 -0
  84. data/tests/rest/send_streaming_message_test.rb +129 -0
  85. data/tests/rest/service_parameters_test.rb +52 -0
  86. data/tests/rest/streaming_event_delivery_test.rb +58 -0
  87. data/tests/rest/subscribe_to_task_test.rb +99 -0
  88. data/tests/rest/task_identifier_semantics_test.rb +67 -0
  89. data/tests/rest/timestamps_test.rb +70 -0
  90. data/tests/rest/versioning_responsibilities_test.rb +46 -0
  91. data/tests/rest/versioning_test.rb +44 -0
  92. metadata +159 -0
@@ -0,0 +1,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