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,428 @@
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
+ """FunctionalTestOrchestrator – high-level lifecycle manager for browser-based
7
+ functional testing.
8
+
9
+ Analogous to :class:`~mannf.core.nat_orchestrator.NATOrchestrator` but
10
+ purpose-built for Playwright-driven UI tests:
11
+
12
+ * Spins up N :class:`~mannf.core.agents.browser_executor_agent.BrowserExecutorAgent`
13
+ instances and one :class:`~mannf.core.agents.browser_coordinator_agent.BrowserCoordinatorAgent`.
14
+ * Dispatches a list of ``{url, steps}`` task dicts through the coordinator.
15
+ * Monitors ``INTERACTION_RESULT`` messages for completion.
16
+ * Returns a structured report with pass/fail counts, per-task outcomes,
17
+ timing, screenshots and DOM snapshots.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import asyncio
23
+ import logging
24
+ import time
25
+ from typing import Any
26
+
27
+ from mannf.core.agents.browser_coordinator_agent import BrowserCoordinatorAgent
28
+ from mannf.core.agents.browser_executor_agent import BrowserExecutorAgent
29
+ from mannf.core.agents.visual_regression_agent import VisualRegressionAgent
30
+ from mannf.core.agents.accessibility_scanner_agent import AccessibilityScannerAgent
31
+ from mannf.core.agents.performance_testing_agent import PerformanceTestingAgent
32
+ from mannf.core.messaging.bus import MessageBus
33
+ from mannf.core.messaging.messages import Message, MessageType
34
+ from mannf.core.reporting.unified_report import UnifiedReportGenerator
35
+
36
+ logger = logging.getLogger(__name__)
37
+
38
+
39
+ class FunctionalTestOrchestrator:
40
+ """Manages the full lifecycle for browser-based functional testing.
41
+
42
+ Parameters
43
+ ----------
44
+ num_browser_agents:
45
+ Number of :class:`BrowserExecutorAgent` instances to create.
46
+ browser_type:
47
+ Playwright browser family: ``"chromium"``, ``"firefox"``, or
48
+ ``"webkit"``.
49
+ headless:
50
+ Run browsers in headless mode (no visible window).
51
+ max_tasks:
52
+ Maximum number of functional tests to execute before stopping.
53
+ bid_timeout_s:
54
+ ECNP bid collection window (seconds).
55
+ enable_visual_regression:
56
+ When ``True``, create a :class:`VisualRegressionAgent` and include
57
+ visual regression results in the :meth:`report` output.
58
+ baseline_dir:
59
+ Directory to store baseline PNG screenshots (used when
60
+ *enable_visual_regression* is ``True``).
61
+ diff_threshold:
62
+ Maximum allowed diff percentage before a visual regression is flagged.
63
+ update_baselines:
64
+ When ``True``, always save new screenshots as baselines instead of
65
+ comparing them.
66
+ enable_accessibility_scan:
67
+ When ``True``, create an :class:`AccessibilityScannerAgent` and include
68
+ accessibility results in the :meth:`report` output.
69
+ min_compliance_score:
70
+ Minimum compliance percentage (0–100) used by
71
+ :class:`AccessibilityScannerAgent` when *enable_accessibility_scan*
72
+ is ``True``.
73
+ enable_performance_testing:
74
+ When ``True``, create a :class:`PerformanceTestingAgent` and include
75
+ performance results in the :meth:`report` output.
76
+ min_performance_score:
77
+ Minimum performance score (0–100) used by
78
+ :class:`PerformanceTestingAgent` when *enable_performance_testing*
79
+ is ``True``.
80
+ custom_perf_thresholds:
81
+ Optional threshold overrides for :class:`PerformanceTestingAgent`.
82
+ """
83
+
84
+ def __init__(
85
+ self,
86
+ num_browser_agents: int = 2,
87
+ browser_type: str = "chromium",
88
+ headless: bool = True,
89
+ max_tasks: int = 50,
90
+ bid_timeout_s: float = 0.05,
91
+ enable_visual_regression: bool = False,
92
+ baseline_dir: str = ".nat/visual_baselines",
93
+ diff_threshold: float = 0.1,
94
+ update_baselines: bool = False,
95
+ enable_accessibility_scan: bool = False,
96
+ min_compliance_score: float = 80.0,
97
+ enable_performance_testing: bool = False,
98
+ min_performance_score: float = 70.0,
99
+ custom_perf_thresholds: dict[str, tuple[float, float, str]] | None = None,
100
+ llm_provider: Any | None = None,
101
+ root_cause_quota: int = -1,
102
+ ) -> None:
103
+ self._num_browser_agents = num_browser_agents
104
+ self._browser_type = browser_type
105
+ self._headless = headless
106
+ self._max_tasks = max_tasks
107
+ self._enable_visual_regression = enable_visual_regression
108
+ self._enable_accessibility_scan = enable_accessibility_scan
109
+ self._enable_performance_testing = enable_performance_testing
110
+ self._llm_provider = llm_provider
111
+ self._root_cause_quota = root_cause_quota
112
+
113
+ self._bus = MessageBus()
114
+
115
+ # Browser executor agents
116
+ self._browser_agents: list[BrowserExecutorAgent] = [
117
+ BrowserExecutorAgent(
118
+ agent_id=f"browser-executor-{i}",
119
+ bus=self._bus,
120
+ service_names=["browser"],
121
+ browser_type=browser_type,
122
+ headless=headless,
123
+ )
124
+ for i in range(num_browser_agents)
125
+ ]
126
+
127
+ # Browser coordinator
128
+ self._coordinator = BrowserCoordinatorAgent(
129
+ agent_id="browser-coordinator-0",
130
+ bus=self._bus,
131
+ service_names=["browser"],
132
+ bid_timeout_s=bid_timeout_s,
133
+ )
134
+
135
+ # Optional visual regression agent
136
+ self._visual_agent: VisualRegressionAgent | None = None
137
+ if enable_visual_regression:
138
+ self._visual_agent = VisualRegressionAgent(
139
+ agent_id="visual-regression-0",
140
+ bus=self._bus,
141
+ service_names=["browser"],
142
+ baseline_dir=baseline_dir,
143
+ diff_threshold=diff_threshold,
144
+ update_baselines=update_baselines,
145
+ )
146
+
147
+ # Optional accessibility scanner agent
148
+ self._accessibility_agent: AccessibilityScannerAgent | None = None
149
+ if enable_accessibility_scan:
150
+ self._accessibility_agent = AccessibilityScannerAgent(
151
+ agent_id="accessibility-scanner-0",
152
+ bus=self._bus,
153
+ service_names=["browser"],
154
+ min_compliance_score=min_compliance_score,
155
+ )
156
+
157
+ # Optional performance testing agent
158
+ self._performance_agent: PerformanceTestingAgent | None = None
159
+ if enable_performance_testing:
160
+ self._performance_agent = PerformanceTestingAgent(
161
+ agent_id="performance-testing-0",
162
+ bus=self._bus,
163
+ service_names=["browser"],
164
+ custom_thresholds=custom_perf_thresholds,
165
+ min_performance_score=min_performance_score,
166
+ )
167
+
168
+ # Subscribe orchestrator to INTERACTION_RESULT for completion tracking
169
+ subscribed_types: set[MessageType] = {MessageType.INTERACTION_RESULT}
170
+ if enable_accessibility_scan:
171
+ subscribed_types.add(MessageType.ACCESSIBILITY_SCAN_RESULT)
172
+
173
+ self._result_queue: asyncio.Queue[Message] = self._bus.subscribe(
174
+ "functional-orchestrator",
175
+ subscribed_types,
176
+ )
177
+
178
+ self._completed = 0
179
+ self._start_time: float | None = None
180
+
181
+ # ------------------------------------------------------------------
182
+ # Main entry point
183
+ # ------------------------------------------------------------------
184
+
185
+ async def run(self, tasks: list[dict[str, Any]]) -> dict[str, Any]:
186
+ """Dispatch browser tasks and wait for all to complete.
187
+
188
+ Parameters
189
+ ----------
190
+ tasks:
191
+ List of task dicts, each with the shape::
192
+
193
+ {
194
+ "url": "https://example.com",
195
+ "steps": [
196
+ {"action": "fill", "selector": "#email", "value": "..."},
197
+ {"action": "click", "selector": "#submit"},
198
+ ],
199
+ }
200
+
201
+ Returns
202
+ -------
203
+ dict
204
+ Summary with ``passed``, ``failed``, ``total``, ``tasks``,
205
+ ``elapsed_seconds``.
206
+ """
207
+ num_tasks = min(len(tasks), self._max_tasks)
208
+ self._start_time = time.monotonic()
209
+
210
+ all_agents = self._browser_agents + [self._coordinator] # type: ignore[list-item]
211
+ if self._visual_agent is not None:
212
+ all_agents = all_agents + [self._visual_agent] # type: ignore[list-item]
213
+ if self._accessibility_agent is not None:
214
+ all_agents = all_agents + [self._accessibility_agent] # type: ignore[list-item]
215
+ if self._performance_agent is not None:
216
+ all_agents = all_agents + [self._performance_agent] # type: ignore[list-item]
217
+
218
+ # Start agent run loops (each calls start() internally)
219
+ agent_tasks = [asyncio.create_task(a.run()) for a in all_agents]
220
+
221
+ # Yield so all run() tasks execute their start() and register subscriptions
222
+ await asyncio.sleep(0)
223
+
224
+ # Start browsers on all executor agents
225
+ for browser_agent in self._browser_agents:
226
+ await browser_agent.start_browser()
227
+
228
+ # Allow subscriptions to propagate
229
+ await asyncio.sleep(0)
230
+
231
+ try:
232
+ # Dispatch tasks through the coordinator
233
+ dispatched_ids: list[str] = []
234
+ for task in tasks[:num_tasks]:
235
+ task_id = await self._coordinator.dispatch_browser_task(
236
+ url=task["url"],
237
+ steps=task.get("steps", []),
238
+ )
239
+ dispatched_ids.append(task_id)
240
+
241
+ # Wait for all dispatched tasks to produce results
242
+ await self._monitor_completions(num_tasks)
243
+
244
+ # Yield briefly so coordinator can drain its inbox (it processes
245
+ # INTERACTION_RESULT / DOM_SNAPSHOT / SCREENSHOT_CAPTURED that
246
+ # were queued while the orchestrator was handling completions)
247
+ await asyncio.sleep(0.05)
248
+
249
+ finally:
250
+ # Stop browsers
251
+ for browser_agent in self._browser_agents:
252
+ await browser_agent.stop_browser()
253
+
254
+ # Stop all agents
255
+ for agent in all_agents:
256
+ agent._running = False
257
+ await asyncio.gather(*agent_tasks, return_exceptions=True)
258
+
259
+ # Phase 10.3: best-effort root cause analysis for failed tasks
260
+ await self._schedule_root_cause_analysis()
261
+
262
+ return self.report()
263
+
264
+ # ------------------------------------------------------------------
265
+ # Completion monitoring
266
+ # ------------------------------------------------------------------
267
+
268
+ async def _monitor_completions(self, expected: int) -> None:
269
+ """Read INTERACTION_RESULT messages until *expected* tasks complete."""
270
+ while self._completed < expected:
271
+ try:
272
+ await asyncio.wait_for(self._result_queue.get(), timeout=10.0)
273
+ self._completed += 1
274
+ except asyncio.TimeoutError:
275
+ logger.warning(
276
+ "FunctionalTestOrchestrator: no result after 10s "
277
+ "(completed=%d / expected=%d)",
278
+ self._completed, expected,
279
+ )
280
+ break
281
+
282
+ # ------------------------------------------------------------------
283
+ # Report
284
+ # ------------------------------------------------------------------
285
+
286
+ def report(self) -> dict[str, Any]:
287
+ """Return structured results for the completed functional test run.
288
+
289
+ Returns
290
+ -------
291
+ dict
292
+ ``{passed, failed, total, tasks, elapsed_seconds,
293
+ dom_snapshots, screenshots, agent_count}``
294
+ When visual regression is enabled, a ``visual_regression`` key is
295
+ also included.
296
+ When accessibility scanning is enabled, an ``accessibility`` key is
297
+ also included.
298
+ """
299
+ elapsed = time.monotonic() - (self._start_time or 0.0)
300
+ functional = self._coordinator.get_functional_results()
301
+
302
+ result: dict[str, Any] = {
303
+ "passed": functional["passed"],
304
+ "failed": functional["failed"],
305
+ "total": functional["total"],
306
+ "tasks": functional["tasks"],
307
+ "dom_snapshots": functional["dom_snapshots"],
308
+ "screenshots": functional["screenshots"],
309
+ "elapsed_seconds": round(elapsed, 2),
310
+ "agent_count": len(self._browser_agents) + 1,
311
+ }
312
+
313
+ if self._visual_agent is not None:
314
+ result["visual_regression"] = self._visual_agent.get_visual_report()
315
+
316
+ if self._accessibility_agent is not None:
317
+ result["accessibility"] = self._accessibility_agent.get_accessibility_report()
318
+
319
+ if self._performance_agent is not None:
320
+ result["performance"] = self._performance_agent.get_performance_report()
321
+
322
+ # Phase 10.3: include any previously computed root cause suggestions
323
+ if hasattr(self, "_root_cause_suggestions"):
324
+ result["root_cause_analysis"] = self._root_cause_suggestions
325
+
326
+ return result
327
+
328
+ # ------------------------------------------------------------------
329
+ # Properties
330
+ # ------------------------------------------------------------------
331
+
332
+ @property
333
+ def coordinator(self) -> BrowserCoordinatorAgent:
334
+ return self._coordinator
335
+
336
+ @property
337
+ def browser_agents(self) -> list[BrowserExecutorAgent]:
338
+ return list(self._browser_agents)
339
+
340
+ @property
341
+ def visual_agent(self) -> VisualRegressionAgent | None:
342
+ """The :class:`VisualRegressionAgent` instance, or ``None`` if not enabled."""
343
+ return self._visual_agent
344
+
345
+ @property
346
+ def accessibility_agent(self) -> AccessibilityScannerAgent | None:
347
+ """The :class:`AccessibilityScannerAgent` instance, or ``None`` if not enabled."""
348
+ return self._accessibility_agent
349
+
350
+ @property
351
+ def performance_agent(self) -> PerformanceTestingAgent | None:
352
+ """The :class:`PerformanceTestingAgent` instance, or ``None`` if not enabled."""
353
+ return self._performance_agent
354
+
355
+ # ------------------------------------------------------------------
356
+ # Unified report generation
357
+ # ------------------------------------------------------------------
358
+
359
+ async def _schedule_root_cause_analysis(self) -> None:
360
+ """Best-effort root cause analysis for failed functional test tasks."""
361
+ if self._llm_provider is None:
362
+ return
363
+
364
+ try:
365
+ from mannf.product.llm.root_cause_service import ScanRootCauseAnalyzer # noqa: PLC0415
366
+
367
+ functional = self._coordinator.get_functional_results()
368
+ failed_tasks = [
369
+ t for t in functional.get("tasks", [])
370
+ if not t.get("passed", True)
371
+ ]
372
+ if not failed_tasks:
373
+ self._root_cause_suggestions: list[dict[str, Any]] = []
374
+ return
375
+
376
+ failures = [
377
+ {
378
+ "id": t.get("task_id", ""),
379
+ "type": "functional",
380
+ "url": t.get("url", ""),
381
+ "error": t.get("error") or t.get("outcome", "Task failed"),
382
+ "severity": "medium",
383
+ }
384
+ for t in failed_tasks
385
+ ]
386
+
387
+ analyzer = ScanRootCauseAnalyzer(
388
+ llm_provider=self._llm_provider,
389
+ quota=self._root_cause_quota,
390
+ )
391
+ suggestions = await analyzer.analyze_failures(failures, {"scan_type": "functional"})
392
+ self._root_cause_suggestions = [s.model_dump() for s in suggestions]
393
+ logger.info(
394
+ "FunctionalTestOrchestrator: root cause analysis produced %d suggestion(s)",
395
+ len(suggestions),
396
+ )
397
+ except Exception as exc: # noqa: BLE001
398
+ logger.debug("FunctionalTestOrchestrator: root cause analysis skipped — %s", exc)
399
+ self._root_cause_suggestions = []
400
+
401
+ def generate_report(
402
+ self,
403
+ output_dir: str = "nat_reports",
404
+ formats: list[str] | None = None,
405
+ ) -> list[str]:
406
+ """Generate a unified HTML/JSON report from the last completed run.
407
+
408
+ This method calls :meth:`report` internally to obtain the latest
409
+ results, then passes them to
410
+ :class:`~mannf.core.reporting.unified_report.UnifiedReportGenerator`
411
+ to produce the output files.
412
+
413
+ Parameters
414
+ ----------
415
+ output_dir:
416
+ Directory to write report files into. Created if it does not
417
+ exist. Defaults to ``"nat_reports"``.
418
+ formats:
419
+ List of ``"html"`` and/or ``"json"``. Defaults to both.
420
+
421
+ Returns
422
+ -------
423
+ list[str]
424
+ Absolute paths of the files that were written.
425
+ """
426
+ report_dict = self.report()
427
+ generator = UnifiedReportGenerator()
428
+ return generator.save_report(report_dict, output_dir=output_dir, formats=formats)
@@ -0,0 +1,11 @@
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
+ """Messaging sub-package for the Multi-Agent Neural Network Framework."""
7
+
8
+ from mannf.core.messaging.messages import Message, MessageType
9
+ from mannf.core.messaging.bus import MessageBus
10
+
11
+ __all__ = ["Message", "MessageType", "MessageBus"]
@@ -0,0 +1,113 @@
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
+ """Async publish-subscribe message bus for inter-agent communication.
7
+
8
+ Agents subscribe to one or more :class:`MessageType` values and receive
9
+ matching :class:`Message` objects through an ``asyncio.Queue``. The bus is
10
+ the *only* shared state between agents, keeping coupling low.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import asyncio
16
+ import logging
17
+ from typing import Dict, List, Optional, Set
18
+
19
+ from mannf.core.messaging.messages import Message, MessageType
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ class MessageBus:
25
+ """Lightweight, in-process publish/subscribe broker.
26
+
27
+ Usage::
28
+
29
+ bus = MessageBus()
30
+ queue = bus.subscribe("agent-1", {MessageType.TEST_RESULT})
31
+ await bus.publish(Message(MessageType.TEST_RESULT, sender_id="agent-2", payload=result))
32
+ msg = await queue.get()
33
+ """
34
+
35
+ def __init__(self) -> None:
36
+ # subscriber_id -> (set of subscribed types, asyncio Queue)
37
+ self._subscribers: Dict[str, tuple[Set[MessageType], asyncio.Queue[Message]]] = {}
38
+ self._lock = asyncio.Lock()
39
+
40
+ # ------------------------------------------------------------------
41
+ # Subscription management
42
+ # ------------------------------------------------------------------
43
+
44
+ def subscribe(
45
+ self,
46
+ subscriber_id: str,
47
+ message_types: Set[MessageType],
48
+ maxsize: int = 4096,
49
+ ) -> asyncio.Queue[Message]:
50
+ """Register *subscriber_id* to receive messages of *message_types*.
51
+
52
+ Returns an ``asyncio.Queue`` the subscriber should read from.
53
+ """
54
+ q: asyncio.Queue[Message] = asyncio.Queue(maxsize=maxsize)
55
+ self._subscribers[subscriber_id] = (message_types, q)
56
+ logger.debug("Subscribed %s to %s", subscriber_id, message_types)
57
+ return q
58
+
59
+ def unsubscribe(self, subscriber_id: str) -> None:
60
+ self._subscribers.pop(subscriber_id, None)
61
+ logger.debug("Unsubscribed %s", subscriber_id)
62
+
63
+ # ------------------------------------------------------------------
64
+ # Publishing
65
+ # ------------------------------------------------------------------
66
+
67
+ async def publish(self, message: Message) -> int:
68
+ """Deliver *message* to all matching subscribers.
69
+
70
+ Returns the number of queues the message was delivered to.
71
+ """
72
+ delivered = 0
73
+ for sub_id, (types, queue) in list(self._subscribers.items()):
74
+ if message.type in types:
75
+ try:
76
+ queue.put_nowait(message)
77
+ delivered += 1
78
+ except asyncio.QueueFull:
79
+ logger.warning(
80
+ "Queue full for subscriber %s; dropping message %s",
81
+ sub_id,
82
+ message.id,
83
+ )
84
+ logger.debug(
85
+ "Published %s from %s to %d subscriber(s)",
86
+ message.type.name,
87
+ message.sender_id,
88
+ delivered,
89
+ )
90
+ return delivered
91
+
92
+ def publish_sync(self, message: Message) -> int:
93
+ """Synchronous variant (does not await); safe from non-async contexts."""
94
+ delivered = 0
95
+ for sub_id, (types, queue) in list(self._subscribers.items()):
96
+ if message.type in types:
97
+ try:
98
+ queue.put_nowait(message)
99
+ delivered += 1
100
+ except asyncio.QueueFull:
101
+ logger.warning(
102
+ "Queue full for subscriber %s; dropping message %s",
103
+ sub_id,
104
+ message.id,
105
+ )
106
+ return delivered
107
+
108
+ @property
109
+ def subscriber_count(self) -> int:
110
+ return len(self._subscribers)
111
+
112
+ def subscriber_ids(self) -> List[str]:
113
+ return list(self._subscribers.keys())
@@ -0,0 +1,89 @@
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
+ """Message types used by agents to communicate over the message bus."""
7
+
8
+ from __future__ import annotations
9
+
10
+ import uuid
11
+ from dataclasses import dataclass, field
12
+ from enum import Enum, auto
13
+ from typing import Any, Optional
14
+
15
+
16
+ class MessageType(Enum):
17
+ # Lifecycle
18
+ AGENT_STARTED = auto()
19
+ AGENT_STOPPED = auto()
20
+
21
+ # Test coordination (basic orchestrator)
22
+ TEST_REQUEST = auto() # ask TestAgent to generate a test case
23
+ TEST_CASE = auto() # a generated TestCase ready for execution
24
+ TEST_RESULT = auto() # result after execution
25
+ TEST_VERDICT = auto() # OracleAgent's pass/fail decision
26
+
27
+ # Monitoring
28
+ METRICS_UPDATE = auto() # MonitorAgent reports system metrics
29
+ ANOMALY_DETECTED = auto() # MonitorAgent detected an anomaly
30
+
31
+ # Adaptation
32
+ STRATEGY_UPDATE = auto() # AdaptiveController broadcasts new weights/strategy
33
+ FEEDBACK = auto() # generic feedback signal for NN training
34
+
35
+ # ── NAT / ECNP messages (Extended Contract Net Protocol) ────────────
36
+ TASK_ANNOUNCEMENT = auto() # CoordinatorAgent broadcasts a testing task
37
+ TASK_BID = auto() # ExecutorAgent submits a bid for a task
38
+ CONTRACT_AWARD = auto() # CoordinatorAgent awards the contract to a bidder
39
+ CONTRACT_RESULT = auto() # ExecutorAgent reports execution outcome to Coordinator
40
+
41
+ # BDI belief propagation
42
+ BELIEF_UPDATE = auto() # AnalyzerAgent broadcasts updated fault-likelihood beliefs
43
+ BELIEF_REQUEST = auto() # Any agent requests the current belief snapshot
44
+
45
+ # ── Browser / Functional Testing messages ────────────────────────────
46
+ INTERACTION_REQUEST = auto() # Request a browser interaction (click, fill, navigate)
47
+ INTERACTION_RESULT = auto() # Result of a browser interaction
48
+ DOM_SNAPSHOT = auto() # DOM snapshot captured by a browser agent
49
+ SCREENSHOT_CAPTURED = auto() # Screenshot taken by a browser agent
50
+
51
+ # ── Visual Regression messages ────────────────────────────────────────
52
+ VISUAL_DIFF_DETECTED = auto() # Screenshot differs from baseline beyond threshold
53
+ VISUAL_BASELINE_SAVED = auto() # New baseline screenshot stored
54
+
55
+ # ── Accessibility Scanning messages ──────────────────────────────────
56
+ ACCESSIBILITY_SCAN_REQUEST = auto() # Request an accessibility scan on a page
57
+ ACCESSIBILITY_SCAN_RESULT = auto() # Result of an accessibility scan (violations found)
58
+
59
+ # ── Performance Testing messages ──────────────────────────────────────
60
+ PERFORMANCE_METRICS_CAPTURED = auto() # Performance metrics collected from a page
61
+ PERFORMANCE_THRESHOLD_VIOLATED = auto() # A performance metric exceeded its threshold
62
+
63
+ # ── Web Crawler messages ──────────────────────────────────────────────
64
+ CRAWL_PROGRESS = auto() # WebCrawlerAgent reports discovery progress (live updates)
65
+ CRAWL_RESULT = auto() # WebCrawlerAgent publishes completed discovery model
66
+
67
+ # ── Autonomous Test Loop messages ─────────────────────────────────────
68
+ AUTONOMOUS_ITERATION = auto() # AutonomousTestLoopAgent completed one iteration
69
+ AUTONOMOUS_COMPLETE = auto() # AutonomousTestLoopAgent finished the full run
70
+
71
+
72
+ @dataclass
73
+ class Message:
74
+ """Envelope that carries a payload between agents on the message bus."""
75
+
76
+ type: MessageType
77
+ sender_id: str
78
+ payload: Any = None
79
+ id: str = field(default_factory=lambda: str(uuid.uuid4()))
80
+ correlation_id: Optional[str] = None # links replies to requests
81
+
82
+ def reply(self, msg_type: MessageType, payload: Any, sender_id: str) -> "Message":
83
+ """Convenience constructor for a reply to this message."""
84
+ return Message(
85
+ type=msg_type,
86
+ sender_id=sender_id,
87
+ payload=payload,
88
+ correlation_id=self.id,
89
+ )