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,509 @@
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
+ """Adaptive Test Generation Controller.
7
+
8
+ Extends the base ε-greedy :class:`~mannf.core.testing.adaptive_controller.AdaptiveController`
9
+ with smarter test-generation strategies guided by BDI belief signals:
10
+
11
+ * **Boundary value analysis** — focus on inputs at boundaries where the neural
12
+ network predicts highest fault likelihood.
13
+ * **Mutation-based fuzzing** — mutate existing test cases more aggressively
14
+ for endpoints the agents believe are fault-prone.
15
+ * **Sequence testing** — identify and test multi-step API flows (e.g.
16
+ create → read → update → delete).
17
+
18
+ Integration with :class:`~mannf.core.prioritization.belief_prioritizer.BeliefPrioritizer`
19
+ allocates test-generation effort proportionally to endpoint risk.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import copy
25
+ import json
26
+ import logging
27
+ import random
28
+ from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple
29
+
30
+ import numpy as np
31
+
32
+ from mannf.core.agents.belief_state import BeliefState
33
+ from mannf.core.neural import NeuralNetwork
34
+ from mannf.core.prioritization.belief_prioritizer import BeliefPrioritizer
35
+ from mannf.core.testing.models import TestCase, TestResult
36
+
37
+ if TYPE_CHECKING:
38
+ from mannf.llm.base import LLMProvider
39
+
40
+ logger = logging.getLogger(__name__)
41
+
42
+ _FEATURE_DIM = 8
43
+
44
+
45
+ class AdaptiveController:
46
+ """Adaptive test-generation controller with BDI-guided strategies.
47
+
48
+ Parameters
49
+ ----------
50
+ initial_epsilon:
51
+ Initial exploration probability (0–1).
52
+ min_epsilon:
53
+ Minimum exploration probability after decay.
54
+ epsilon_decay:
55
+ Multiplicative decay applied after each :meth:`update` call.
56
+ learning_rate:
57
+ Step size for the value-network update.
58
+ prioritizer:
59
+ Optional :class:`BeliefPrioritizer` to allocate generation effort.
60
+ mutation_intensity:
61
+ Base mutation intensity (0–1). Higher values apply larger random
62
+ perturbations to mutated inputs.
63
+ """
64
+
65
+ # Common CRUD sequence patterns used by the sequence tester
66
+ _CRUD_SEQUENCE: List[str] = ["create", "read", "update", "delete"]
67
+ _CRUD_METHODS: Dict[str, str] = {
68
+ "create": "POST",
69
+ "read": "GET",
70
+ "update": "PUT",
71
+ "delete": "DELETE",
72
+ }
73
+
74
+ def __init__(
75
+ self,
76
+ initial_epsilon: float = 0.9,
77
+ min_epsilon: float = 0.05,
78
+ epsilon_decay: float = 0.995,
79
+ learning_rate: float = 0.005,
80
+ prioritizer: Optional[BeliefPrioritizer] = None,
81
+ mutation_intensity: float = 0.3,
82
+ llm_provider: Optional["LLMProvider"] = None,
83
+ ) -> None:
84
+ self.epsilon = initial_epsilon
85
+ self.min_epsilon = min_epsilon
86
+ self.epsilon_decay = epsilon_decay
87
+ self.mutation_intensity = mutation_intensity
88
+ self.prioritizer = prioritizer
89
+ self.llm_provider = llm_provider
90
+
91
+ self.value_net = NeuralNetwork(
92
+ layer_sizes=[_FEATURE_DIM, 16, 8, 1],
93
+ hidden_activation="relu",
94
+ learning_rate=learning_rate,
95
+ )
96
+
97
+ self._step_count = 0
98
+ self._cumulative_reward = 0.0
99
+
100
+ # Sequence memory: stores observed (endpoint, method) pairs to infer flows
101
+ self._sequence_memory: List[Tuple[str, str]] = []
102
+ self._max_sequence_memory = 200
103
+
104
+ # ------------------------------------------------------------------
105
+ # Selection (ε-greedy)
106
+ # ------------------------------------------------------------------
107
+
108
+ def select(self, candidates: List[TestCase]) -> Optional[TestCase]:
109
+ """Pick the next test case using ε-greedy selection.
110
+
111
+ If a *prioritizer* is set, candidates are pre-ordered by risk before
112
+ the greedy selection is applied.
113
+ """
114
+ if not candidates:
115
+ return None
116
+
117
+ ordered = candidates
118
+ if self.prioritizer is not None:
119
+ ordered = self.prioritizer.prioritize(candidates)
120
+
121
+ if np.random.random() < self.epsilon:
122
+ choice = ordered[int(np.random.randint(len(ordered)))]
123
+ logger.debug("AdaptiveController: exploring → %s", choice.id[:8])
124
+ else:
125
+ scores = [
126
+ float(self.value_net.predict(c.feature_vector(_FEATURE_DIM))[0])
127
+ for c in ordered
128
+ ]
129
+ best_idx = int(np.argmax(scores))
130
+ choice = ordered[best_idx]
131
+ logger.debug(
132
+ "AdaptiveController: exploiting → %s (score=%.4f)",
133
+ choice.id[:8],
134
+ scores[best_idx],
135
+ )
136
+ return choice
137
+
138
+ # ------------------------------------------------------------------
139
+ # Learning
140
+ # ------------------------------------------------------------------
141
+
142
+ def update(self, test_case: TestCase, result: TestResult) -> float:
143
+ """Update the value network with the observed reward signal.
144
+
145
+ Returns the MSE training loss for the current step.
146
+ """
147
+ reward = result.reward_signal()
148
+ self._cumulative_reward += reward
149
+
150
+ x = test_case.feature_vector(_FEATURE_DIM)
151
+ target = np.array([reward])
152
+ loss = self.value_net.train_step(x, target)
153
+
154
+ # Record for sequence inference
155
+ method = test_case.metadata.get("method", "GET")
156
+ self._record_sequence(test_case.target, method)
157
+
158
+ # Update prioritizer if available
159
+ if self.prioritizer is not None:
160
+ self.prioritizer.record_result(result, test_case.target)
161
+
162
+ self.epsilon = max(self.min_epsilon, self.epsilon * self.epsilon_decay)
163
+ self._step_count += 1
164
+
165
+ logger.debug(
166
+ "AdaptiveController update: reward=%.3f loss=%.6f ε=%.4f",
167
+ reward,
168
+ loss,
169
+ self.epsilon,
170
+ )
171
+ return loss
172
+
173
+ # ------------------------------------------------------------------
174
+ # Boundary value analysis
175
+ # ------------------------------------------------------------------
176
+
177
+ def generate_boundary_variants(
178
+ self,
179
+ base_case: TestCase,
180
+ belief_states: Optional[List[BeliefState]] = None,
181
+ ) -> List[TestCase]:
182
+ """Generate boundary-value variants of *base_case*.
183
+
184
+ For each numeric input parameter, creates test cases at:
185
+ * value = 0
186
+ * value = -1 (underflow)
187
+ * value = maximum observed + 1 (overflow)
188
+ * value = ``None`` / missing (null injection)
189
+
190
+ The intensity of boundary analysis is scaled by the endpoint's belief
191
+ signal — high-risk endpoints produce more variants.
192
+ """
193
+ risk_multiplier = self._endpoint_risk(base_case.target, belief_states)
194
+ # Number of variants scales with risk (1–4)
195
+ n_variants = max(1, round(risk_multiplier * 4))
196
+
197
+ variants: List[TestCase] = []
198
+ numeric_keys = [
199
+ k for k, v in base_case.inputs.items() if isinstance(v, (int, float))
200
+ ]
201
+
202
+ boundary_values: List[Any] = [0, -1, 2**31 - 1, None]
203
+
204
+ for key in numeric_keys:
205
+ original_val = base_case.inputs[key]
206
+ for bv in boundary_values[: n_variants]:
207
+ new_inputs = dict(base_case.inputs)
208
+ new_inputs[key] = bv
209
+ variant = TestCase(
210
+ target=base_case.target,
211
+ inputs=new_inputs,
212
+ expected_behavior=(
213
+ f"Boundary value {bv!r} for '{key}' — expect graceful handling"
214
+ ),
215
+ priority=min(1.0, base_case.priority + 0.1 * risk_multiplier),
216
+ metadata={
217
+ **base_case.metadata,
218
+ "generation_strategy": "boundary_value_analysis",
219
+ "boundary_key": key,
220
+ "boundary_value": str(bv),
221
+ "source_case_id": base_case.id,
222
+ },
223
+ )
224
+ variants.append(variant)
225
+
226
+ if not variants:
227
+ # No numeric params: inject empty input as boundary
228
+ variant = TestCase(
229
+ target=base_case.target,
230
+ inputs={},
231
+ expected_behavior="Empty inputs — expect graceful handling",
232
+ priority=base_case.priority,
233
+ metadata={
234
+ **base_case.metadata,
235
+ "generation_strategy": "boundary_value_analysis",
236
+ "boundary_key": "__empty__",
237
+ "source_case_id": base_case.id,
238
+ },
239
+ )
240
+ variants.append(variant)
241
+
242
+ return variants
243
+
244
+ # ------------------------------------------------------------------
245
+ # Mutation-based fuzzing
246
+ # ------------------------------------------------------------------
247
+
248
+ def mutate(
249
+ self,
250
+ base_case: TestCase,
251
+ belief_states: Optional[List[BeliefState]] = None,
252
+ n_mutations: int = 3,
253
+ ) -> List[TestCase]:
254
+ """Generate mutated variants of *base_case*.
255
+
256
+ Mutation intensity scales with the endpoint's BDI risk signal:
257
+ high-risk endpoints receive more aggressive perturbations.
258
+
259
+ Mutation operations (applied randomly):
260
+ * Numeric values: add Gaussian noise scaled by mutation intensity.
261
+ * String values: random character substitution or length change.
262
+ * Boolean values: flip.
263
+ * Keys: randomly drop one key (missing-parameter test).
264
+ """
265
+ risk = self._endpoint_risk(base_case.target, belief_states)
266
+ intensity = self.mutation_intensity * (0.5 + risk)
267
+
268
+ mutations: List[TestCase] = []
269
+ for i in range(n_mutations):
270
+ mutated_inputs = self._apply_mutation(base_case.inputs, intensity)
271
+ mutated = TestCase(
272
+ target=base_case.target,
273
+ inputs=mutated_inputs,
274
+ expected_behavior=f"Mutation #{i + 1} of '{base_case.id[:8]}' — fault injection",
275
+ priority=min(1.0, base_case.priority + 0.05 * risk),
276
+ metadata={
277
+ **base_case.metadata,
278
+ "generation_strategy": "mutation_fuzzing",
279
+ "mutation_index": i,
280
+ "mutation_intensity": round(intensity, 3),
281
+ "source_case_id": base_case.id,
282
+ },
283
+ )
284
+ mutations.append(mutated)
285
+ return mutations
286
+
287
+ async def mutate_with_llm(
288
+ self,
289
+ base_case: TestCase,
290
+ belief_states: Optional[List[BeliefState]] = None,
291
+ n_mutations: int = 3,
292
+ ) -> List[TestCase]:
293
+ """Generate mutated variants, adding one LLM-guided smart mutation when available.
294
+
295
+ The LLM mutation is appended to the random mutations produced by
296
+ :meth:`mutate` and tagged with ``generation_strategy = "llm_mutation"``.
297
+ Falls back to :meth:`mutate` if the LLM is unavailable or fails.
298
+ """
299
+ mutations = self.mutate(base_case, belief_states=belief_states, n_mutations=n_mutations)
300
+
301
+ if self.llm_provider is None or not self.llm_provider.is_available():
302
+ return mutations
303
+
304
+ from mannf.llm.prompts import MUTATION_PROMPT # noqa: PLC0415
305
+
306
+ prompt = MUTATION_PROMPT.format(
307
+ n=1,
308
+ target=base_case.target,
309
+ inputs=json.dumps(base_case.inputs, indent=2),
310
+ expected_behavior=base_case.expected_behavior,
311
+ )
312
+ try:
313
+ raw = await self.llm_provider.generate(prompt)
314
+ raw_cases = self.llm_provider._parse_test_cases(raw)
315
+ for raw_case in raw_cases[:1]:
316
+ if not isinstance(raw_case, dict):
317
+ continue
318
+ inputs = raw_case.get("inputs", {})
319
+ if not isinstance(inputs, dict):
320
+ inputs = copy.deepcopy(base_case.inputs)
321
+ mutations.append(
322
+ TestCase(
323
+ target=base_case.target,
324
+ inputs=inputs,
325
+ expected_behavior=raw_case.get(
326
+ "expected_behavior",
327
+ f"LLM mutation of '{base_case.id[:8]}'",
328
+ ),
329
+ priority=min(1.0, base_case.priority + 0.1),
330
+ metadata={
331
+ **base_case.metadata,
332
+ "generation_strategy": "llm_mutation",
333
+ "generator": "llm",
334
+ "source_case_id": base_case.id,
335
+ "mutation_rationale": raw_case.get("mutation_rationale", ""),
336
+ },
337
+ )
338
+ )
339
+ except Exception as exc: # noqa: BLE001
340
+ logger.warning(
341
+ "AdaptiveController: LLM mutation failed for %s: %s",
342
+ base_case.target, exc,
343
+ )
344
+
345
+ return mutations
346
+
347
+ def _apply_mutation(self, inputs: Dict[str, Any], intensity: float) -> Dict[str, Any]:
348
+ mutated = copy.deepcopy(inputs)
349
+ if not mutated:
350
+ return mutated
351
+
352
+ keys = list(mutated.keys())
353
+ op = random.choice(["perturb", "drop", "flip", "null"])
354
+
355
+ if op == "drop" and len(keys) > 1:
356
+ drop_key = random.choice(keys)
357
+ del mutated[drop_key]
358
+ elif op == "null":
359
+ null_key = random.choice(keys)
360
+ mutated[null_key] = None
361
+ elif op == "flip":
362
+ bool_keys = [k for k, v in mutated.items() if isinstance(v, bool)]
363
+ if bool_keys:
364
+ flip_key = random.choice(bool_keys)
365
+ mutated[flip_key] = not mutated[flip_key]
366
+ else: # perturb numerics / strings
367
+ for key in keys:
368
+ val = mutated[key]
369
+ if isinstance(val, (int, float)):
370
+ noise = random.gauss(0, intensity * max(1.0, abs(val)))
371
+ mutated[key] = type(val)(val + noise) if isinstance(val, float) else int(val + noise)
372
+ elif isinstance(val, str):
373
+ if val and random.random() < intensity:
374
+ idx = random.randrange(len(val))
375
+ mutated[key] = val[:idx] + random.choice("abcdefXYZ!@#") + val[idx + 1:]
376
+
377
+ return mutated
378
+
379
+ def generate_sequence_tests(
380
+ self,
381
+ endpoints: List[str],
382
+ base_inputs: Optional[Dict[str, Dict[str, Any]]] = None,
383
+ belief_states: Optional[List[BeliefState]] = None,
384
+ ) -> List[List[TestCase]]:
385
+ """Generate multi-step API flow test sequences.
386
+
387
+ Infers CRUD sequences from the observed endpoint list and optionally
388
+ from the planner agent's sequence memory. Returns a list of test-case
389
+ *sequences*, each representing a complete API flow.
390
+
391
+ Parameters
392
+ ----------
393
+ endpoints:
394
+ Known endpoint targets (e.g. ``["/users", "/users/{id}"]``).
395
+ base_inputs:
396
+ Mapping of endpoint → default inputs. Defaults to empty dicts.
397
+ belief_states:
398
+ Optional belief states to prioritize high-risk sequences.
399
+
400
+ Returns
401
+ -------
402
+ list of list of TestCase
403
+ Each inner list is an ordered sequence of test cases forming a flow.
404
+ """
405
+ base_inputs = base_inputs or {}
406
+ sequences: List[List[TestCase]] = []
407
+
408
+ inferred = self._infer_sequences(endpoints)
409
+ for seq_endpoints in inferred:
410
+ seq = []
411
+ for i, (ep, method) in enumerate(seq_endpoints):
412
+ tc = TestCase(
413
+ target=ep,
414
+ inputs=dict(base_inputs.get(ep, {})),
415
+ expected_behavior=f"Sequence step {i + 1}/{len(seq_endpoints)}: {method} {ep}",
416
+ priority=self._endpoint_risk(ep, belief_states),
417
+ metadata={
418
+ "generation_strategy": "sequence_testing",
419
+ "sequence_step": i,
420
+ "sequence_length": len(seq_endpoints),
421
+ "method": method,
422
+ },
423
+ )
424
+ seq.append(tc)
425
+ if seq:
426
+ sequences.append(seq)
427
+
428
+ return sequences
429
+
430
+ def _infer_sequences(
431
+ self, endpoints: List[str]
432
+ ) -> List[List[Tuple[str, str]]]:
433
+ """Heuristically group endpoints into CRUD-like sequences."""
434
+ # Group endpoints by common resource root
435
+ # e.g. /users, /users/{id} → one group
436
+ groups: Dict[str, List[str]] = {}
437
+ for ep in endpoints:
438
+ root = ep.split("{")[0].rstrip("/")
439
+ if root not in groups:
440
+ groups[root] = []
441
+ groups[root].append(ep)
442
+
443
+ sequences = []
444
+ for root, eps in groups.items():
445
+ # Assign CRUD methods heuristically
446
+ seq: List[Tuple[str, str]] = []
447
+ has_id = any("{" in ep for ep in eps)
448
+ base_ep = min(eps, key=len) # shortest = collection endpoint
449
+ detail_ep = max(eps, key=len) if has_id else base_ep
450
+
451
+ seq.append((base_ep, "POST")) # create
452
+ seq.append((detail_ep, "GET")) # read
453
+ if has_id:
454
+ seq.append((detail_ep, "PUT")) # update
455
+ seq.append((detail_ep, "DELETE")) # delete
456
+
457
+ if len(seq) >= 2:
458
+ sequences.append(seq)
459
+
460
+ # Also add any patterns observed in sequence memory
461
+ if len(self._sequence_memory) >= 3:
462
+ # Extract unique transitions
463
+ mem_seq: List[Tuple[str, str]] = []
464
+ seen: set = set()
465
+ for item in self._sequence_memory[-10:]:
466
+ if item not in seen:
467
+ seen.add(item)
468
+ mem_seq.append(item)
469
+ if len(mem_seq) >= 2:
470
+ sequences.append(mem_seq)
471
+
472
+ return sequences
473
+
474
+ def _record_sequence(self, endpoint: str, method: str) -> None:
475
+ self._sequence_memory.append((endpoint, method))
476
+ if len(self._sequence_memory) > self._max_sequence_memory:
477
+ self._sequence_memory.pop(0)
478
+
479
+ # ------------------------------------------------------------------
480
+ # Helpers
481
+ # ------------------------------------------------------------------
482
+
483
+ def _endpoint_risk(
484
+ self,
485
+ endpoint: str,
486
+ belief_states: Optional[List[BeliefState]],
487
+ ) -> float:
488
+ if not belief_states:
489
+ return 0.5
490
+ signals = [bs.fault_likelihood.get(endpoint, 0.5) for bs in belief_states]
491
+ return sum(signals) / len(signals)
492
+
493
+ # ------------------------------------------------------------------
494
+ # Diagnostics
495
+ # ------------------------------------------------------------------
496
+
497
+ @property
498
+ def step_count(self) -> int:
499
+ return self._step_count
500
+
501
+ @property
502
+ def average_reward(self) -> float:
503
+ if self._step_count == 0:
504
+ return 0.0
505
+ return self._cumulative_reward / self._step_count
506
+
507
+ def score(self, test_case: TestCase) -> float:
508
+ """Return the current predicted value for a test case."""
509
+ return float(self.value_net.predict(test_case.feature_vector(_FEATURE_DIM))[0])