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,513 @@
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
+ """OpenAPI 3.x / Swagger 2.x spec parser.
7
+
8
+ Reads a spec file (JSON or YAML) and auto-generates :class:`~mannf.core.testing.models.TestCase`
9
+ objects covering both positive and negative test scenarios. Discovered
10
+ endpoints are also registered into a provided :class:`~mannf.core.distributed.endpoint.EndpointRegistry`.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import json
16
+ import logging
17
+ import re
18
+ from pathlib import Path
19
+ from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple
20
+ from urllib.parse import urlparse
21
+
22
+ import yaml
23
+
24
+ from mannf.core.distributed.endpoint import Endpoint, EndpointRegistry
25
+ from mannf.core.testing.models import TestCase
26
+
27
+ if TYPE_CHECKING:
28
+ from mannf.product.llm.base import LLMProvider
29
+
30
+ logger = logging.getLogger(__name__)
31
+
32
+ # ---------------------------------------------------------------------------
33
+ # Helpers
34
+ # ---------------------------------------------------------------------------
35
+
36
+ _SCALAR_DEFAULTS: Dict[str, Any] = {
37
+ "string": "example",
38
+ "integer": 1,
39
+ "number": 1.0,
40
+ "boolean": True,
41
+ "array": [],
42
+ "object": {},
43
+ }
44
+
45
+ _WRONG_TYPE_VALUES: Dict[str, Any] = {
46
+ "string": 42,
47
+ "integer": "not-a-number",
48
+ "number": "not-a-number",
49
+ "boolean": "not-a-boolean",
50
+ }
51
+
52
+
53
+ def _load_spec(path: str) -> Dict[str, Any]:
54
+ """Load a JSON or YAML spec file from *path*."""
55
+ p = Path(path)
56
+ text = p.read_text(encoding="utf-8")
57
+ if p.suffix.lower() in {".yaml", ".yml"}:
58
+ return yaml.safe_load(text)
59
+ return json.loads(text)
60
+
61
+
62
+ def _resolve_ref(spec: Dict[str, Any], ref: str) -> Dict[str, Any]:
63
+ """Resolve a ``$ref`` pointer within *spec* (internal refs only)."""
64
+ if not ref.startswith("#/"):
65
+ return {}
66
+ parts = ref.lstrip("#/").split("/")
67
+ node: Any = spec
68
+ for part in parts:
69
+ part = part.replace("~1", "/").replace("~0", "~")
70
+ if not isinstance(node, dict) or part not in node:
71
+ return {}
72
+ node = node[part]
73
+ return node if isinstance(node, dict) else {}
74
+
75
+
76
+ def _schema_example(spec: Dict[str, Any], schema: Dict[str, Any], depth: int = 0) -> Any:
77
+ """Generate a minimal valid example value for *schema*."""
78
+ if depth > 5:
79
+ return None
80
+
81
+ if "$ref" in schema:
82
+ schema = _resolve_ref(spec, schema["$ref"])
83
+
84
+ schema_type = schema.get("type", "object")
85
+
86
+ if "example" in schema:
87
+ return schema["example"]
88
+ if "default" in schema:
89
+ return schema["default"]
90
+ if "enum" in schema and schema["enum"]:
91
+ return schema["enum"][0]
92
+
93
+ if schema_type == "object" or "properties" in schema:
94
+ props = schema.get("properties", {})
95
+ return {k: _schema_example(spec, v, depth + 1) for k, v in props.items()}
96
+
97
+ if schema_type == "array":
98
+ items = schema.get("items", {})
99
+ return [_schema_example(spec, items, depth + 1)]
100
+
101
+ return _SCALAR_DEFAULTS.get(schema_type, None)
102
+
103
+
104
+ def _detect_version(spec: Dict[str, Any]) -> str:
105
+ """Return ``"openapi3"`` or ``"swagger2"``."""
106
+ if "openapi" in spec:
107
+ return "openapi3"
108
+ if "swagger" in spec:
109
+ return "swagger2"
110
+ return "openapi3"
111
+
112
+
113
+ def _base_url_from_spec(spec: Dict[str, Any]) -> Optional[str]:
114
+ """Extract base URL from the spec if present."""
115
+ version = _detect_version(spec)
116
+ if version == "openapi3":
117
+ servers = spec.get("servers", [])
118
+ if servers:
119
+ return servers[0].get("url", "").rstrip("/")
120
+ else: # swagger2
121
+ host = spec.get("host", "")
122
+ base_path = spec.get("basePath", "/").rstrip("/")
123
+ scheme = (spec.get("schemes") or ["http"])[0]
124
+ if host:
125
+ return f"{scheme}://{host}{base_path}"
126
+ return None
127
+
128
+
129
+ def _path_to_endpoint_name(path: str) -> str:
130
+ """Turn ``/users/{id}/orders`` into ``users-id-orders``."""
131
+ name = re.sub(r"[{}]", "", path)
132
+ name = re.sub(r"[^a-zA-Z0-9]+", "-", name).strip("-")
133
+ return name or "root"
134
+
135
+
136
+ # ---------------------------------------------------------------------------
137
+ # Public API
138
+ # ---------------------------------------------------------------------------
139
+
140
+ class OpenApiParser:
141
+ """Parse an OpenAPI / Swagger spec and generate :class:`TestCase` objects.
142
+
143
+ Parameters
144
+ ----------
145
+ spec_path:
146
+ Path to the spec file (JSON or YAML).
147
+ base_url:
148
+ Override the base URL extracted from the spec. When ``None`` the
149
+ parser tries to read ``servers[0].url`` (OpenAPI 3) or
150
+ ``host`` + ``basePath`` (Swagger 2).
151
+ registry:
152
+ :class:`EndpointRegistry` to populate with discovered endpoints.
153
+ A fresh registry is created when ``None`` is passed.
154
+ max_tests:
155
+ Maximum number of :class:`TestCase` objects to generate. ``None``
156
+ means no limit.
157
+ """
158
+
159
+ def __init__(
160
+ self,
161
+ spec_path: str,
162
+ base_url: Optional[str] = None,
163
+ registry: Optional[EndpointRegistry] = None,
164
+ max_tests: Optional[int] = None,
165
+ llm_provider: Optional["LLMProvider"] = None,
166
+ ) -> None:
167
+ self.spec_path = spec_path
168
+ self.spec = _load_spec(spec_path)
169
+ self.base_url = (base_url or _base_url_from_spec(self.spec) or "").rstrip("/")
170
+ self.registry = registry if registry is not None else EndpointRegistry()
171
+ self.max_tests = max_tests
172
+ self._version = _detect_version(self.spec)
173
+ self.llm_provider = llm_provider
174
+
175
+ def parse(self) -> List[TestCase]:
176
+ """Parse the spec and return all generated :class:`TestCase` objects."""
177
+ test_cases: List[TestCase] = []
178
+ paths = self.spec.get("paths", {})
179
+
180
+ for path, path_item in paths.items():
181
+ if not isinstance(path_item, dict):
182
+ continue
183
+
184
+ # Register endpoint
185
+ ep_name = _path_to_endpoint_name(path)
186
+ parsed_url = urlparse(self.base_url) if self.base_url else None
187
+ host = parsed_url.hostname or "localhost" if parsed_url else "localhost"
188
+ port = (
189
+ parsed_url.port or (443 if (parsed_url and parsed_url.scheme == "https") else 80)
190
+ if parsed_url else 80
191
+ )
192
+ protocol = (parsed_url.scheme or "http") if parsed_url else "http"
193
+
194
+ endpoint = Endpoint(
195
+ name=ep_name,
196
+ host=host,
197
+ port=port,
198
+ protocol=protocol,
199
+ tags={"path": path},
200
+ )
201
+ self.registry.register(endpoint)
202
+
203
+ for method in ("get", "post", "put", "patch", "delete", "head", "options"):
204
+ operation = path_item.get(method)
205
+ if not isinstance(operation, dict):
206
+ continue
207
+
208
+ cases = self._generate_cases_for_operation(path, method.upper(), operation)
209
+ test_cases.extend(cases)
210
+
211
+ if self.max_tests is not None and len(test_cases) >= self.max_tests:
212
+ return test_cases[: self.max_tests]
213
+
214
+ return test_cases
215
+
216
+ async def parse_with_llm(self) -> List[TestCase]:
217
+ """Parse the spec and augment with LLM-generated test cases when available.
218
+
219
+ Behaves identically to :meth:`parse` when no LLM provider is configured
220
+ or when the provider is not available (fail-safe).
221
+ """
222
+ test_cases: List[TestCase] = []
223
+ paths = self.spec.get("paths", {})
224
+
225
+ for path, path_item in paths.items():
226
+ if not isinstance(path_item, dict):
227
+ continue
228
+
229
+ # Register endpoint
230
+ ep_name = _path_to_endpoint_name(path)
231
+ parsed_url = urlparse(self.base_url) if self.base_url else None
232
+ host = parsed_url.hostname or "localhost" if parsed_url else "localhost"
233
+ port = (
234
+ parsed_url.port or (443 if (parsed_url and parsed_url.scheme == "https") else 80)
235
+ if parsed_url else 80
236
+ )
237
+ protocol = (parsed_url.scheme or "http") if parsed_url else "http"
238
+
239
+ endpoint = Endpoint(
240
+ name=ep_name,
241
+ host=host,
242
+ port=port,
243
+ protocol=protocol,
244
+ tags={"path": path},
245
+ )
246
+ self.registry.register(endpoint)
247
+
248
+ for method in ("get", "post", "put", "patch", "delete", "head", "options"):
249
+ operation = path_item.get(method)
250
+ if not isinstance(operation, dict):
251
+ continue
252
+
253
+ cases = self._generate_cases_for_operation(path, method.upper(), operation)
254
+
255
+ # LLM augmentation
256
+ if self.llm_provider is not None and self.llm_provider.is_available():
257
+ body_schema, _ = self._extract_body_schema(operation)
258
+ parameters = operation.get("parameters", [])
259
+ endpoint_info = {
260
+ "method": method.upper(),
261
+ "path": path,
262
+ "description": (
263
+ operation.get("summary")
264
+ or operation.get("description")
265
+ or ""
266
+ ),
267
+ "parameters": parameters,
268
+ "schema": body_schema or {},
269
+ }
270
+ spec_context = {
271
+ "info": self.spec.get("info", {}),
272
+ "servers": self.spec.get("servers", []),
273
+ }
274
+ llm_cases = await self._generate_llm_cases(
275
+ path, method.upper(), operation, endpoint_info, spec_context
276
+ )
277
+ cases.extend(llm_cases)
278
+
279
+ test_cases.extend(cases)
280
+
281
+ if self.max_tests is not None and len(test_cases) >= self.max_tests:
282
+ return test_cases[: self.max_tests]
283
+
284
+ return test_cases
285
+
286
+ async def _generate_llm_cases(
287
+ self,
288
+ path: str,
289
+ method: str,
290
+ operation: Dict[str, Any],
291
+ endpoint_info: Dict[str, Any],
292
+ spec_context: Dict[str, Any],
293
+ ) -> List[TestCase]:
294
+ """Call the LLM provider and convert results to :class:`TestCase` objects."""
295
+ assert self.llm_provider is not None
296
+ try:
297
+ raw_cases = await self.llm_provider.generate_test_cases(
298
+ endpoint_info, spec_context, n=5
299
+ )
300
+ except Exception as exc: # noqa: BLE001
301
+ logger.warning("OpenApiParser: LLM generation failed for %s %s: %s", method, path, exc)
302
+ return []
303
+
304
+ cases: List[TestCase] = []
305
+ summary = operation.get("summary") or operation.get("operationId") or f"{method} {path}"
306
+ for raw in raw_cases:
307
+ if not isinstance(raw, dict):
308
+ continue
309
+ inputs = raw.get("inputs", {})
310
+ if not isinstance(inputs, dict):
311
+ inputs = {}
312
+ inputs.setdefault("method", method)
313
+ cases.append(
314
+ TestCase(
315
+ target=path,
316
+ inputs=inputs,
317
+ expected_behavior=raw.get("expected_behavior", f"[llm] {summary}"),
318
+ priority=0.7,
319
+ metadata={
320
+ "spec_path": path,
321
+ "method": method,
322
+ "test_type": raw.get("test_type", "llm"),
323
+ "generator": "llm",
324
+ "generation_strategy": "llm_edge_case",
325
+ },
326
+ )
327
+ )
328
+ return cases
329
+
330
+ # ------------------------------------------------------------------
331
+ # Internal helpers
332
+ # ------------------------------------------------------------------
333
+
334
+ def _generate_cases_for_operation(
335
+ self,
336
+ path: str,
337
+ method: str,
338
+ operation: Dict[str, Any],
339
+ ) -> List[TestCase]:
340
+ """Return positive + negative test cases for one operation."""
341
+ cases: List[TestCase] = []
342
+ summary = operation.get("summary") or operation.get("operationId") or f"{method} {path}"
343
+
344
+ # Collect parameter info
345
+ parameters: List[Dict[str, Any]] = operation.get("parameters", [])
346
+ # Inherit path-level parameters if any
347
+ path_params, query_params, header_params = self._parse_parameters(parameters)
348
+
349
+ # Request body (OpenAPI 3 / Swagger 2)
350
+ body_schema, required_body_fields = self._extract_body_schema(operation)
351
+
352
+ # ---- Positive test case ----------------------------------------
353
+ pos_inputs: Dict[str, Any] = {"method": method}
354
+ if query_params:
355
+ pos_inputs["query_params"] = query_params
356
+ if header_params:
357
+ pos_inputs["headers"] = header_params
358
+
359
+ if body_schema and method in {"POST", "PUT", "PATCH"}:
360
+ pos_inputs["json"] = _schema_example(self.spec, body_schema)
361
+
362
+ # Replace path parameters in the target path
363
+ resolved_path = path
364
+ for param_name, param_value in path_params.items():
365
+ resolved_path = resolved_path.replace(f"{{{param_name}}}", str(param_value))
366
+
367
+ cases.append(
368
+ TestCase(
369
+ target=resolved_path,
370
+ inputs=pos_inputs,
371
+ expected_behavior=f"[positive] {summary}",
372
+ priority=0.5,
373
+ metadata={"spec_path": path, "method": method, "test_type": "positive"},
374
+ )
375
+ )
376
+
377
+ # ---- Negative test cases ----------------------------------------
378
+ # 1. Missing required body fields
379
+ if body_schema and required_body_fields and method in {"POST", "PUT", "PATCH"}:
380
+ full_body = _schema_example(self.spec, body_schema) or {}
381
+ if isinstance(full_body, dict) and required_body_fields:
382
+ # Drop the first required field
383
+ field_to_drop = next(iter(required_body_fields))
384
+ missing_body = {k: v for k, v in full_body.items() if k != field_to_drop}
385
+ neg_inputs = {**pos_inputs, "json": missing_body, "expected_status_codes": [400, 422]}
386
+ cases.append(
387
+ TestCase(
388
+ target=resolved_path,
389
+ inputs=neg_inputs,
390
+ expected_behavior=f"[negative] {summary}: missing required field '{field_to_drop}'",
391
+ priority=0.6,
392
+ metadata={
393
+ "spec_path": path,
394
+ "method": method,
395
+ "test_type": "negative",
396
+ "neg_type": "missing_required_field",
397
+ "missing_field": field_to_drop,
398
+ },
399
+ )
400
+ )
401
+
402
+ # 2. Wrong type for first required body field
403
+ if body_schema and required_body_fields and method in {"POST", "PUT", "PATCH"}:
404
+ full_body = _schema_example(self.spec, body_schema) or {}
405
+ if isinstance(full_body, dict):
406
+ field_name = next(iter(required_body_fields))
407
+ props = body_schema.get("properties", {})
408
+ field_schema = props.get(field_name, {})
409
+ if "$ref" in field_schema:
410
+ field_schema = _resolve_ref(self.spec, field_schema["$ref"])
411
+ field_type = field_schema.get("type", "string")
412
+ wrong_val = _WRONG_TYPE_VALUES.get(field_type)
413
+ if wrong_val is not None:
414
+ wrong_body = {**full_body, field_name: wrong_val}
415
+ neg_inputs2 = {
416
+ **pos_inputs,
417
+ "json": wrong_body,
418
+ "expected_status_codes": [400, 422],
419
+ }
420
+ cases.append(
421
+ TestCase(
422
+ target=resolved_path,
423
+ inputs=neg_inputs2,
424
+ expected_behavior=(
425
+ f"[negative] {summary}: wrong type for '{field_name}'"
426
+ ),
427
+ priority=0.6,
428
+ metadata={
429
+ "spec_path": path,
430
+ "method": method,
431
+ "test_type": "negative",
432
+ "neg_type": "wrong_type",
433
+ "field": field_name,
434
+ },
435
+ )
436
+ )
437
+
438
+ # 3. Non-existent resource (append bogus ID for paths ending with a param)
439
+ if "{" not in path and method == "GET":
440
+ neg_path = resolved_path.rstrip("/") + "/99999999"
441
+ neg_inputs3 = {**pos_inputs, "expected_status_codes": [404]}
442
+ cases.append(
443
+ TestCase(
444
+ target=neg_path,
445
+ inputs=neg_inputs3,
446
+ expected_behavior=f"[negative] {summary}: non-existent resource",
447
+ priority=0.4,
448
+ metadata={
449
+ "spec_path": path,
450
+ "method": method,
451
+ "test_type": "negative",
452
+ "neg_type": "not_found",
453
+ },
454
+ )
455
+ )
456
+
457
+ return cases
458
+
459
+ def _parse_parameters(
460
+ self,
461
+ parameters: List[Dict[str, Any]],
462
+ ) -> Tuple[Dict[str, Any], Dict[str, Any], Dict[str, Any]]:
463
+ """Return (path_params, query_params, header_params) example dicts."""
464
+ path_params: Dict[str, Any] = {}
465
+ query_params: Dict[str, Any] = {}
466
+ header_params: Dict[str, Any] = {}
467
+
468
+ for param in parameters:
469
+ if "$ref" in param:
470
+ param = _resolve_ref(self.spec, param["$ref"])
471
+ name = param.get("name", "")
472
+ location = param.get("in", "")
473
+ schema = param.get("schema", {}) or {}
474
+ example = param.get("example") or _schema_example(self.spec, schema)
475
+
476
+ if location == "path":
477
+ path_params[name] = example if example is not None else "1"
478
+ elif location == "query":
479
+ query_params[name] = example if example is not None else ""
480
+ elif location == "header":
481
+ header_params[name] = str(example) if example is not None else ""
482
+
483
+ return path_params, query_params, header_params
484
+
485
+ def _extract_body_schema(
486
+ self,
487
+ operation: Dict[str, Any],
488
+ ) -> Tuple[Optional[Dict[str, Any]], List[str]]:
489
+ """Return (body_schema, required_fields) for the operation's request body."""
490
+ # OpenAPI 3
491
+ request_body = operation.get("requestBody", {})
492
+ if request_body:
493
+ content = request_body.get("content", {})
494
+ for media_type in ("application/json", "application/x-www-form-urlencoded"):
495
+ if media_type in content:
496
+ schema = content[media_type].get("schema", {})
497
+ if "$ref" in schema:
498
+ schema = _resolve_ref(self.spec, schema["$ref"])
499
+ required = schema.get("required", [])
500
+ return schema, list(required)
501
+
502
+ # Swagger 2 body parameter
503
+ for param in operation.get("parameters", []):
504
+ if "$ref" in param:
505
+ param = _resolve_ref(self.spec, param["$ref"])
506
+ if param.get("in") == "body":
507
+ schema = param.get("schema", {})
508
+ if "$ref" in schema:
509
+ schema = _resolve_ref(self.spec, schema["$ref"])
510
+ required = schema.get("required", [])
511
+ return schema, list(required)
512
+
513
+ return None, []