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,190 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optparse"
|
|
4
|
+
require_relative "../a2a_test_framework"
|
|
5
|
+
|
|
6
|
+
module A2ATestFramework
|
|
7
|
+
class CLI
|
|
8
|
+
CATEGORIES = {
|
|
9
|
+
"discovery" => %w[
|
|
10
|
+
agent_discovery_test
|
|
11
|
+
agent_card_caching_test
|
|
12
|
+
agent_card_signing_test
|
|
13
|
+
],
|
|
14
|
+
"send_message" => %w[
|
|
15
|
+
send_message_test
|
|
16
|
+
],
|
|
17
|
+
"streaming" => %w[
|
|
18
|
+
send_streaming_message_test
|
|
19
|
+
streaming_event_delivery_test
|
|
20
|
+
subscribe_to_task_test
|
|
21
|
+
],
|
|
22
|
+
"tasks" => %w[
|
|
23
|
+
get_task_test
|
|
24
|
+
list_tasks_test
|
|
25
|
+
cancel_task_test
|
|
26
|
+
],
|
|
27
|
+
"push_notifications" => %w[
|
|
28
|
+
push_notification_delivery_test
|
|
29
|
+
create_task_push_notification_config_test
|
|
30
|
+
get_task_push_notification_config_test
|
|
31
|
+
list_task_push_notification_configs_test
|
|
32
|
+
delete_task_push_notification_config_test
|
|
33
|
+
],
|
|
34
|
+
"errors" => %w[
|
|
35
|
+
error_handling_test
|
|
36
|
+
error_code_mappings_test
|
|
37
|
+
],
|
|
38
|
+
"versioning" => %w[
|
|
39
|
+
versioning_test
|
|
40
|
+
versioning_responsibilities_test
|
|
41
|
+
extension_versioning_test
|
|
42
|
+
],
|
|
43
|
+
"security" => %w[
|
|
44
|
+
security_considerations_test
|
|
45
|
+
protocol_security_test
|
|
46
|
+
authentication_authorization_test
|
|
47
|
+
in_task_authorization_test
|
|
48
|
+
],
|
|
49
|
+
"protocol" => %w[
|
|
50
|
+
protocol_data_model_test
|
|
51
|
+
json_rpc_binding_test
|
|
52
|
+
http_rest_binding_test
|
|
53
|
+
json_field_naming_test
|
|
54
|
+
custom_binding_test
|
|
55
|
+
protocol_selection_negotiation_test
|
|
56
|
+
functional_equivalence_test
|
|
57
|
+
iana_registrations_test
|
|
58
|
+
],
|
|
59
|
+
"semantics" => %w[
|
|
60
|
+
context_identifier_semantics_test
|
|
61
|
+
task_identifier_semantics_test
|
|
62
|
+
history_length_semantics_test
|
|
63
|
+
field_presence_optionality_test
|
|
64
|
+
idempotency_test
|
|
65
|
+
multi_turn_conversation_test
|
|
66
|
+
messages_and_artifacts_test
|
|
67
|
+
timestamps_test
|
|
68
|
+
service_parameters_test
|
|
69
|
+
capability_validation_test
|
|
70
|
+
],
|
|
71
|
+
"extended" => %w[
|
|
72
|
+
get_extended_agent_card_test
|
|
73
|
+
],
|
|
74
|
+
}.freeze
|
|
75
|
+
|
|
76
|
+
def initialize(argv)
|
|
77
|
+
@argv = argv
|
|
78
|
+
@options = {
|
|
79
|
+
url: ENV.fetch("A2A_BASE_URL", "http://localhost:9292"),
|
|
80
|
+
binding: "rest",
|
|
81
|
+
only: [],
|
|
82
|
+
files: [],
|
|
83
|
+
}
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def run
|
|
87
|
+
parse_options!
|
|
88
|
+
|
|
89
|
+
ENV["A2A_BASE_URL"] = @options[:url]
|
|
90
|
+
|
|
91
|
+
files = resolve_test_files
|
|
92
|
+
if files.empty?
|
|
93
|
+
$stderr.puts "No test files found matching the given criteria."
|
|
94
|
+
exit 1
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
require "bundler/setup"
|
|
98
|
+
require "scampi"
|
|
99
|
+
Scampi.summary_on_exit
|
|
100
|
+
|
|
101
|
+
files.each { |f| load f }
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
private
|
|
105
|
+
|
|
106
|
+
def parse_options!
|
|
107
|
+
parser = OptionParser.new do |opts|
|
|
108
|
+
opts.banner = "Usage: a2a-test [options] [test_files...]"
|
|
109
|
+
opts.separator ""
|
|
110
|
+
opts.separator "Run A2A protocol conformance tests against an A2A server implementation."
|
|
111
|
+
opts.separator ""
|
|
112
|
+
opts.separator "Options:"
|
|
113
|
+
|
|
114
|
+
opts.on("--url URL", "Base URL of the A2A server (default: $A2A_BASE_URL or http://localhost:9292)") do |url|
|
|
115
|
+
@options[:url] = url
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
opts.on("--binding BINDING", "Protocol binding to test: rest, grpc (default: rest)") do |binding|
|
|
119
|
+
unless %w[rest grpc].include?(binding)
|
|
120
|
+
$stderr.puts "Error: --binding must be 'rest' or 'grpc'"
|
|
121
|
+
exit 1
|
|
122
|
+
end
|
|
123
|
+
@options[:binding] = binding
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
opts.on("--only CATEGORIES", "Comma-separated list of test categories to run") do |cats|
|
|
127
|
+
@options[:only] = cats.split(",").map(&:strip)
|
|
128
|
+
invalid = @options[:only] - CATEGORIES.keys
|
|
129
|
+
unless invalid.empty?
|
|
130
|
+
$stderr.puts "Error: unknown categories: #{invalid.join(', ')}"
|
|
131
|
+
$stderr.puts "Available categories: #{CATEGORIES.keys.join(', ')}"
|
|
132
|
+
exit 1
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
opts.on("--list-categories", "List available test categories and exit") do
|
|
137
|
+
puts "Available test categories:"
|
|
138
|
+
puts ""
|
|
139
|
+
CATEGORIES.each do |name, files|
|
|
140
|
+
puts " #{name}"
|
|
141
|
+
files.each { |f| puts " - #{f}" }
|
|
142
|
+
puts ""
|
|
143
|
+
end
|
|
144
|
+
exit 0
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
opts.on("-v", "--version", "Print version and exit") do
|
|
148
|
+
puts "a2a-test-framework #{A2ATestFramework::VERSION}"
|
|
149
|
+
exit 0
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
opts.on("-h", "--help", "Show this help message") do
|
|
153
|
+
puts opts
|
|
154
|
+
exit 0
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
parser.parse!(@argv)
|
|
159
|
+
|
|
160
|
+
# Remaining arguments are test file paths
|
|
161
|
+
@options[:files] = @argv.dup
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def resolve_test_files
|
|
165
|
+
# If specific file paths were provided, use those
|
|
166
|
+
unless @options[:files].empty?
|
|
167
|
+
return @options[:files].map { |f| File.expand_path(f) }.select { |f| File.exist?(f) }
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
binding_dir = File.join(A2ATestFramework::TESTS_DIR, @options[:binding])
|
|
171
|
+
|
|
172
|
+
unless Dir.exist?(binding_dir)
|
|
173
|
+
$stderr.puts "Error: test directory not found: #{binding_dir}"
|
|
174
|
+
exit 1
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
if @options[:only].empty?
|
|
178
|
+
# Run all tests for the binding
|
|
179
|
+
Dir.glob(File.join(binding_dir, "**/*_test.rb")).sort
|
|
180
|
+
else
|
|
181
|
+
# Filter by category
|
|
182
|
+
test_basenames = @options[:only].flat_map { |cat| CATEGORIES[cat] }.compact.uniq
|
|
183
|
+
test_basenames.filter_map do |basename|
|
|
184
|
+
path = File.join(binding_dir, "#{basename}.rb")
|
|
185
|
+
path if File.exist?(path)
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# frozen_string_literal: false
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "json"
|
|
5
|
+
require "uri"
|
|
6
|
+
require "timeout"
|
|
7
|
+
|
|
8
|
+
# Simple SSE client for testing streaming endpoints.
|
|
9
|
+
# Reads SSE events from a chunked HTTP response.
|
|
10
|
+
module SSEClient
|
|
11
|
+
Event = Struct.new(:data, :event, :id, keyword_init: true)
|
|
12
|
+
|
|
13
|
+
# POST to a streaming endpoint and collect SSE events.
|
|
14
|
+
# Returns an array of Event structs.
|
|
15
|
+
def self.post_stream(path, body, timeout_seconds: 10, headers: {})
|
|
16
|
+
collect_events(:post, path, body: body, timeout_seconds: timeout_seconds, headers: headers)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# GET a streaming endpoint and collect SSE events.
|
|
20
|
+
# Returns an array of Event structs.
|
|
21
|
+
def self.get_stream(path, timeout_seconds: 10, headers: {})
|
|
22
|
+
collect_events(:get, path, timeout_seconds: timeout_seconds, headers: headers)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def self.collect_events(method, path, body: nil, timeout_seconds: 10, headers: {})
|
|
28
|
+
uri = URI("#{BASE_URL}#{path}")
|
|
29
|
+
events = []
|
|
30
|
+
|
|
31
|
+
Timeout.timeout(timeout_seconds) do
|
|
32
|
+
Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") do |http|
|
|
33
|
+
request = case method
|
|
34
|
+
when :post
|
|
35
|
+
req = Net::HTTP::Post.new(uri.path)
|
|
36
|
+
req["Content-Type"] = "application/json"
|
|
37
|
+
req.body = JSON.generate(body) if body
|
|
38
|
+
req
|
|
39
|
+
when :get
|
|
40
|
+
Net::HTTP::Get.new(uri.request_uri)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
request["Accept"] = "text/event-stream"
|
|
44
|
+
headers.each { |k, v| request[k] = v }
|
|
45
|
+
|
|
46
|
+
http.request(request) do |response|
|
|
47
|
+
buffer = ""
|
|
48
|
+
current_event = nil
|
|
49
|
+
current_data_lines = []
|
|
50
|
+
|
|
51
|
+
response.read_body do |chunk|
|
|
52
|
+
buffer << chunk
|
|
53
|
+
while (line_end = buffer.index("\n"))
|
|
54
|
+
line = buffer.slice!(0..line_end).chomp("\r\n").chomp("\n")
|
|
55
|
+
|
|
56
|
+
if line.empty?
|
|
57
|
+
# Empty line = event boundary
|
|
58
|
+
if current_data_lines.any?
|
|
59
|
+
data_str = current_data_lines.join("\n")
|
|
60
|
+
parsed = begin
|
|
61
|
+
JSON.parse(data_str)
|
|
62
|
+
rescue JSON::ParserError
|
|
63
|
+
data_str
|
|
64
|
+
end
|
|
65
|
+
events << Event.new(
|
|
66
|
+
data: parsed,
|
|
67
|
+
event: current_event
|
|
68
|
+
)
|
|
69
|
+
end
|
|
70
|
+
current_event = nil
|
|
71
|
+
current_data_lines = []
|
|
72
|
+
elsif line.start_with?("data: ")
|
|
73
|
+
current_data_lines << line.sub(/\Adata: /, "")
|
|
74
|
+
elsif line.start_with?("data:")
|
|
75
|
+
current_data_lines << line.sub(/\Adata:/, "")
|
|
76
|
+
elsif line.start_with?("event: ")
|
|
77
|
+
current_event = line.sub(/\Aevent: /, "")
|
|
78
|
+
elsif line.start_with?("id: ")
|
|
79
|
+
# ignored for now
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Handle any remaining data in buffer
|
|
85
|
+
if current_data_lines.any?
|
|
86
|
+
data_str = current_data_lines.join("\n")
|
|
87
|
+
parsed = begin
|
|
88
|
+
JSON.parse(data_str)
|
|
89
|
+
rescue JSON::ParserError
|
|
90
|
+
data_str
|
|
91
|
+
end
|
|
92
|
+
events << Event.new(data: parsed, event: current_event)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
events
|
|
99
|
+
rescue Timeout::Error
|
|
100
|
+
events
|
|
101
|
+
rescue EOFError, IOError, Errno::ECONNRESET
|
|
102
|
+
events
|
|
103
|
+
end
|
|
104
|
+
end
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bundler/setup"
|
|
4
|
+
require "scampi"
|
|
5
|
+
require "net/http"
|
|
6
|
+
require "json"
|
|
7
|
+
require "uri"
|
|
8
|
+
require "securerandom"
|
|
9
|
+
require "a2a"
|
|
10
|
+
|
|
11
|
+
BASE_URL = ENV.fetch("A2A_BASE_URL", "http://localhost:9292")
|
|
12
|
+
|
|
13
|
+
# --- HTTP Helpers -----------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
def http_post(path, body, headers: {})
|
|
16
|
+
uri = URI("#{BASE_URL}#{path}")
|
|
17
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
18
|
+
http.use_ssl = uri.scheme == "https"
|
|
19
|
+
http.read_timeout = 30
|
|
20
|
+
request = Net::HTTP::Post.new(uri.path)
|
|
21
|
+
request["Content-Type"] = "application/json"
|
|
22
|
+
headers.each { |k, v| request[k] = v }
|
|
23
|
+
request.body = JSON.generate(body)
|
|
24
|
+
http.request(request)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def http_get(path, headers: {})
|
|
28
|
+
uri = URI("#{BASE_URL}#{path}")
|
|
29
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
30
|
+
http.use_ssl = uri.scheme == "https"
|
|
31
|
+
http.read_timeout = 30
|
|
32
|
+
request = Net::HTTP::Get.new(uri.request_uri)
|
|
33
|
+
headers.each { |k, v| request[k] = v }
|
|
34
|
+
http.request(request)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def http_delete(path, headers: {})
|
|
38
|
+
uri = URI("#{BASE_URL}#{path}")
|
|
39
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
40
|
+
http.use_ssl = uri.scheme == "https"
|
|
41
|
+
http.read_timeout = 30
|
|
42
|
+
request = Net::HTTP::Delete.new(uri.path)
|
|
43
|
+
headers.each { |k, v| request[k] = v }
|
|
44
|
+
http.request(request)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def parse_json(response)
|
|
48
|
+
JSON.parse(response.body)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# --- Request Builders (using A2A Schema objects) ----------------------------
|
|
52
|
+
|
|
53
|
+
def build_send_message_request(text: "Hello, agent", task_id: nil, context_id: nil, configuration: nil, metadata: nil)
|
|
54
|
+
message = {
|
|
55
|
+
"messageId" => SecureRandom.uuid,
|
|
56
|
+
"role" => "ROLE_USER",
|
|
57
|
+
"parts" => [{ "text" => text }]
|
|
58
|
+
}
|
|
59
|
+
message["taskId"] = task_id if task_id
|
|
60
|
+
message["contextId"] = context_id if context_id
|
|
61
|
+
|
|
62
|
+
A2A::Schema["Send Message Request"].new(
|
|
63
|
+
message: message,
|
|
64
|
+
configuration: configuration,
|
|
65
|
+
metadata: metadata
|
|
66
|
+
).to_h
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def build_get_task_request(id:, history_length: nil)
|
|
70
|
+
A2A::Schema["Get Task Request"].new(
|
|
71
|
+
id: id,
|
|
72
|
+
history_length: history_length
|
|
73
|
+
).to_h
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def build_list_tasks_request(context_id: nil, status: nil, page_size: nil, page_token: nil, history_length: nil, include_artifacts: nil)
|
|
77
|
+
A2A::Schema["List Tasks Request"].new(
|
|
78
|
+
context_id: context_id,
|
|
79
|
+
status: status,
|
|
80
|
+
page_size: page_size,
|
|
81
|
+
page_token: page_token,
|
|
82
|
+
history_length: history_length,
|
|
83
|
+
include_artifacts: include_artifacts
|
|
84
|
+
).to_h
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def build_cancel_task_request(id:, metadata: nil)
|
|
88
|
+
A2A::Schema["Cancel Task Request"].new(
|
|
89
|
+
id: id,
|
|
90
|
+
metadata: metadata
|
|
91
|
+
).to_h
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def build_subscribe_to_task_request(id:)
|
|
95
|
+
A2A::Schema["Subscribe To Task Request"].new(id: id).to_h
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def build_push_notification_config(task_id:, url: "https://example.com/webhook", token: nil, authentication: nil)
|
|
99
|
+
A2A::Schema["Task Push Notification Config"].new(
|
|
100
|
+
task_id: task_id,
|
|
101
|
+
url: url,
|
|
102
|
+
token: token,
|
|
103
|
+
authentication: authentication
|
|
104
|
+
).to_h
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def build_get_push_notification_config_request(id:, task_id:)
|
|
108
|
+
A2A::Schema["Get Task Push Notification Config Request"].new(
|
|
109
|
+
id: id,
|
|
110
|
+
task_id: task_id
|
|
111
|
+
).to_h
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def build_list_push_notification_configs_request(task_id:, page_size: nil, page_token: nil)
|
|
115
|
+
A2A::Schema["List Task Push Notification Configs Request"].new(
|
|
116
|
+
task_id: task_id,
|
|
117
|
+
page_size: page_size,
|
|
118
|
+
page_token: page_token
|
|
119
|
+
).to_h
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def build_delete_push_notification_config_request(id:, task_id:)
|
|
123
|
+
A2A::Schema["Delete Task Push Notification Config Request"].new(
|
|
124
|
+
id: id,
|
|
125
|
+
task_id: task_id
|
|
126
|
+
).to_h
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# --- Lifecycle Helpers ------------------------------------------------------
|
|
130
|
+
|
|
131
|
+
# Sends a message and returns the task hash from the response.
|
|
132
|
+
def create_task!(text: "Hello, agent")
|
|
133
|
+
body = build_send_message_request(text: text)
|
|
134
|
+
response = http_post("/message:send", body)
|
|
135
|
+
raise "Failed to create task: HTTP #{response.code}" unless response.code.to_i == 200
|
|
136
|
+
data = parse_json(response)
|
|
137
|
+
raise "No task in response" unless data["task"]
|
|
138
|
+
data["task"]
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Retrieves a task by ID.
|
|
142
|
+
def get_task!(id)
|
|
143
|
+
response = http_get("/tasks/#{id}")
|
|
144
|
+
raise "Failed to get task: HTTP #{response.code}" unless response.code.to_i == 200
|
|
145
|
+
parse_json(response)
|
|
146
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "a2a_test_framework/version"
|
|
4
|
+
|
|
5
|
+
module A2ATestFramework
|
|
6
|
+
# Root directory of the gem (for locating test files, schemas, etc.)
|
|
7
|
+
ROOT = File.expand_path("..", __dir__)
|
|
8
|
+
|
|
9
|
+
# Directory containing the test files
|
|
10
|
+
TESTS_DIR = File.join(ROOT, "tests")
|
|
11
|
+
|
|
12
|
+
# Directory containing endpoint JSON schemas
|
|
13
|
+
ENDPOINTS_DIR = File.join(ROOT, "endpoints")
|
|
14
|
+
|
|
15
|
+
# Path to the protocol schema bundle
|
|
16
|
+
SCHEMA_PATH = File.join(ROOT, "a2a.json")
|
|
17
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
require "a2a_test_framework/test_helper"
|
|
2
|
+
|
|
3
|
+
# NOTE: All tests commented out -- gRPC binding not supported by reference server
|
|
4
|
+
|
|
5
|
+
# require "a2a_test_framework/test_helper"
|
|
6
|
+
|
|
7
|
+
# # NOTE: All tests commented out -- gRPC binding not supported by reference server
|
|
8
|
+
|
|
9
|
+
# # describe "A2AService/CancelTask" do
|
|
10
|
+
# # # --- Successful Cancellation ---
|
|
11
|
+
# #
|
|
12
|
+
# # describe "when a client sends a CancelTask request for a working task" do
|
|
13
|
+
# # it "should respond with an updated Task object" do
|
|
14
|
+
# # end
|
|
15
|
+
# #
|
|
16
|
+
# # it "should reflect the cancellation in the Task status" do
|
|
17
|
+
# # end
|
|
18
|
+
# # end
|
|
19
|
+
# #
|
|
20
|
+
# # describe "when a client sends a CancelTask request for a task in input_required state" do
|
|
21
|
+
# # it "should respond with an updated Task object" do
|
|
22
|
+
# # end
|
|
23
|
+
# #
|
|
24
|
+
# # it "should reflect the cancellation in the Task status" do
|
|
25
|
+
# # end
|
|
26
|
+
# # end
|
|
27
|
+
# #
|
|
28
|
+
# # describe "when a client sends a CancelTask request for a cancelable task" do
|
|
29
|
+
# # it "should return an updated Task object with cancellation status" do
|
|
30
|
+
# # end
|
|
31
|
+
# # end
|
|
32
|
+
# #
|
|
33
|
+
# # # --- Error Cases ---
|
|
34
|
+
# #
|
|
35
|
+
# # describe "when a client sends a CancelTask request for a completed task" do
|
|
36
|
+
# # it "should respond with a TaskNotCancelableError" do
|
|
37
|
+
# # end
|
|
38
|
+
# # end
|
|
39
|
+
# #
|
|
40
|
+
# # describe "when a client sends a CancelTask request for a failed task" do
|
|
41
|
+
# # it "should respond with a TaskNotCancelableError" do
|
|
42
|
+
# # end
|
|
43
|
+
# # end
|
|
44
|
+
# #
|
|
45
|
+
# # describe "when a client sends a CancelTask request for an already canceled task" do
|
|
46
|
+
# # it "should respond with a TaskNotCancelableError" do
|
|
47
|
+
# # end
|
|
48
|
+
# # end
|
|
49
|
+
# #
|
|
50
|
+
# # describe "when a client sends a CancelTask request with a non-existent task ID" do
|
|
51
|
+
# # it "should respond with a TaskNotFoundError" do
|
|
52
|
+
# # end
|
|
53
|
+
# # end
|
|
54
|
+
# #
|
|
55
|
+
# # describe "when a client sends a CancelTask request for a task not accessible to the client" do
|
|
56
|
+
# # it "should respond with a TaskNotFoundError" do
|
|
57
|
+
# # end
|
|
58
|
+
# # end
|
|
59
|
+
# #
|
|
60
|
+
# # # --- Idempotency ---
|
|
61
|
+
# #
|
|
62
|
+
# # describe "when a client sends multiple CancelTask requests for the same task" do
|
|
63
|
+
# # it "should handle repeated cancellation requests idempotently" do
|
|
64
|
+
# # end
|
|
65
|
+
# #
|
|
66
|
+
# # it "should respond with TaskNotCancelableError or TaskNotFoundError on subsequent attempts" do
|
|
67
|
+
# # end
|
|
68
|
+
# # end
|
|
69
|
+
# # end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
require "a2a_test_framework/test_helper"
|
|
2
|
+
|
|
3
|
+
# NOTE: All tests commented out -- gRPC binding not supported by reference server
|
|
4
|
+
|
|
5
|
+
# require "a2a_test_framework/test_helper"
|
|
6
|
+
|
|
7
|
+
# # NOTE: All tests commented out -- gRPC binding not supported by reference server
|
|
8
|
+
|
|
9
|
+
# # describe "A2AService/CreateTaskPushNotificationConfig" do
|
|
10
|
+
# # # --- Successful Creation ---
|
|
11
|
+
# #
|
|
12
|
+
# # describe "when a client creates a push notification config with a valid webhook URL" do
|
|
13
|
+
# # it "should respond with a PushNotificationConfig object" do
|
|
14
|
+
# # end
|
|
15
|
+
# #
|
|
16
|
+
# # it "should contain an assigned ID in the response" do
|
|
17
|
+
# # end
|
|
18
|
+
# # end
|
|
19
|
+
# #
|
|
20
|
+
# # describe "when a push notification config is created for a task" do
|
|
21
|
+
# # it "should establish a webhook endpoint for task update notifications" do
|
|
22
|
+
# # end
|
|
23
|
+
# #
|
|
24
|
+
# # it "should send HTTP POST requests to the configured webhook URL when task updates occur" do
|
|
25
|
+
# # end
|
|
26
|
+
# # end
|
|
27
|
+
# #
|
|
28
|
+
# # describe "when the task status changes after config creation" do
|
|
29
|
+
# # it "should send an HTTP POST request to the configured webhook URL" do
|
|
30
|
+
# # end
|
|
31
|
+
# #
|
|
32
|
+
# # it "should send the payload as a StreamResponse object" do
|
|
33
|
+
# # end
|
|
34
|
+
# # end
|
|
35
|
+
# #
|
|
36
|
+
# # # --- Configuration Persistence ---
|
|
37
|
+
# #
|
|
38
|
+
# # describe "when a push notification config exists for a non-terminal task" do
|
|
39
|
+
# # it "should remain active while the task is in a non-terminal state" do
|
|
40
|
+
# # end
|
|
41
|
+
# #
|
|
42
|
+
# # it "should remain active until explicitly deleted" do
|
|
43
|
+
# # end
|
|
44
|
+
# # end
|
|
45
|
+
# #
|
|
46
|
+
# # describe "when a task with push notification config reaches completed state" do
|
|
47
|
+
# # it "should not require the configuration to persist beyond task completion" do
|
|
48
|
+
# # end
|
|
49
|
+
# # end
|
|
50
|
+
# #
|
|
51
|
+
# # # --- Error Cases ---
|
|
52
|
+
# #
|
|
53
|
+
# # describe "when the server does not support push notifications" do
|
|
54
|
+
# # it "should respond with a PushNotificationNotSupportedError" do
|
|
55
|
+
# # end
|
|
56
|
+
# # end
|
|
57
|
+
# #
|
|
58
|
+
# # describe "when a client sends a request with a non-existent task ID" do
|
|
59
|
+
# # it "should respond with a TaskNotFoundError" do
|
|
60
|
+
# # end
|
|
61
|
+
# # end
|
|
62
|
+
# #
|
|
63
|
+
# # describe "when a client sends a request for a task not accessible to the client" do
|
|
64
|
+
# # it "should respond with a TaskNotFoundError" do
|
|
65
|
+
# # end
|
|
66
|
+
# # end
|
|
67
|
+
# #
|
|
68
|
+
# # # --- Capability Validation ---
|
|
69
|
+
# #
|
|
70
|
+
# # describe "when the AgentCard declares pushNotifications capability as true" do
|
|
71
|
+
# # it "should accept and process the request" do
|
|
72
|
+
# # end
|
|
73
|
+
# # end
|
|
74
|
+
# #
|
|
75
|
+
# # describe "when the AgentCard declares pushNotifications capability as false" do
|
|
76
|
+
# # it "should respond with a PushNotificationNotSupportedError" do
|
|
77
|
+
# # end
|
|
78
|
+
# # end
|
|
79
|
+
# # end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
require "a2a_test_framework/test_helper"
|
|
2
|
+
|
|
3
|
+
# NOTE: All tests commented out -- gRPC binding not supported by reference server
|
|
4
|
+
|
|
5
|
+
# require "a2a_test_framework/test_helper"
|
|
6
|
+
|
|
7
|
+
# # NOTE: All tests commented out -- gRPC binding not supported by reference server
|
|
8
|
+
|
|
9
|
+
# # describe "A2AService/DeleteTaskPushNotificationConfig" do
|
|
10
|
+
# # # --- Successful Deletion ---
|
|
11
|
+
# #
|
|
12
|
+
# # describe "when a client deletes an existing push notification config" do
|
|
13
|
+
# # it "should respond with a confirmation of deletion" do
|
|
14
|
+
# # end
|
|
15
|
+
# #
|
|
16
|
+
# # it "should permanently remove the configuration" do
|
|
17
|
+
# # end
|
|
18
|
+
# #
|
|
19
|
+
# # it "should cause subsequent GetTaskPushNotificationConfig requests to fail" do
|
|
20
|
+
# # end
|
|
21
|
+
# # end
|
|
22
|
+
# #
|
|
23
|
+
# # describe "when a task changes status after config deletion" do
|
|
24
|
+
# # it "should not send notifications to the previously configured webhook URL" do
|
|
25
|
+
# # end
|
|
26
|
+
# # end
|
|
27
|
+
# #
|
|
28
|
+
# # # --- Idempotency ---
|
|
29
|
+
# #
|
|
30
|
+
# # describe "when a client sends multiple delete requests for the same config" do
|
|
31
|
+
# # it "should have the same effect as a single delete" do
|
|
32
|
+
# # end
|
|
33
|
+
# #
|
|
34
|
+
# # it "should not return an error on the second request" do
|
|
35
|
+
# # end
|
|
36
|
+
# # end
|
|
37
|
+
# #
|
|
38
|
+
# # describe "when a client deletes an already-deleted config" do
|
|
39
|
+
# # it "should not return an error" do
|
|
40
|
+
# # end
|
|
41
|
+
# # end
|
|
42
|
+
# #
|
|
43
|
+
# # # --- Error Cases ---
|
|
44
|
+
# #
|
|
45
|
+
# # describe "when the server does not support push notifications" do
|
|
46
|
+
# # it "should respond with a PushNotificationNotSupportedError" do
|
|
47
|
+
# # end
|
|
48
|
+
# # end
|
|
49
|
+
# #
|
|
50
|
+
# # describe "when a client sends a request with a non-existent task ID" do
|
|
51
|
+
# # it "should respond with a TaskNotFoundError" do
|
|
52
|
+
# # end
|
|
53
|
+
# # end
|
|
54
|
+
# # end
|