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,342 @@
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
+ """Kafka consumer/producer test adapter.
7
+
8
+ :class:`KafkaAdapter` provides a test adapter for Apache Kafka topics that
9
+ implements the same ``execute()`` / ``health_check()`` interface as
10
+ :class:`~mannf.product.integrations.http_sut.HttpApiSUT`.
11
+
12
+ Supports:
13
+
14
+ * **Producer tests** — publish a message to a topic and verify no errors.
15
+ * **Consumer tests** — subscribe to a topic and verify a message arrives
16
+ within a configurable timeout.
17
+ * **Round-trip tests** — produce a message, then consume from a reply topic
18
+ to verify end-to-end delivery.
19
+
20
+ The adapter uses ``kafka-python`` when available. When not installed, all
21
+ execute() calls return a skip result so NAT continues functioning.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import json
27
+ import logging
28
+ import time
29
+ import uuid
30
+ from typing import Any, Dict, List, Optional
31
+
32
+ from mannf.core.distributed.endpoint import Endpoint, EndpointRegistry
33
+ from mannf.core.distributed.system_under_test import SystemUnderTest
34
+ from mannf.core.testing.models import TestCase, TestResult
35
+
36
+ logger = logging.getLogger(__name__)
37
+
38
+
39
+ def _kafka_available() -> bool:
40
+ """Return True if the ``kafka-python`` package is installed."""
41
+ try:
42
+ import kafka # noqa: F401
43
+ return True
44
+ except ImportError:
45
+ return False
46
+
47
+
48
+ def _bytes_passthrough(value: bytes) -> bytes:
49
+ """Identity serializer for Kafka producers — pass bytes through unchanged."""
50
+ return value
51
+
52
+
53
+ class KafkaAdapter(SystemUnderTest):
54
+ """Execute Kafka topic tests (produce / consume / round-trip).
55
+
56
+ Parameters
57
+ ----------
58
+ bootstrap_servers:
59
+ Kafka broker address(es) (e.g. ``"localhost:9092"`` or a list).
60
+ timeout:
61
+ Per-operation timeout in seconds (default: 10).
62
+ consumer_group:
63
+ Consumer group ID (default: ``"nat-test-group-<uuid>"``)
64
+ to avoid interfering with production consumers.
65
+ producer_config:
66
+ Additional ``kafka-python`` producer configuration overrides.
67
+ consumer_config:
68
+ Additional ``kafka-python`` consumer configuration overrides.
69
+ """
70
+
71
+ def __init__(
72
+ self,
73
+ bootstrap_servers: str | list[str] = "localhost:9092",
74
+ timeout: float = 10.0,
75
+ consumer_group: Optional[str] = None,
76
+ producer_config: Optional[Dict[str, Any]] = None,
77
+ consumer_config: Optional[Dict[str, Any]] = None,
78
+ ) -> None:
79
+ if isinstance(bootstrap_servers, str):
80
+ self.bootstrap_servers = [bootstrap_servers]
81
+ else:
82
+ self.bootstrap_servers = list(bootstrap_servers)
83
+
84
+ self.timeout = timeout
85
+ self.consumer_group = consumer_group or f"nat-test-group-{uuid.uuid4().hex[:8]}"
86
+ self.producer_config: Dict[str, Any] = producer_config or {}
87
+ self.consumer_config: Dict[str, Any] = consumer_config or {}
88
+
89
+ host = self.bootstrap_servers[0].split(":")[0]
90
+ try:
91
+ port = int(self.bootstrap_servers[0].split(":")[1])
92
+ except (IndexError, ValueError):
93
+ port = 9092
94
+
95
+ self._registry = EndpointRegistry()
96
+ self._registry.register(
97
+ Endpoint(
98
+ name="default",
99
+ host=host,
100
+ port=port,
101
+ protocol="kafka",
102
+ )
103
+ )
104
+
105
+ # ------------------------------------------------------------------
106
+ # SystemUnderTest interface
107
+ # ------------------------------------------------------------------
108
+
109
+ @property
110
+ def registry(self) -> EndpointRegistry:
111
+ return self._registry
112
+
113
+ async def execute(self, test_case: TestCase) -> TestResult:
114
+ """Execute a Kafka test described by *test_case*.
115
+
116
+ ``test_case.target`` is the Kafka **topic** name.
117
+
118
+ ``test_case.inputs`` may contain:
119
+
120
+ * ``action`` — one of ``"produce"`` (default), ``"consume"``,
121
+ ``"round_trip"``.
122
+ * ``payload`` — dict or string to publish (for produce/round_trip).
123
+ * ``reply_topic`` — topic to consume from in round_trip mode.
124
+ * ``max_poll_records`` — max records to consume (default: 1).
125
+ """
126
+ if not _kafka_available():
127
+ return TestResult(
128
+ test_case_id=test_case.id,
129
+ passed=False,
130
+ actual_output=None,
131
+ execution_time_ms=0.0,
132
+ error=(
133
+ "kafka-python package is not installed. "
134
+ "Install it with: pip install kafka-python"
135
+ ),
136
+ )
137
+
138
+ action: str = str(test_case.inputs.get("action", "produce")).lower()
139
+ topic: str = test_case.target
140
+
141
+ if action == "produce":
142
+ return await self._action_produce(test_case, topic)
143
+ elif action == "consume":
144
+ return await self._action_consume(test_case, topic)
145
+ elif action == "round_trip":
146
+ return await self._action_round_trip(test_case, topic)
147
+ else:
148
+ return TestResult(
149
+ test_case_id=test_case.id,
150
+ passed=False,
151
+ actual_output=None,
152
+ execution_time_ms=0.0,
153
+ error=f"Unknown Kafka action: {action!r}",
154
+ )
155
+
156
+ async def health_check(self) -> bool:
157
+ """Check that the Kafka broker is reachable."""
158
+ if not _kafka_available():
159
+ return False
160
+ try:
161
+ from kafka import KafkaAdminClient
162
+
163
+ admin = KafkaAdminClient(
164
+ bootstrap_servers=self.bootstrap_servers,
165
+ request_timeout_ms=int(min(self.timeout, 5.0) * 1000),
166
+ )
167
+ admin.close()
168
+ return True
169
+ except Exception: # noqa: BLE001
170
+ return False
171
+
172
+ # ------------------------------------------------------------------
173
+ # Action helpers
174
+ # ------------------------------------------------------------------
175
+
176
+ async def _action_produce(self, test_case: TestCase, topic: str) -> TestResult:
177
+ """Publish a message to *topic*."""
178
+ from kafka import KafkaProducer
179
+ from kafka.errors import KafkaError
180
+
181
+ payload = test_case.inputs.get("payload") or {}
182
+ if isinstance(payload, dict):
183
+ value_bytes = json.dumps(payload).encode("utf-8")
184
+ else:
185
+ value_bytes = str(payload).encode("utf-8")
186
+
187
+ start = time.monotonic()
188
+ try:
189
+ producer = KafkaProducer(
190
+ bootstrap_servers=self.bootstrap_servers,
191
+ value_serializer=_bytes_passthrough,
192
+ request_timeout_ms=int(self.timeout * 1000),
193
+ **self.producer_config,
194
+ )
195
+ future = producer.send(topic, value=value_bytes)
196
+ record_metadata = future.get(timeout=self.timeout)
197
+ producer.close()
198
+ elapsed_ms = (time.monotonic() - start) * 1000.0
199
+ return TestResult(
200
+ test_case_id=test_case.id,
201
+ passed=True,
202
+ actual_output={
203
+ "topic": record_metadata.topic,
204
+ "partition": record_metadata.partition,
205
+ "offset": record_metadata.offset,
206
+ },
207
+ execution_time_ms=elapsed_ms,
208
+ error=None,
209
+ )
210
+ except Exception as exc: # noqa: BLE001
211
+ elapsed_ms = (time.monotonic() - start) * 1000.0
212
+ return TestResult(
213
+ test_case_id=test_case.id,
214
+ passed=False,
215
+ actual_output=None,
216
+ execution_time_ms=elapsed_ms,
217
+ error=f"Kafka produce error: {exc}",
218
+ )
219
+
220
+ async def _action_consume(self, test_case: TestCase, topic: str) -> TestResult:
221
+ """Consume at least one message from *topic* within the timeout."""
222
+ from kafka import KafkaConsumer
223
+
224
+ max_records = int(test_case.inputs.get("max_poll_records") or 1)
225
+ start = time.monotonic()
226
+ try:
227
+ consumer = KafkaConsumer(
228
+ topic,
229
+ bootstrap_servers=self.bootstrap_servers,
230
+ group_id=self.consumer_group,
231
+ auto_offset_reset="latest",
232
+ consumer_timeout_ms=int(self.timeout * 1000),
233
+ max_poll_records=max_records,
234
+ **self.consumer_config,
235
+ )
236
+ records: list[dict] = []
237
+ for msg in consumer:
238
+ try:
239
+ value = json.loads(msg.value.decode("utf-8"))
240
+ except Exception: # noqa: BLE001
241
+ value = msg.value.decode("utf-8", errors="replace")
242
+ records.append({
243
+ "topic": msg.topic,
244
+ "partition": msg.partition,
245
+ "offset": msg.offset,
246
+ "value": value,
247
+ })
248
+ if len(records) >= max_records:
249
+ break
250
+ consumer.close()
251
+ elapsed_ms = (time.monotonic() - start) * 1000.0
252
+
253
+ if not records:
254
+ return TestResult(
255
+ test_case_id=test_case.id,
256
+ passed=False,
257
+ actual_output={"records": []},
258
+ execution_time_ms=elapsed_ms,
259
+ error="No messages received within timeout",
260
+ )
261
+
262
+ return TestResult(
263
+ test_case_id=test_case.id,
264
+ passed=True,
265
+ actual_output={"records": records, "count": len(records)},
266
+ execution_time_ms=elapsed_ms,
267
+ error=None,
268
+ )
269
+ except Exception as exc: # noqa: BLE001
270
+ elapsed_ms = (time.monotonic() - start) * 1000.0
271
+ return TestResult(
272
+ test_case_id=test_case.id,
273
+ passed=False,
274
+ actual_output=None,
275
+ execution_time_ms=elapsed_ms,
276
+ error=f"Kafka consume error: {exc}",
277
+ )
278
+
279
+ async def _action_round_trip(self, test_case: TestCase, topic: str) -> TestResult:
280
+ """Produce to *topic* then consume from a reply topic."""
281
+ from kafka import KafkaConsumer, KafkaProducer
282
+
283
+ reply_topic = str(test_case.inputs.get("reply_topic") or f"{topic}.reply")
284
+ payload = test_case.inputs.get("payload") or {}
285
+
286
+ # Embed correlation ID for matching
287
+ correlation_id = uuid.uuid4().hex
288
+ if isinstance(payload, dict):
289
+ payload = {**payload, "_nat_correlation_id": correlation_id}
290
+ value_bytes = json.dumps(payload).encode("utf-8")
291
+
292
+ start = time.monotonic()
293
+ try:
294
+ producer = KafkaProducer(
295
+ bootstrap_servers=self.bootstrap_servers,
296
+ value_serializer=_bytes_passthrough,
297
+ request_timeout_ms=int(self.timeout * 1000),
298
+ **self.producer_config,
299
+ )
300
+ future = producer.send(topic, value=value_bytes)
301
+ future.get(timeout=self.timeout)
302
+ producer.close()
303
+
304
+ # Now consume from reply topic
305
+ consumer = KafkaConsumer(
306
+ reply_topic,
307
+ bootstrap_servers=self.bootstrap_servers,
308
+ group_id=f"{self.consumer_group}-reply",
309
+ auto_offset_reset="latest",
310
+ consumer_timeout_ms=int(self.timeout * 1000),
311
+ max_poll_records=10,
312
+ **self.consumer_config,
313
+ )
314
+
315
+ reply_found = False
316
+ for msg in consumer:
317
+ try:
318
+ val = json.loads(msg.value.decode("utf-8"))
319
+ if isinstance(val, dict) and val.get("_nat_correlation_id") == correlation_id:
320
+ reply_found = True
321
+ break
322
+ except Exception: # noqa: BLE001
323
+ pass
324
+ consumer.close()
325
+
326
+ elapsed_ms = (time.monotonic() - start) * 1000.0
327
+ return TestResult(
328
+ test_case_id=test_case.id,
329
+ passed=reply_found,
330
+ actual_output={"round_trip_complete": reply_found},
331
+ execution_time_ms=elapsed_ms,
332
+ error=None if reply_found else "Reply message not received within timeout",
333
+ )
334
+ except Exception as exc: # noqa: BLE001
335
+ elapsed_ms = (time.monotonic() - start) * 1000.0
336
+ return TestResult(
337
+ test_case_id=test_case.id,
338
+ passed=False,
339
+ actual_output=None,
340
+ execution_time_ms=elapsed_ms,
341
+ error=f"Kafka round-trip error: {exc}",
342
+ )