nat-engine 1__py3-none-any.whl

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 (299) hide show
  1. mannf/__init__.py +33 -0
  2. mannf/__main__.py +10 -0
  3. mannf/_version.py +8 -0
  4. mannf/agents/__init__.py +7 -0
  5. mannf/agents/analyzer_agent.py +9 -0
  6. mannf/agents/base.py +9 -0
  7. mannf/agents/bdi_agent.py +9 -0
  8. mannf/agents/belief_state.py +9 -0
  9. mannf/agents/coordinator_agent.py +9 -0
  10. mannf/agents/executor_agent.py +9 -0
  11. mannf/agents/monitor_agent.py +9 -0
  12. mannf/agents/oracle_agent.py +9 -0
  13. mannf/agents/planner_agent.py +9 -0
  14. mannf/agents/test_agent.py +9 -0
  15. mannf/anomaly/__init__.py +7 -0
  16. mannf/anomaly/enhanced_detector.py +9 -0
  17. mannf/cli.py +9 -0
  18. mannf/core/__init__.py +26 -0
  19. mannf/core/agents/__init__.py +52 -0
  20. mannf/core/agents/accessibility_scanner_agent.py +245 -0
  21. mannf/core/agents/analyzer_agent.py +224 -0
  22. mannf/core/agents/autonomous_loop_agent.py +1086 -0
  23. mannf/core/agents/autonomous_loop_models.py +62 -0
  24. mannf/core/agents/autonomous_run_differ.py +427 -0
  25. mannf/core/agents/base.py +128 -0
  26. mannf/core/agents/bdi_agent.py +330 -0
  27. mannf/core/agents/belief_state.py +202 -0
  28. mannf/core/agents/browser_coordinator_agent.py +224 -0
  29. mannf/core/agents/browser_executor_agent.py +410 -0
  30. mannf/core/agents/coordinator_agent.py +262 -0
  31. mannf/core/agents/executor_agent.py +222 -0
  32. mannf/core/agents/monitor_agent.py +188 -0
  33. mannf/core/agents/oracle_agent.py +150 -0
  34. mannf/core/agents/performance_testing_agent.py +279 -0
  35. mannf/core/agents/planner_agent.py +128 -0
  36. mannf/core/agents/test_agent.py +249 -0
  37. mannf/core/agents/visual_regression_agent.py +311 -0
  38. mannf/core/agents/web_crawler_agent.py +510 -0
  39. mannf/core/agents/worker_pool.py +366 -0
  40. mannf/core/anomaly/__init__.py +14 -0
  41. mannf/core/anomaly/enhanced_detector.py +541 -0
  42. mannf/core/browser/__init__.py +63 -0
  43. mannf/core/browser/accessibility_scanner.py +424 -0
  44. mannf/core/browser/discovery_model.py +178 -0
  45. mannf/core/browser/dom_snapshot.py +349 -0
  46. mannf/core/browser/ingestor_bridge.py +371 -0
  47. mannf/core/browser/performance_metrics.py +217 -0
  48. mannf/core/browser/reflection_analyzer.py +442 -0
  49. mannf/core/browser/scenario_generator.py +1100 -0
  50. mannf/core/browser/security_scenario_generator.py +695 -0
  51. mannf/core/browser/visual_comparer.py +159 -0
  52. mannf/core/diagnostics/__init__.py +28 -0
  53. mannf/core/diagnostics/failure_clusterer.py +211 -0
  54. mannf/core/diagnostics/flake_detector.py +233 -0
  55. mannf/core/diagnostics/root_cause_analyzer.py +273 -0
  56. mannf/core/distributed/__init__.py +16 -0
  57. mannf/core/distributed/endpoint.py +139 -0
  58. mannf/core/distributed/system_under_test.py +207 -0
  59. mannf/core/functional_orchestrator.py +428 -0
  60. mannf/core/messaging/__init__.py +11 -0
  61. mannf/core/messaging/bus.py +113 -0
  62. mannf/core/messaging/messages.py +89 -0
  63. mannf/core/nat_orchestrator.py +342 -0
  64. mannf/core/neural/__init__.py +183 -0
  65. mannf/core/orchestrator.py +272 -0
  66. mannf/core/prioritization/__init__.py +17 -0
  67. mannf/core/prioritization/adaptive_controller.py +509 -0
  68. mannf/core/prioritization/belief_prioritizer.py +231 -0
  69. mannf/core/prioritization/risk_scorer.py +430 -0
  70. mannf/core/reporting/__init__.py +12 -0
  71. mannf/core/reporting/unified_report.py +664 -0
  72. mannf/core/testing/__init__.py +17 -0
  73. mannf/core/testing/adaptive_controller.py +149 -0
  74. mannf/core/testing/models.py +179 -0
  75. mannf/core/validation/__init__.py +10 -0
  76. mannf/core/validation/self_validation_runner.py +180 -0
  77. mannf/dashboard/__init__.py +7 -0
  78. mannf/dashboard/app.py +9 -0
  79. mannf/dashboard/models.py +9 -0
  80. mannf/dashboard/static/index.html +2538 -0
  81. mannf/dashboard/telemetry.py +9 -0
  82. mannf/distributed/__init__.py +7 -0
  83. mannf/distributed/endpoint.py +9 -0
  84. mannf/distributed/system_under_test.py +9 -0
  85. mannf/healing/__init__.py +7 -0
  86. mannf/healing/graphql_schema_diff.py +9 -0
  87. mannf/healing/healer.py +9 -0
  88. mannf/healing/models.py +9 -0
  89. mannf/healing/schema_diff.py +9 -0
  90. mannf/integrations/__init__.py +7 -0
  91. mannf/integrations/auth.py +9 -0
  92. mannf/integrations/graphql_parser.py +9 -0
  93. mannf/integrations/graphql_sut.py +9 -0
  94. mannf/integrations/http_sut.py +9 -0
  95. mannf/integrations/openapi_parser.py +9 -0
  96. mannf/integrations/postman_parser.py +9 -0
  97. mannf/llm/__init__.py +7 -0
  98. mannf/llm/anthropic_provider.py +9 -0
  99. mannf/llm/base.py +9 -0
  100. mannf/llm/config.py +9 -0
  101. mannf/llm/factory.py +9 -0
  102. mannf/llm/openai_provider.py +9 -0
  103. mannf/llm/prompts.py +9 -0
  104. mannf/messaging/__init__.py +7 -0
  105. mannf/messaging/bus.py +9 -0
  106. mannf/messaging/messages.py +9 -0
  107. mannf/nat_orchestrator.py +9 -0
  108. mannf/neural/__init__.py +7 -0
  109. mannf/orchestrator.py +9 -0
  110. mannf/prioritization/__init__.py +7 -0
  111. mannf/prioritization/adaptive_controller.py +9 -0
  112. mannf/prioritization/belief_prioritizer.py +9 -0
  113. mannf/prioritization/risk_scorer.py +9 -0
  114. mannf/product/__init__.py +29 -0
  115. mannf/product/admin/__init__.py +3 -0
  116. mannf/product/admin/routes.py +514 -0
  117. mannf/product/auth/__init__.py +5 -0
  118. mannf/product/auth/saml.py +212 -0
  119. mannf/product/billing/__init__.py +5 -0
  120. mannf/product/billing/audit.py +160 -0
  121. mannf/product/billing/feature_gates.py +180 -0
  122. mannf/product/billing/metering.py +179 -0
  123. mannf/product/billing/notifications.py +181 -0
  124. mannf/product/billing/plans.py +133 -0
  125. mannf/product/billing/rate_limits.py +35 -0
  126. mannf/product/billing/stripe_billing.py +906 -0
  127. mannf/product/billing/tenant_auth.py +233 -0
  128. mannf/product/billing/tenant_manager.py +873 -0
  129. mannf/product/cli.py +3900 -0
  130. mannf/product/cli_admin.py +408 -0
  131. mannf/product/dashboard/__init__.py +61 -0
  132. mannf/product/dashboard/app.py +3567 -0
  133. mannf/product/dashboard/models.py +460 -0
  134. mannf/product/dashboard/static/index.html +6347 -0
  135. mannf/product/dashboard/static/manifest.json +25 -0
  136. mannf/product/dashboard/static/pwa-icon-192.png +0 -0
  137. mannf/product/dashboard/static/pwa-icon-512.png +0 -0
  138. mannf/product/dashboard/static/sw.js +64 -0
  139. mannf/product/dashboard/telemetry.py +547 -0
  140. mannf/product/database.py +145 -0
  141. mannf/product/demo.py +844 -0
  142. mannf/product/doctor.py +509 -0
  143. mannf/product/exporters/__init__.py +65 -0
  144. mannf/product/exporters/azuredevops_exporter.py +257 -0
  145. mannf/product/exporters/base.py +307 -0
  146. mannf/product/exporters/bugzilla_exporter.py +200 -0
  147. mannf/product/exporters/dedup.py +275 -0
  148. mannf/product/exporters/finding_adapter.py +216 -0
  149. mannf/product/exporters/github_exporter.py +197 -0
  150. mannf/product/exporters/gitlab_exporter.py +215 -0
  151. mannf/product/exporters/jira_exporter.py +180 -0
  152. mannf/product/exporters/linear_exporter.py +195 -0
  153. mannf/product/exporters/loader.py +233 -0
  154. mannf/product/exporters/pagerduty_exporter.py +363 -0
  155. mannf/product/exporters/sentry_exporter.py +322 -0
  156. mannf/product/exporters/servicenow_exporter.py +240 -0
  157. mannf/product/exporters/shortcut_exporter.py +231 -0
  158. mannf/product/exporters/webhook_exporter.py +383 -0
  159. mannf/product/formatters/__init__.py +18 -0
  160. mannf/product/formatters/allure_formatter.py +161 -0
  161. mannf/product/formatters/ctrf_formatter.py +149 -0
  162. mannf/product/healing/__init__.py +30 -0
  163. mannf/product/healing/graphql_schema_diff.py +152 -0
  164. mannf/product/healing/healer.py +141 -0
  165. mannf/product/healing/models.py +175 -0
  166. mannf/product/healing/schema_diff.py +251 -0
  167. mannf/product/ingestors/__init__.py +77 -0
  168. mannf/product/ingestors/base.py +256 -0
  169. mannf/product/ingestors/bgstm_ingestor.py +764 -0
  170. mannf/product/ingestors/curl_ingestor.py +1019 -0
  171. mannf/product/ingestors/cypress_ingestor.py +487 -0
  172. mannf/product/ingestors/gherkin_ingestor.py +967 -0
  173. mannf/product/ingestors/graphql_ingestor.py +845 -0
  174. mannf/product/ingestors/grpc_ingestor.py +591 -0
  175. mannf/product/ingestors/har_ingestor.py +976 -0
  176. mannf/product/ingestors/loader.py +284 -0
  177. mannf/product/ingestors/models.py +146 -0
  178. mannf/product/ingestors/openapi_ingestor.py +606 -0
  179. mannf/product/ingestors/playwright_ingestor.py +449 -0
  180. mannf/product/ingestors/postman_ingestor.py +631 -0
  181. mannf/product/ingestors/traffic_ingestor.py +679 -0
  182. mannf/product/ingestors/websocket_ingestor.py +526 -0
  183. mannf/product/integrations/__init__.py +21 -0
  184. mannf/product/integrations/auth.py +190 -0
  185. mannf/product/integrations/graphql_parser.py +436 -0
  186. mannf/product/integrations/graphql_sut.py +247 -0
  187. mannf/product/integrations/grpc_sut.py +469 -0
  188. mannf/product/integrations/http_sut.py +237 -0
  189. mannf/product/integrations/kafka_adapter.py +342 -0
  190. mannf/product/integrations/openapi_parser.py +513 -0
  191. mannf/product/integrations/postman_parser.py +467 -0
  192. mannf/product/integrations/webhook_receiver.py +344 -0
  193. mannf/product/integrations/websocket_sut.py +434 -0
  194. mannf/product/llm/__init__.py +25 -0
  195. mannf/product/llm/anthropic_provider.py +94 -0
  196. mannf/product/llm/base.py +267 -0
  197. mannf/product/llm/config.py +48 -0
  198. mannf/product/llm/factory.py +42 -0
  199. mannf/product/llm/openai_provider.py +93 -0
  200. mannf/product/llm/prompts.py +403 -0
  201. mannf/product/llm/root_cause_service.py +311 -0
  202. mannf/product/llm/test_plan_models.py +78 -0
  203. mannf/product/metrics.py +149 -0
  204. mannf/product/middleware/__init__.py +3 -0
  205. mannf/product/middleware/audit_middleware.py +112 -0
  206. mannf/product/middleware/tenant_isolation.py +114 -0
  207. mannf/product/models.py +347 -0
  208. mannf/product/notifications/__init__.py +24 -0
  209. mannf/product/notifications/dispatcher.py +411 -0
  210. mannf/product/onboarding.py +190 -0
  211. mannf/product/orchestration/__init__.py +39 -0
  212. mannf/product/orchestration/ingest_scan_orchestrator.py +339 -0
  213. mannf/product/orchestration/pipeline.py +401 -0
  214. mannf/product/orchestrator.py +987 -0
  215. mannf/product/orchestrator_models.py +269 -0
  216. mannf/product/regression/__init__.py +36 -0
  217. mannf/product/regression/differ.py +172 -0
  218. mannf/product/regression/masking.py +100 -0
  219. mannf/product/regression/models.py +232 -0
  220. mannf/product/regression/recorder.py +124 -0
  221. mannf/product/regression/replayer.py +168 -0
  222. mannf/product/reports/__init__.py +10 -0
  223. mannf/product/reports/pdf.py +132 -0
  224. mannf/product/scheduling/__init__.py +57 -0
  225. mannf/product/scheduling/cron_utils.py +251 -0
  226. mannf/product/scheduling/engine.py +473 -0
  227. mannf/product/scheduling/models.py +86 -0
  228. mannf/product/scheduling/queue.py +894 -0
  229. mannf/product/scheduling/store.py +235 -0
  230. mannf/product/security/__init__.py +21 -0
  231. mannf/product/security/belief_guided.py +143 -0
  232. mannf/product/security/checks/__init__.py +55 -0
  233. mannf/product/security/checks/base.py +69 -0
  234. mannf/product/security/checks/bfla.py +77 -0
  235. mannf/product/security/checks/bola.py +77 -0
  236. mannf/product/security/checks/bopla.py +80 -0
  237. mannf/product/security/checks/broken_auth.py +86 -0
  238. mannf/product/security/checks/graphql_security.py +299 -0
  239. mannf/product/security/checks/inventory.py +70 -0
  240. mannf/product/security/checks/misconfig.py +158 -0
  241. mannf/product/security/checks/resource_consumption.py +70 -0
  242. mannf/product/security/checks/sensitive_flows.py +80 -0
  243. mannf/product/security/checks/ssrf.py +101 -0
  244. mannf/product/security/checks/unsafe_consumption.py +120 -0
  245. mannf/product/security/models.py +92 -0
  246. mannf/product/security/plugin_loader.py +182 -0
  247. mannf/product/security/reporter.py +92 -0
  248. mannf/product/security/scanner.py +183 -0
  249. mannf/product/server.py +6220 -0
  250. mannf/product/setup_wizard.py +873 -0
  251. mannf/product/status.py +404 -0
  252. mannf/product/storage/__init__.py +10 -0
  253. mannf/product/storage/artifact_store.py +343 -0
  254. mannf/product/telemetry.py +300 -0
  255. mannf/product/uninstall.py +169 -0
  256. mannf/product/upgrade.py +139 -0
  257. mannf/product/weights/__init__.py +13 -0
  258. mannf/product/weights/blob_store.py +299 -0
  259. mannf/product/weights/factory.py +42 -0
  260. mannf/product/weights/registry.py +159 -0
  261. mannf/product/weights/store.py +210 -0
  262. mannf/regression/__init__.py +7 -0
  263. mannf/regression/differ.py +9 -0
  264. mannf/regression/masking.py +9 -0
  265. mannf/regression/models.py +9 -0
  266. mannf/regression/recorder.py +9 -0
  267. mannf/regression/replayer.py +9 -0
  268. mannf/security/__init__.py +7 -0
  269. mannf/security/belief_guided.py +9 -0
  270. mannf/security/checks/__init__.py +7 -0
  271. mannf/security/checks/base.py +9 -0
  272. mannf/security/checks/bfla.py +9 -0
  273. mannf/security/checks/bola.py +9 -0
  274. mannf/security/checks/bopla.py +9 -0
  275. mannf/security/checks/broken_auth.py +9 -0
  276. mannf/security/checks/graphql_security.py +9 -0
  277. mannf/security/checks/inventory.py +9 -0
  278. mannf/security/checks/misconfig.py +9 -0
  279. mannf/security/checks/resource_consumption.py +9 -0
  280. mannf/security/checks/sensitive_flows.py +9 -0
  281. mannf/security/checks/ssrf.py +9 -0
  282. mannf/security/checks/unsafe_consumption.py +9 -0
  283. mannf/security/models.py +9 -0
  284. mannf/security/reporter.py +9 -0
  285. mannf/security/scanner.py +9 -0
  286. mannf/server.py +9 -0
  287. mannf/testing/__init__.py +7 -0
  288. mannf/testing/adaptive_controller.py +9 -0
  289. mannf/testing/models.py +9 -0
  290. mannf/weights/__init__.py +7 -0
  291. mannf/weights/registry.py +9 -0
  292. mannf/weights/store.py +9 -0
  293. nat_engine-1.dist-info/METADATA +555 -0
  294. nat_engine-1.dist-info/RECORD +299 -0
  295. nat_engine-1.dist-info/WHEEL +5 -0
  296. nat_engine-1.dist-info/entry_points.txt +4 -0
  297. nat_engine-1.dist-info/licenses/LICENSE +651 -0
  298. nat_engine-1.dist-info/licenses/NOTICE +178 -0
  299. nat_engine-1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,247 @@
1
+ # Copyright (C) 2026 Brad Guider
2
+ # This file is part of NAT (Neural Agent Testing Framework).
3
+ # Licensed under the AGPL-3.0. See LICENSE for details.
4
+ # Commercial licensing available — see COMMERCIAL_LICENSE.md.
5
+
6
+ """GraphQL System-Under-Test implementation.
7
+
8
+ :class:`GraphQLSUT` introspects a GraphQL endpoint, generates test cases via
9
+ :class:`~mannf.product.integrations.graphql_parser.GraphQLParser`, and executes them
10
+ using ``httpx``.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import logging
16
+ import time
17
+ from typing import Any, Dict, List, Optional
18
+
19
+ import httpx
20
+
21
+ from mannf.product.integrations.auth import AuthStrategy, NoAuth
22
+ from mannf.product.integrations.graphql_parser import GraphQLParser
23
+ from mannf.core.testing.models import TestCase, TestResult
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+ # Standard GraphQL introspection query
28
+ _INTROSPECTION_QUERY = """
29
+ {
30
+ __schema {
31
+ queryType { name }
32
+ mutationType { name }
33
+ subscriptionType { name }
34
+ types {
35
+ name
36
+ kind
37
+ fields {
38
+ name
39
+ type { name kind ofType { name kind } }
40
+ args { name type { name kind ofType { name kind } } defaultValue }
41
+ }
42
+ }
43
+ }
44
+ }
45
+ """
46
+
47
+
48
+ class GraphQLSUT:
49
+ """Execute GraphQL test cases against a GraphQL API endpoint.
50
+
51
+ Parameters
52
+ ----------
53
+ endpoint_url:
54
+ Full URL to the GraphQL endpoint (e.g. ``"http://localhost:4000/graphql"``).
55
+ auth:
56
+ Authentication strategy applied before each request.
57
+ Defaults to :class:`~mannf.product.integrations.auth.NoAuth`.
58
+ timeout:
59
+ HTTP request timeout in seconds (default: 10).
60
+ headers:
61
+ Additional HTTP headers added to every request.
62
+ """
63
+
64
+ def __init__(
65
+ self,
66
+ endpoint_url: str,
67
+ auth: Optional[AuthStrategy] = None,
68
+ timeout: float = 10.0,
69
+ headers: Optional[Dict[str, str]] = None,
70
+ recorder: Optional[Any] = None,
71
+ ) -> None:
72
+ self.endpoint_url = endpoint_url
73
+ self.auth: AuthStrategy = auth or NoAuth()
74
+ self.timeout = timeout
75
+ self.headers: Dict[str, str] = {
76
+ "Content-Type": "application/json",
77
+ **(headers or {}),
78
+ }
79
+ self.recorder = recorder
80
+
81
+ # ------------------------------------------------------------------
82
+ # Core helpers
83
+ # ------------------------------------------------------------------
84
+
85
+ async def _post(self, payload: Any) -> httpx.Response:
86
+ """POST *payload* to the GraphQL endpoint, applying auth."""
87
+ request_kwargs: Dict[str, Any] = {
88
+ "method": "POST",
89
+ "url": self.endpoint_url,
90
+ "headers": dict(self.headers),
91
+ "json": payload,
92
+ "timeout": self.timeout,
93
+ }
94
+ request_kwargs = await self.auth.apply(request_kwargs)
95
+
96
+ async with httpx.AsyncClient() as client:
97
+ return await client.request(**request_kwargs)
98
+
99
+ # ------------------------------------------------------------------
100
+ # Public API
101
+ # ------------------------------------------------------------------
102
+
103
+ async def introspect(self) -> dict:
104
+ """Send a GraphQL introspection query and return the raw result dict."""
105
+ response = await self._post({"query": _INTROSPECTION_QUERY})
106
+ return response.json()
107
+
108
+ async def execute_query(
109
+ self, query: str, variables: Optional[dict] = None
110
+ ) -> dict:
111
+ """Execute a GraphQL query.
112
+
113
+ Returns
114
+ -------
115
+ dict
116
+ Parsed JSON body — typically ``{"data": ..., "errors": [...]}``.
117
+ """
118
+ payload: Dict[str, Any] = {"query": query}
119
+ if variables:
120
+ payload["variables"] = variables
121
+ response = await self._post(payload)
122
+ return response.json()
123
+
124
+ async def execute_mutation(
125
+ self, mutation: str, variables: Optional[dict] = None
126
+ ) -> dict:
127
+ """Execute a GraphQL mutation.
128
+
129
+ Parameters
130
+ ----------
131
+ mutation:
132
+ The GraphQL mutation document string.
133
+ variables:
134
+ Optional dict of variable values.
135
+ """
136
+ return await self.execute_query(mutation, variables)
137
+
138
+ async def generate_test_cases(self) -> List[TestCase]:
139
+ """Introspect the schema and return generated :class:`~mannf.core.testing.models.TestCase` objects."""
140
+ raw = await self.introspect()
141
+ parser = GraphQLParser(raw)
142
+ schema = parser.parse()
143
+ return parser.generate_test_cases(schema, self.endpoint_url)
144
+
145
+ async def run_test(self, test_case: TestCase) -> TestResult:
146
+ """Execute a single :class:`~mannf.core.testing.models.TestCase` and return a :class:`~mannf.core.testing.models.TestResult`.
147
+
148
+ A GraphQL response is considered **passing** when:
149
+
150
+ 1. The HTTP status code is 200 (or matches ``expected_status_codes``).
151
+ 2. The response body contains a ``"data"`` key.
152
+
153
+ If the response has an ``"errors"`` key it is logged as a warning but
154
+ does **not** automatically cause the test to fail.
155
+ """
156
+ json_payload = test_case.inputs.get("json")
157
+ expected_codes = frozenset(
158
+ test_case.inputs.get("expected_status_codes") or [200]
159
+ )
160
+
161
+ start = time.monotonic()
162
+ try:
163
+ response = await self._post(json_payload)
164
+ elapsed_ms = (time.monotonic() - start) * 1000.0
165
+
166
+ status_ok = response.status_code in expected_codes
167
+
168
+ try:
169
+ body: Any = response.json()
170
+ except Exception:
171
+ body = response.text
172
+
173
+ error_msg: Optional[str] = None
174
+
175
+ if not status_ok:
176
+ error_msg = f"Unexpected HTTP status {response.status_code}"
177
+ result = TestResult(
178
+ test_case_id=test_case.id,
179
+ passed=False,
180
+ actual_output=body,
181
+ execution_time_ms=elapsed_ms,
182
+ error=error_msg,
183
+ )
184
+ else:
185
+ # A valid GraphQL response must have a "data" key
186
+ if isinstance(body, dict):
187
+ has_data = "data" in body
188
+ gql_errors = body.get("errors")
189
+ if gql_errors:
190
+ logger.warning(
191
+ "GraphQL errors in response for %s: %s",
192
+ test_case.target,
193
+ gql_errors,
194
+ )
195
+ passed = has_data
196
+ if not passed:
197
+ error_msg = "Response missing 'data' key"
198
+ else:
199
+ # Batch responses are lists — treat as passing when status is OK
200
+ passed = True
201
+
202
+ result = TestResult(
203
+ test_case_id=test_case.id,
204
+ passed=passed,
205
+ actual_output=body,
206
+ execution_time_ms=elapsed_ms,
207
+ error=error_msg,
208
+ )
209
+
210
+ if self.recorder is not None:
211
+ self.recorder.record(
212
+ test_case_id=test_case.id,
213
+ method="POST",
214
+ url=self.endpoint_url,
215
+ request_headers=dict(self.headers),
216
+ request_body=json_payload,
217
+ query_params=None,
218
+ status_code=response.status_code,
219
+ response_headers=dict(response.headers),
220
+ response_body=body,
221
+ latency_ms=elapsed_ms,
222
+ passed=result.passed,
223
+ error=result.error,
224
+ )
225
+
226
+ return result
227
+
228
+ except httpx.TimeoutException as exc:
229
+ elapsed_ms = (time.monotonic() - start) * 1000.0
230
+ logger.warning("GraphQLSUT: timeout for %s — %s", self.endpoint_url, exc)
231
+ return TestResult(
232
+ test_case_id=test_case.id,
233
+ passed=False,
234
+ actual_output=None,
235
+ execution_time_ms=elapsed_ms,
236
+ error=f"Request timed out: {exc}",
237
+ )
238
+ except httpx.RequestError as exc:
239
+ elapsed_ms = (time.monotonic() - start) * 1000.0
240
+ logger.warning("GraphQLSUT: connection error for %s — %s", self.endpoint_url, exc)
241
+ return TestResult(
242
+ test_case_id=test_case.id,
243
+ passed=False,
244
+ actual_output=None,
245
+ execution_time_ms=elapsed_ms,
246
+ error=f"Connection error: {exc}",
247
+ )