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