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,487 @@
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
+ """Cypress test-file ingestor plugin.
7
+
8
+ Parses Cypress ``.cy.js`` / ``.cy.ts`` test files using regex-based
9
+ extraction (no AST or Node.js runtime required) and converts the discovered
10
+ test actions into :class:`~mannf.product.ingestors.models.IngestedTestCase`
11
+ objects that match the :class:`~mannf.core.functional_orchestrator.FunctionalTestOrchestrator`
12
+ task shape.
13
+
14
+ Supported Cypress patterns
15
+ --------------------------
16
+ * ``describe('suite name', () => { ... })`` / ``context(...)`` — suite blocks
17
+ * ``it('test name', () => { ... })`` / ``specify(...)`` — individual test blocks
18
+ * ``cy.visit('url')`` / ``cy.visit("url")``
19
+ * ``cy.get('selector').type('value')``
20
+ * ``cy.get('selector').clear().type('value')``
21
+ * ``cy.get('selector').click()``
22
+ * ``cy.get('selector').check()`` / ``.uncheck()``
23
+ * ``cy.get('selector').select('value')``
24
+ * ``cy.get('selector').first().click()`` / ``.eq(n).click()``
25
+ * ``cy.contains('text').click()`` / ``cy.contains('selector', 'text').click()``
26
+ * ``cy.url().should('include', '/path')`` / ``('eq', 'url')``
27
+ * ``cy.get('selector').should('be.visible')`` / ``'exist'`` / ``'have.text', '...'``
28
+ * ``cy.intercept('method', 'url').as('alias')`` — captured as metadata
29
+ * ``cy.wait('@alias')`` — captured as wait step
30
+ * ``cy.scrollTo('position')`` / ``cy.get('selector').scrollIntoView()``
31
+
32
+ The extracted tests are tagged with ``["cypress"]`` and their source
33
+ file reference is set to ``file.cy.ts::<test_name>``.
34
+ """
35
+
36
+ from __future__ import annotations
37
+
38
+ import logging
39
+ import re
40
+ from pathlib import Path
41
+ from typing import Any
42
+
43
+ from mannf.product.ingestors.base import IngestorPlugin
44
+ from mannf.product.ingestors.models import (
45
+ IngestedTestCase,
46
+ IngestorSource,
47
+ IngestResult,
48
+ )
49
+
50
+ logger = logging.getLogger(__name__)
51
+
52
+ # ---------------------------------------------------------------------------
53
+ # Regex patterns
54
+ # ---------------------------------------------------------------------------
55
+
56
+ # it('name', ...) or it("name", ...) or it(`name`, ...)
57
+ # Also matches specify('name', ...)
58
+ # Use \b to avoid matching 'it' inside words like 'visit'
59
+ _RE_IT_BLOCK = re.compile(
60
+ r"""\b(?:it|specify)\s*\(\s*(['"`])(.*?)\1""",
61
+ re.DOTALL,
62
+ )
63
+
64
+ # describe('suite name', ...) or describe("suite name", ...)
65
+ # Also matches context('suite name', ...)
66
+ _RE_DESCRIBE_BLOCK = re.compile(
67
+ r"""\b(?:describe|context)\s*\(\s*(['"`])(.*?)\1""",
68
+ re.DOTALL,
69
+ )
70
+
71
+ # cy.visit('url') / cy.visit("url") / cy.visit(`url`)
72
+ _RE_VISIT = re.compile(
73
+ r"""cy\.visit\s*\(\s*(['"`])(.*?)\1""",
74
+ )
75
+
76
+ # cy.get('selector').type('value') — handles optional .clear() before .type()
77
+ _RE_GET_TYPE = re.compile(
78
+ r"""cy\.get\s*\(\s*(['"`])(.*?)\1\s*\)(?:\.clear\s*\(\s*\))?\.type\s*\(\s*(['"`])(.*?)\3\s*\)""",
79
+ )
80
+
81
+ # cy.get('selector').click() — supports chained .first() / .eq(n) before .click()
82
+ _RE_GET_CLICK = re.compile(
83
+ r"""cy\.get\s*\(\s*(['"`])(.*?)\1\s*\)(?:\.(?:first|last)\s*\(\s*\)|\.eq\s*\(\s*\d+\s*\))*\.click\s*\(\s*\)""",
84
+ )
85
+
86
+ # cy.get('selector').check() or .uncheck()
87
+ _RE_GET_CHECK = re.compile(
88
+ r"""cy\.get\s*\(\s*(['"`])(.*?)\1\s*\)\.(?:check|uncheck)\s*\(\s*\)""",
89
+ )
90
+
91
+ # cy.get('selector').select('value')
92
+ _RE_GET_SELECT = re.compile(
93
+ r"""cy\.get\s*\(\s*(['"`])(.*?)\1\s*\)\.select\s*\(\s*(['"`])(.*?)\3\s*\)""",
94
+ )
95
+
96
+ # cy.contains('text').click() — one-argument form
97
+ _RE_CONTAINS_CLICK_1 = re.compile(
98
+ r"""cy\.contains\s*\(\s*(['"`])(.*?)\1\s*\)\.click\s*\(\s*\)""",
99
+ )
100
+
101
+ # cy.contains('selector', 'text').click() — two-argument form
102
+ _RE_CONTAINS_CLICK_2 = re.compile(
103
+ r"""cy\.contains\s*\(\s*(['"`])(.*?)\1\s*,\s*(['"`])(.*?)\3\s*\)\.click\s*\(\s*\)""",
104
+ )
105
+
106
+ # cy.url().should('include'|'eq', 'value')
107
+ _RE_URL_SHOULD = re.compile(
108
+ r"""cy\.url\s*\(\s*\)\.should\s*\(\s*(['"`])(include|eq|contain)\1\s*,\s*(['"`])(.*?)\3\s*\)""",
109
+ )
110
+
111
+ # cy.get('selector').should('assertion') — single-arg form (e.g., 'be.visible', 'exist', 'not.exist')
112
+ _RE_GET_SHOULD_1 = re.compile(
113
+ r"""cy\.get\s*\(\s*(['"`])(.*?)\1\s*\)\.should\s*\(\s*(['"`])(.*?)\3\s*\)""",
114
+ )
115
+
116
+ # cy.get('selector').should('assertion', 'value') — two-arg form (e.g., 'have.text', 'have.value')
117
+ _RE_GET_SHOULD_2 = re.compile(
118
+ r"""cy\.get\s*\(\s*(['"`])(.*?)\1\s*\)\.should\s*\(\s*(['"`])(.*?)\3\s*,\s*(['"`])(.*?)\5\s*\)""",
119
+ )
120
+
121
+ # cy.intercept('METHOD', 'url').as('alias')
122
+ _RE_INTERCEPT = re.compile(
123
+ r"""cy\.intercept\s*\(\s*(['"`])(.*?)\1\s*,\s*(['"`])(.*?)\3\s*\)\.as\s*\(\s*(['"`])(.*?)\5\s*\)""",
124
+ )
125
+
126
+ # cy.wait('@alias')
127
+ _RE_WAIT = re.compile(
128
+ r"""cy\.wait\s*\(\s*(['"`])(@\w+)\1\s*\)""",
129
+ )
130
+
131
+ # cy.scrollTo('position')
132
+ _RE_SCROLL_TO = re.compile(
133
+ r"""cy\.scrollTo\s*\(\s*(['"`])(.*?)\1\s*\)""",
134
+ )
135
+
136
+ # cy.get('selector').scrollIntoView()
137
+ _RE_SCROLL_INTO_VIEW = re.compile(
138
+ r"""cy\.get\s*\(\s*(['"`])(.*?)\1\s*\)\.scrollIntoView\s*\(\s*\)""",
139
+ )
140
+
141
+ # Priority heuristics
142
+ _HIGH_PRIORITY_KEYWORDS: tuple[str, ...] = (
143
+ "login", "auth", "register", "checkout", "payment", "critical",
144
+ )
145
+ _NEGATIVE_KEYWORDS: tuple[str, ...] = (
146
+ "invalid", "error", "fail", "negative", "reject", "wrong",
147
+ )
148
+
149
+ # Cypress file extensions
150
+ _CYPRESS_EXTENSIONS: frozenset[str] = frozenset(
151
+ [".cy.js", ".cy.ts", ".cy.jsx", ".cy.tsx"]
152
+ )
153
+
154
+ # ---------------------------------------------------------------------------
155
+ # Helper — extract test blocks from source
156
+ # ---------------------------------------------------------------------------
157
+
158
+
159
+ def _extract_test_blocks(content: str) -> list[tuple[str, str]]:
160
+ """Return a list of (test_name, test_body) pairs from *content*.
161
+
162
+ Uses a brace-counting approach to isolate each ``it('name', ...)`` /
163
+ ``specify('name', ...)`` block. The test name is optionally prefixed with
164
+ the enclosing ``describe`` / ``context`` suite name so that tests from
165
+ different suites are unambiguous.
166
+ """
167
+ results: list[tuple[str, str]] = []
168
+
169
+ # Build a list of (start, end, suite_name) for every describe/context block
170
+ # so we can look up which suite contains each it() call.
171
+ suite_ranges: list[tuple[int, int, str]] = []
172
+ for dm in _RE_DESCRIBE_BLOCK.finditer(content):
173
+ suite_name = dm.group(2)
174
+ pos = dm.end()
175
+ brace_pos = content.find("{", pos)
176
+ if brace_pos == -1:
177
+ continue
178
+ depth = 0
179
+ end_pos = brace_pos
180
+ for i, ch in enumerate(content[brace_pos:], start=brace_pos):
181
+ if ch == "{":
182
+ depth += 1
183
+ elif ch == "}":
184
+ depth -= 1
185
+ if depth == 0:
186
+ end_pos = i
187
+ break
188
+ suite_ranges.append((dm.start(), end_pos, suite_name))
189
+
190
+ for m in _RE_IT_BLOCK.finditer(content):
191
+ raw_name = m.group(2)
192
+
193
+ # Determine the innermost enclosing suite (if any)
194
+ suite_name: str | None = None
195
+ for s_start, s_end, s_name in suite_ranges:
196
+ if s_start <= m.start() <= s_end:
197
+ suite_name = s_name
198
+ full_name = f"{suite_name} > {raw_name}" if suite_name else raw_name
199
+
200
+ # Find opening '{' of the arrow function body
201
+ pos = m.end()
202
+ arrow_match = content.find("=> {", pos)
203
+ if arrow_match != -1:
204
+ brace_pos = content.find("{", arrow_match)
205
+ else:
206
+ brace_pos = content.find("{", pos)
207
+ if brace_pos == -1:
208
+ continue
209
+ # Brace-count to find the matching '}'
210
+ depth = 0
211
+ end_pos = brace_pos
212
+ for i, ch in enumerate(content[brace_pos:], start=brace_pos):
213
+ if ch == "{":
214
+ depth += 1
215
+ elif ch == "}":
216
+ depth -= 1
217
+ if depth == 0:
218
+ end_pos = i
219
+ break
220
+ body = content[brace_pos : end_pos + 1]
221
+ results.append((full_name, body))
222
+
223
+ return results
224
+
225
+
226
+ # ---------------------------------------------------------------------------
227
+ # Step extractors
228
+ # ---------------------------------------------------------------------------
229
+
230
+
231
+ def _extract_steps(body: str) -> list[dict[str, Any]]:
232
+ """Extract all recognisable Cypress actions from *body* as step dicts."""
233
+ steps: list[dict[str, Any]] = []
234
+
235
+ # cy.visit
236
+ for m in _RE_VISIT.finditer(body):
237
+ url = m.group(2)
238
+ steps.append(
239
+ {"action": "navigate", "selector": url, "url": url, "_pos": m.start()}
240
+ )
241
+
242
+ # cy.get().type() (with optional .clear() prefix)
243
+ for m in _RE_GET_TYPE.finditer(body):
244
+ steps.append(
245
+ {"action": "fill", "selector": m.group(2), "value": m.group(4), "_pos": m.start()}
246
+ )
247
+
248
+ # cy.get().click() (with optional .first()/.eq(n) prefix)
249
+ for m in _RE_GET_CLICK.finditer(body):
250
+ steps.append(
251
+ {"action": "click", "selector": m.group(2), "_pos": m.start()}
252
+ )
253
+
254
+ # cy.get().check() / .uncheck() → treated as click
255
+ for m in _RE_GET_CHECK.finditer(body):
256
+ steps.append(
257
+ {"action": "click", "selector": m.group(2), "_pos": m.start()}
258
+ )
259
+
260
+ # cy.get().select() → mapped to fill
261
+ for m in _RE_GET_SELECT.finditer(body):
262
+ steps.append(
263
+ {"action": "fill", "selector": m.group(2), "value": m.group(4), "_pos": m.start()}
264
+ )
265
+
266
+ # cy.contains('selector', 'text').click() — two-arg form (more specific, match first)
267
+ for m in _RE_CONTAINS_CLICK_2.finditer(body):
268
+ steps.append(
269
+ {"action": "click", "selector": f"text={m.group(4)}", "_pos": m.start()}
270
+ )
271
+
272
+ # cy.contains('text').click() — one-arg form
273
+ # Exclude positions already matched by the two-arg form.
274
+ contains_two_arg_positions = {
275
+ m.start() for m in _RE_CONTAINS_CLICK_2.finditer(body)
276
+ }
277
+ for m in _RE_CONTAINS_CLICK_1.finditer(body):
278
+ if m.start() not in contains_two_arg_positions:
279
+ steps.append(
280
+ {"action": "click", "selector": f"text={m.group(2)}", "_pos": m.start()}
281
+ )
282
+
283
+ # cy.url().should(...)
284
+ for m in _RE_URL_SHOULD.finditer(body):
285
+ steps.append(
286
+ {
287
+ "action": "assert",
288
+ "assertion": "url",
289
+ "value": m.group(4),
290
+ "_pos": m.start(),
291
+ }
292
+ )
293
+
294
+ # cy.get().should('assertion', 'value') — two-arg form (more specific, match first)
295
+ for m in _RE_GET_SHOULD_2.finditer(body):
296
+ steps.append(
297
+ {
298
+ "action": "assert",
299
+ "assertion": m.group(4),
300
+ "selector": m.group(2),
301
+ "value": m.group(6),
302
+ "_pos": m.start(),
303
+ }
304
+ )
305
+
306
+ # cy.get().should('assertion') — single-arg form
307
+ # Only emit if this position wasn't already captured by the two-arg form.
308
+ two_arg_positions = {
309
+ m.start() for m in _RE_GET_SHOULD_2.finditer(body)
310
+ }
311
+ for m in _RE_GET_SHOULD_1.finditer(body):
312
+ if m.start() not in two_arg_positions:
313
+ steps.append(
314
+ {
315
+ "action": "assert",
316
+ "assertion": m.group(4),
317
+ "selector": m.group(2),
318
+ "_pos": m.start(),
319
+ }
320
+ )
321
+
322
+ # cy.intercept().as() — capture as metadata step
323
+ for m in _RE_INTERCEPT.finditer(body):
324
+ steps.append(
325
+ {
326
+ "action": "intercept",
327
+ "method": m.group(2),
328
+ "url": m.group(4),
329
+ "alias": m.group(6),
330
+ "_pos": m.start(),
331
+ }
332
+ )
333
+
334
+ # cy.wait('@alias')
335
+ for m in _RE_WAIT.finditer(body):
336
+ steps.append(
337
+ {"action": "wait", "alias": m.group(2), "_pos": m.start()}
338
+ )
339
+
340
+ # cy.scrollTo()
341
+ for m in _RE_SCROLL_TO.finditer(body):
342
+ steps.append(
343
+ {"action": "scroll", "position": m.group(2), "_pos": m.start()}
344
+ )
345
+
346
+ # cy.get().scrollIntoView()
347
+ for m in _RE_SCROLL_INTO_VIEW.finditer(body):
348
+ steps.append(
349
+ {"action": "scroll", "selector": m.group(2), "_pos": m.start()}
350
+ )
351
+
352
+ # Sort by position so steps are in source order
353
+ steps.sort(key=lambda s: s.pop("_pos", 0))
354
+ return steps
355
+
356
+
357
+ # ---------------------------------------------------------------------------
358
+ # Priority / tag inference
359
+ # ---------------------------------------------------------------------------
360
+
361
+
362
+ def _infer_priority(name: str) -> str:
363
+ name_lower = name.lower()
364
+ if any(kw in name_lower for kw in _HIGH_PRIORITY_KEYWORDS):
365
+ return "high"
366
+ if any(kw in name_lower for kw in _NEGATIVE_KEYWORDS):
367
+ return "medium"
368
+ return "medium"
369
+
370
+
371
+ def _infer_tags(name: str) -> list[str]:
372
+ tags = ["cypress"]
373
+ name_lower = name.lower()
374
+ if any(kw in name_lower for kw in _NEGATIVE_KEYWORDS):
375
+ tags.append("negative")
376
+ if any(kw in name_lower for kw in _HIGH_PRIORITY_KEYWORDS):
377
+ tags.append("smoke")
378
+ return tags
379
+
380
+
381
+ # ---------------------------------------------------------------------------
382
+ # CypressIngestor
383
+ # ---------------------------------------------------------------------------
384
+
385
+
386
+ class CypressIngestor(IngestorPlugin):
387
+ """Ingestor for Cypress ``.cy.js`` / ``.cy.ts`` test files.
388
+
389
+ Parses Cypress test files using regex-based extraction and converts
390
+ ``cy.visit()``, ``cy.get().type()``, ``cy.get().click()``,
391
+ ``cy.contains()``, ``cy.url().should()``, and similar calls into
392
+ :class:`~mannf.product.ingestors.models.IngestedTestCase` objects.
393
+ """
394
+
395
+ name = "cypress"
396
+ display_name = "Cypress Test"
397
+ description = "Parse Cypress .cy.js/.cy.ts test files into test cases."
398
+ supported_extensions = [".cy.js", ".cy.ts", ".cy.jsx", ".cy.tsx"]
399
+ supported_formats = ["cypress"]
400
+
401
+ # ------------------------------------------------------------------
402
+ # IngestorPlugin interface
403
+ # ------------------------------------------------------------------
404
+
405
+ async def ingest(
406
+ self, source: IngestorSource, config: dict
407
+ ) -> IngestResult:
408
+ """Parse the Cypress source and return an :class:`IngestResult`."""
409
+ try:
410
+ content = self._read_source(source)
411
+ except (FileNotFoundError, ValueError) as exc:
412
+ return self._make_error_result(source, str(exc))
413
+
414
+ if not content.strip():
415
+ return IngestResult(
416
+ success=True,
417
+ source=source,
418
+ warnings=["Cypress source is empty"],
419
+ stats={"test_case_count": 0},
420
+ )
421
+
422
+ source_name = Path(source.path).name if source.path else "cypress.cy.ts"
423
+ test_cases = self._parse(content, source_name)
424
+
425
+ return IngestResult(
426
+ success=True,
427
+ source=source,
428
+ test_cases=test_cases,
429
+ stats={"test_case_count": len(test_cases)},
430
+ )
431
+
432
+ def validate_config(self, config: dict) -> list[str]:
433
+ """No required configuration keys."""
434
+ return []
435
+
436
+ def can_handle(self, source: IngestorSource) -> bool:
437
+ """Return True for cypress format or matching file extension/content."""
438
+ if source.format == "cypress":
439
+ return True
440
+ if source.path is not None:
441
+ name = Path(source.path).name.lower()
442
+ if any(name.endswith(ext) for ext in _CYPRESS_EXTENSIONS):
443
+ return True
444
+ if source.raw_content is not None:
445
+ return (
446
+ "cy.visit(" in source.raw_content
447
+ or "cy.get(" in source.raw_content
448
+ or "cy.contains(" in source.raw_content
449
+ )
450
+ return False
451
+
452
+ # ------------------------------------------------------------------
453
+ # Internal parsing
454
+ # ------------------------------------------------------------------
455
+
456
+ def _parse(self, content: str, source_name: str) -> list[IngestedTestCase]:
457
+ """Extract all test cases from *content*."""
458
+ test_cases: list[IngestedTestCase] = []
459
+ seen_names: set[str] = set()
460
+
461
+ for name, body in _extract_test_blocks(content):
462
+ # Deduplicate test names (different describe blocks may use the
463
+ # same bare it() name; disambiguate with a counter)
464
+ unique_name = name
465
+ if unique_name in seen_names:
466
+ counter = 2
467
+ while f"{name} ({counter})" in seen_names:
468
+ counter += 1
469
+ unique_name = f"{name} ({counter})"
470
+ seen_names.add(unique_name)
471
+
472
+ steps = _extract_steps(body)
473
+ priority = _infer_priority(unique_name)
474
+ tags = _infer_tags(unique_name)
475
+
476
+ tc = IngestedTestCase(
477
+ name=unique_name,
478
+ description=f"Cypress test: {unique_name}",
479
+ steps=steps,
480
+ priority=priority,
481
+ tags=tags,
482
+ source_ref=f"{source_name}::{unique_name}",
483
+ metadata={"source_format": "cypress"},
484
+ )
485
+ test_cases.append(tc)
486
+
487
+ return test_cases