superqode 0.1.5__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 (288) hide show
  1. superqode/__init__.py +33 -0
  2. superqode/acp/__init__.py +23 -0
  3. superqode/acp/client.py +913 -0
  4. superqode/acp/permission_screen.py +457 -0
  5. superqode/acp/types.py +480 -0
  6. superqode/acp_discovery.py +856 -0
  7. superqode/agent/__init__.py +22 -0
  8. superqode/agent/edit_strategies.py +334 -0
  9. superqode/agent/loop.py +892 -0
  10. superqode/agent/qe_report_templates.py +39 -0
  11. superqode/agent/system_prompts.py +353 -0
  12. superqode/agent_output.py +721 -0
  13. superqode/agent_stream.py +953 -0
  14. superqode/agents/__init__.py +59 -0
  15. superqode/agents/acp_registry.py +305 -0
  16. superqode/agents/client.py +249 -0
  17. superqode/agents/data/augmentcode.com.toml +51 -0
  18. superqode/agents/data/cagent.dev.toml +51 -0
  19. superqode/agents/data/claude.com.toml +60 -0
  20. superqode/agents/data/codeassistant.dev.toml +51 -0
  21. superqode/agents/data/codex.openai.com.toml +57 -0
  22. superqode/agents/data/fastagent.ai.toml +66 -0
  23. superqode/agents/data/geminicli.com.toml +77 -0
  24. superqode/agents/data/goose.block.xyz.toml +54 -0
  25. superqode/agents/data/junie.jetbrains.com.toml +56 -0
  26. superqode/agents/data/kimi.moonshot.cn.toml +57 -0
  27. superqode/agents/data/llmlingagent.dev.toml +51 -0
  28. superqode/agents/data/molt.bot.toml +49 -0
  29. superqode/agents/data/opencode.ai.toml +60 -0
  30. superqode/agents/data/stakpak.dev.toml +51 -0
  31. superqode/agents/data/vtcode.dev.toml +51 -0
  32. superqode/agents/discovery.py +266 -0
  33. superqode/agents/messaging.py +160 -0
  34. superqode/agents/persona.py +166 -0
  35. superqode/agents/registry.py +421 -0
  36. superqode/agents/schema.py +72 -0
  37. superqode/agents/unified.py +367 -0
  38. superqode/app/__init__.py +111 -0
  39. superqode/app/constants.py +314 -0
  40. superqode/app/css.py +366 -0
  41. superqode/app/models.py +118 -0
  42. superqode/app/suggester.py +125 -0
  43. superqode/app/widgets.py +1591 -0
  44. superqode/app_enhanced.py +399 -0
  45. superqode/app_main.py +17187 -0
  46. superqode/approval.py +312 -0
  47. superqode/atomic.py +296 -0
  48. superqode/commands/__init__.py +1 -0
  49. superqode/commands/acp.py +965 -0
  50. superqode/commands/agents.py +180 -0
  51. superqode/commands/auth.py +278 -0
  52. superqode/commands/config.py +374 -0
  53. superqode/commands/init.py +826 -0
  54. superqode/commands/providers.py +819 -0
  55. superqode/commands/qe.py +1145 -0
  56. superqode/commands/roles.py +380 -0
  57. superqode/commands/serve.py +172 -0
  58. superqode/commands/suggestions.py +127 -0
  59. superqode/commands/superqe.py +460 -0
  60. superqode/config/__init__.py +51 -0
  61. superqode/config/loader.py +812 -0
  62. superqode/config/schema.py +498 -0
  63. superqode/core/__init__.py +111 -0
  64. superqode/core/roles.py +281 -0
  65. superqode/danger.py +386 -0
  66. superqode/data/superqode-template.yaml +1522 -0
  67. superqode/design_system.py +1080 -0
  68. superqode/dialogs/__init__.py +6 -0
  69. superqode/dialogs/base.py +39 -0
  70. superqode/dialogs/model.py +130 -0
  71. superqode/dialogs/provider.py +870 -0
  72. superqode/diff_view.py +919 -0
  73. superqode/enterprise.py +21 -0
  74. superqode/evaluation/__init__.py +25 -0
  75. superqode/evaluation/adapters.py +93 -0
  76. superqode/evaluation/behaviors.py +89 -0
  77. superqode/evaluation/engine.py +209 -0
  78. superqode/evaluation/scenarios.py +96 -0
  79. superqode/execution/__init__.py +36 -0
  80. superqode/execution/linter.py +538 -0
  81. superqode/execution/modes.py +347 -0
  82. superqode/execution/resolver.py +283 -0
  83. superqode/execution/runner.py +642 -0
  84. superqode/file_explorer.py +811 -0
  85. superqode/file_viewer.py +471 -0
  86. superqode/flash.py +183 -0
  87. superqode/guidance/__init__.py +58 -0
  88. superqode/guidance/config.py +203 -0
  89. superqode/guidance/prompts.py +71 -0
  90. superqode/harness/__init__.py +54 -0
  91. superqode/harness/accelerator.py +291 -0
  92. superqode/harness/config.py +319 -0
  93. superqode/harness/validator.py +147 -0
  94. superqode/history.py +279 -0
  95. superqode/integrations/superopt_runner.py +124 -0
  96. superqode/logging/__init__.py +49 -0
  97. superqode/logging/adapters.py +219 -0
  98. superqode/logging/formatter.py +923 -0
  99. superqode/logging/integration.py +341 -0
  100. superqode/logging/sinks.py +170 -0
  101. superqode/logging/unified_log.py +417 -0
  102. superqode/lsp/__init__.py +26 -0
  103. superqode/lsp/client.py +544 -0
  104. superqode/main.py +1069 -0
  105. superqode/mcp/__init__.py +89 -0
  106. superqode/mcp/auth_storage.py +380 -0
  107. superqode/mcp/client.py +1236 -0
  108. superqode/mcp/config.py +319 -0
  109. superqode/mcp/integration.py +337 -0
  110. superqode/mcp/oauth.py +436 -0
  111. superqode/mcp/oauth_callback.py +385 -0
  112. superqode/mcp/types.py +290 -0
  113. superqode/memory/__init__.py +31 -0
  114. superqode/memory/feedback.py +342 -0
  115. superqode/memory/store.py +522 -0
  116. superqode/notifications.py +369 -0
  117. superqode/optimization/__init__.py +5 -0
  118. superqode/optimization/config.py +33 -0
  119. superqode/permissions/__init__.py +25 -0
  120. superqode/permissions/rules.py +488 -0
  121. superqode/plan.py +323 -0
  122. superqode/providers/__init__.py +33 -0
  123. superqode/providers/gateway/__init__.py +165 -0
  124. superqode/providers/gateway/base.py +228 -0
  125. superqode/providers/gateway/litellm_gateway.py +1170 -0
  126. superqode/providers/gateway/openresponses_gateway.py +436 -0
  127. superqode/providers/health.py +297 -0
  128. superqode/providers/huggingface/__init__.py +74 -0
  129. superqode/providers/huggingface/downloader.py +472 -0
  130. superqode/providers/huggingface/endpoints.py +442 -0
  131. superqode/providers/huggingface/hub.py +531 -0
  132. superqode/providers/huggingface/inference.py +394 -0
  133. superqode/providers/huggingface/transformers_runner.py +516 -0
  134. superqode/providers/local/__init__.py +100 -0
  135. superqode/providers/local/base.py +438 -0
  136. superqode/providers/local/discovery.py +418 -0
  137. superqode/providers/local/lmstudio.py +256 -0
  138. superqode/providers/local/mlx.py +457 -0
  139. superqode/providers/local/ollama.py +486 -0
  140. superqode/providers/local/sglang.py +268 -0
  141. superqode/providers/local/tgi.py +260 -0
  142. superqode/providers/local/tool_support.py +477 -0
  143. superqode/providers/local/vllm.py +258 -0
  144. superqode/providers/manager.py +1338 -0
  145. superqode/providers/models.py +1016 -0
  146. superqode/providers/models_dev.py +578 -0
  147. superqode/providers/openresponses/__init__.py +87 -0
  148. superqode/providers/openresponses/converters/__init__.py +17 -0
  149. superqode/providers/openresponses/converters/messages.py +343 -0
  150. superqode/providers/openresponses/converters/tools.py +268 -0
  151. superqode/providers/openresponses/schema/__init__.py +56 -0
  152. superqode/providers/openresponses/schema/models.py +585 -0
  153. superqode/providers/openresponses/streaming/__init__.py +5 -0
  154. superqode/providers/openresponses/streaming/parser.py +338 -0
  155. superqode/providers/openresponses/tools/__init__.py +21 -0
  156. superqode/providers/openresponses/tools/apply_patch.py +352 -0
  157. superqode/providers/openresponses/tools/code_interpreter.py +290 -0
  158. superqode/providers/openresponses/tools/file_search.py +333 -0
  159. superqode/providers/openresponses/tools/mcp_adapter.py +252 -0
  160. superqode/providers/registry.py +716 -0
  161. superqode/providers/usage.py +332 -0
  162. superqode/pure_mode.py +384 -0
  163. superqode/qr/__init__.py +23 -0
  164. superqode/qr/dashboard.py +781 -0
  165. superqode/qr/generator.py +1018 -0
  166. superqode/qr/templates.py +135 -0
  167. superqode/safety/__init__.py +41 -0
  168. superqode/safety/sandbox.py +413 -0
  169. superqode/safety/warnings.py +256 -0
  170. superqode/server/__init__.py +33 -0
  171. superqode/server/lsp_server.py +775 -0
  172. superqode/server/web.py +250 -0
  173. superqode/session/__init__.py +25 -0
  174. superqode/session/persistence.py +580 -0
  175. superqode/session/sharing.py +477 -0
  176. superqode/session.py +475 -0
  177. superqode/sidebar.py +2991 -0
  178. superqode/stream_view.py +648 -0
  179. superqode/styles/__init__.py +3 -0
  180. superqode/superqe/__init__.py +184 -0
  181. superqode/superqe/acp_runner.py +1064 -0
  182. superqode/superqe/constitution/__init__.py +62 -0
  183. superqode/superqe/constitution/evaluator.py +308 -0
  184. superqode/superqe/constitution/loader.py +432 -0
  185. superqode/superqe/constitution/schema.py +250 -0
  186. superqode/superqe/events.py +591 -0
  187. superqode/superqe/frameworks/__init__.py +65 -0
  188. superqode/superqe/frameworks/base.py +234 -0
  189. superqode/superqe/frameworks/e2e.py +263 -0
  190. superqode/superqe/frameworks/executor.py +237 -0
  191. superqode/superqe/frameworks/javascript.py +409 -0
  192. superqode/superqe/frameworks/python.py +373 -0
  193. superqode/superqe/frameworks/registry.py +92 -0
  194. superqode/superqe/mcp_tools/__init__.py +47 -0
  195. superqode/superqe/mcp_tools/core_tools.py +418 -0
  196. superqode/superqe/mcp_tools/registry.py +230 -0
  197. superqode/superqe/mcp_tools/testing_tools.py +167 -0
  198. superqode/superqe/noise.py +89 -0
  199. superqode/superqe/orchestrator.py +778 -0
  200. superqode/superqe/roles.py +609 -0
  201. superqode/superqe/session.py +713 -0
  202. superqode/superqe/skills/__init__.py +57 -0
  203. superqode/superqe/skills/base.py +106 -0
  204. superqode/superqe/skills/core_skills.py +899 -0
  205. superqode/superqe/skills/registry.py +90 -0
  206. superqode/superqe/verifier.py +101 -0
  207. superqode/superqe_cli.py +76 -0
  208. superqode/tool_call.py +358 -0
  209. superqode/tools/__init__.py +93 -0
  210. superqode/tools/agent_tools.py +496 -0
  211. superqode/tools/base.py +324 -0
  212. superqode/tools/batch_tool.py +133 -0
  213. superqode/tools/diagnostics.py +311 -0
  214. superqode/tools/edit_tools.py +653 -0
  215. superqode/tools/enhanced_base.py +515 -0
  216. superqode/tools/file_tools.py +269 -0
  217. superqode/tools/file_tracking.py +45 -0
  218. superqode/tools/lsp_tools.py +610 -0
  219. superqode/tools/network_tools.py +350 -0
  220. superqode/tools/permissions.py +400 -0
  221. superqode/tools/question_tool.py +324 -0
  222. superqode/tools/search_tools.py +598 -0
  223. superqode/tools/shell_tools.py +259 -0
  224. superqode/tools/todo_tools.py +121 -0
  225. superqode/tools/validation.py +80 -0
  226. superqode/tools/web_tools.py +639 -0
  227. superqode/tui.py +1152 -0
  228. superqode/tui_integration.py +875 -0
  229. superqode/tui_widgets/__init__.py +27 -0
  230. superqode/tui_widgets/widgets/__init__.py +18 -0
  231. superqode/tui_widgets/widgets/progress.py +185 -0
  232. superqode/tui_widgets/widgets/tool_display.py +188 -0
  233. superqode/undo_manager.py +574 -0
  234. superqode/utils/__init__.py +5 -0
  235. superqode/utils/error_handling.py +323 -0
  236. superqode/utils/fuzzy.py +257 -0
  237. superqode/widgets/__init__.py +477 -0
  238. superqode/widgets/agent_collab.py +390 -0
  239. superqode/widgets/agent_store.py +936 -0
  240. superqode/widgets/agent_switcher.py +395 -0
  241. superqode/widgets/animation_manager.py +284 -0
  242. superqode/widgets/code_context.py +356 -0
  243. superqode/widgets/command_palette.py +412 -0
  244. superqode/widgets/connection_status.py +537 -0
  245. superqode/widgets/conversation_history.py +470 -0
  246. superqode/widgets/diff_indicator.py +155 -0
  247. superqode/widgets/enhanced_status_bar.py +385 -0
  248. superqode/widgets/enhanced_toast.py +476 -0
  249. superqode/widgets/file_browser.py +809 -0
  250. superqode/widgets/file_reference.py +585 -0
  251. superqode/widgets/issue_timeline.py +340 -0
  252. superqode/widgets/leader_key.py +264 -0
  253. superqode/widgets/mode_switcher.py +445 -0
  254. superqode/widgets/model_picker.py +234 -0
  255. superqode/widgets/permission_preview.py +1205 -0
  256. superqode/widgets/prompt.py +358 -0
  257. superqode/widgets/provider_connect.py +725 -0
  258. superqode/widgets/pty_shell.py +587 -0
  259. superqode/widgets/qe_dashboard.py +321 -0
  260. superqode/widgets/resizable_sidebar.py +377 -0
  261. superqode/widgets/response_changes.py +218 -0
  262. superqode/widgets/response_display.py +528 -0
  263. superqode/widgets/rich_tool_display.py +613 -0
  264. superqode/widgets/sidebar_panels.py +1180 -0
  265. superqode/widgets/slash_complete.py +356 -0
  266. superqode/widgets/split_view.py +612 -0
  267. superqode/widgets/status_bar.py +273 -0
  268. superqode/widgets/superqode_display.py +786 -0
  269. superqode/widgets/thinking_display.py +815 -0
  270. superqode/widgets/throbber.py +87 -0
  271. superqode/widgets/toast.py +206 -0
  272. superqode/widgets/unified_output.py +1073 -0
  273. superqode/workspace/__init__.py +75 -0
  274. superqode/workspace/artifacts.py +472 -0
  275. superqode/workspace/coordinator.py +353 -0
  276. superqode/workspace/diff_tracker.py +429 -0
  277. superqode/workspace/git_guard.py +373 -0
  278. superqode/workspace/git_snapshot.py +526 -0
  279. superqode/workspace/manager.py +750 -0
  280. superqode/workspace/snapshot.py +357 -0
  281. superqode/workspace/watcher.py +535 -0
  282. superqode/workspace/worktree.py +440 -0
  283. superqode-0.1.5.dist-info/METADATA +204 -0
  284. superqode-0.1.5.dist-info/RECORD +288 -0
  285. superqode-0.1.5.dist-info/WHEEL +5 -0
  286. superqode-0.1.5.dist-info/entry_points.txt +3 -0
  287. superqode-0.1.5.dist-info/licenses/LICENSE +648 -0
  288. superqode-0.1.5.dist-info/top_level.txt +1 -0
@@ -0,0 +1,234 @@
1
+ """
2
+ Base classes for test framework support.
3
+
4
+ Provides abstract interfaces for:
5
+ - Test discovery
6
+ - Test execution
7
+ - Result parsing
8
+ """
9
+
10
+ from abc import ABC, abstractmethod
11
+ from dataclasses import dataclass, field
12
+ from datetime import datetime
13
+ from enum import Enum
14
+ from pathlib import Path
15
+ from typing import Any, Dict, List, Optional
16
+ import asyncio
17
+
18
+
19
+ class TestStatus(str, Enum):
20
+ """Status of a test execution."""
21
+
22
+ PASSED = "passed"
23
+ FAILED = "failed"
24
+ SKIPPED = "skipped"
25
+ ERROR = "error"
26
+ PENDING = "pending"
27
+
28
+
29
+ @dataclass
30
+ class FrameworkConfig:
31
+ """Configuration for a test framework."""
32
+
33
+ project_root: Path = field(default_factory=Path.cwd)
34
+ test_pattern: str = "**/test_*.py"
35
+ timeout_seconds: int = 300
36
+ parallel: bool = True
37
+ workers: int = 4
38
+ verbose: bool = False
39
+ coverage: bool = False
40
+ fail_fast: bool = False
41
+ retry_count: int = 0
42
+ env: Dict[str, str] = field(default_factory=dict)
43
+ extra_args: List[str] = field(default_factory=list)
44
+
45
+
46
+ @dataclass
47
+ class TestResult:
48
+ """Result of a single test execution."""
49
+
50
+ name: str
51
+ status: TestStatus
52
+ duration_ms: float
53
+ file_path: Optional[str] = None
54
+ line_number: Optional[int] = None
55
+ error_message: Optional[str] = None
56
+ stack_trace: Optional[str] = None
57
+ stdout: Optional[str] = None
58
+ stderr: Optional[str] = None
59
+ retry_count: int = 0
60
+ metadata: Dict[str, Any] = field(default_factory=dict)
61
+
62
+
63
+ @dataclass
64
+ class TestSuite:
65
+ """A collection of tests."""
66
+
67
+ name: str
68
+ file_path: str
69
+ tests: List[str] = field(default_factory=list)
70
+ setup_file: Optional[str] = None
71
+ teardown_file: Optional[str] = None
72
+ metadata: Dict[str, Any] = field(default_factory=dict)
73
+
74
+
75
+ @dataclass
76
+ class ExecutionResult:
77
+ """Result of executing tests."""
78
+
79
+ framework: str
80
+ started_at: datetime
81
+ ended_at: Optional[datetime] = None
82
+ duration_seconds: float = 0.0
83
+ total: int = 0
84
+ passed: int = 0
85
+ failed: int = 0
86
+ skipped: int = 0
87
+ errors: int = 0
88
+ test_results: List[TestResult] = field(default_factory=list)
89
+ coverage_percentage: Optional[float] = None
90
+ output: str = ""
91
+ error_output: str = ""
92
+
93
+ @property
94
+ def success(self) -> bool:
95
+ """Did all tests pass?"""
96
+ return self.failed == 0 and self.errors == 0
97
+
98
+ @property
99
+ def pass_rate(self) -> float:
100
+ """Calculate pass rate."""
101
+ if self.total == 0:
102
+ return 0.0
103
+ return self.passed / self.total
104
+
105
+ def to_dict(self) -> Dict[str, Any]:
106
+ """Convert to dictionary."""
107
+ return {
108
+ "framework": self.framework,
109
+ "started_at": self.started_at.isoformat(),
110
+ "ended_at": self.ended_at.isoformat() if self.ended_at else None,
111
+ "duration_seconds": self.duration_seconds,
112
+ "total": self.total,
113
+ "passed": self.passed,
114
+ "failed": self.failed,
115
+ "skipped": self.skipped,
116
+ "errors": self.errors,
117
+ "success": self.success,
118
+ "pass_rate": self.pass_rate,
119
+ "coverage": self.coverage_percentage,
120
+ }
121
+
122
+
123
+ class TestFramework(ABC):
124
+ """
125
+ Abstract base class for test frameworks.
126
+
127
+ Implement this class to add support for a new test framework.
128
+ """
129
+
130
+ # Framework metadata - override in subclasses
131
+ NAME = "base"
132
+ DISPLAY_NAME = "Base Framework"
133
+ LANGUAGE = "unknown"
134
+ FILE_PATTERNS = ["**/test_*.py"]
135
+
136
+ def __init__(self, config: Optional[FrameworkConfig] = None):
137
+ """Initialize the framework."""
138
+ self.config = config or FrameworkConfig()
139
+
140
+ @abstractmethod
141
+ async def discover(self) -> List[TestSuite]:
142
+ """
143
+ Discover tests in the project.
144
+
145
+ Returns:
146
+ List of TestSuite objects found
147
+ """
148
+ pass
149
+
150
+ @abstractmethod
151
+ async def execute(self, tests: Optional[List[str]] = None, **kwargs) -> ExecutionResult:
152
+ """
153
+ Execute tests.
154
+
155
+ Args:
156
+ tests: Specific tests to run (None = all)
157
+ **kwargs: Framework-specific options
158
+
159
+ Returns:
160
+ ExecutionResult with all test results
161
+ """
162
+ pass
163
+
164
+ @abstractmethod
165
+ def parse_results(self, output: str) -> List[TestResult]:
166
+ """
167
+ Parse test output into structured results.
168
+
169
+ Args:
170
+ output: Raw output from test runner
171
+
172
+ Returns:
173
+ List of TestResult objects
174
+ """
175
+ pass
176
+
177
+ def get_command(self, tests: Optional[List[str]] = None) -> List[str]:
178
+ """
179
+ Get the command to run tests.
180
+
181
+ Args:
182
+ tests: Specific tests to run
183
+
184
+ Returns:
185
+ Command as list of strings
186
+ """
187
+ raise NotImplementedError
188
+
189
+ @classmethod
190
+ def detect(cls, project_root: Path) -> bool:
191
+ """
192
+ Detect if this framework is used in the project.
193
+
194
+ Args:
195
+ project_root: Root directory of the project
196
+
197
+ Returns:
198
+ True if framework is detected
199
+ """
200
+ return False
201
+
202
+ async def run_command(
203
+ self, command: List[str], timeout: Optional[int] = None
204
+ ) -> tuple[int, str, str]:
205
+ """
206
+ Run a shell command.
207
+
208
+ Args:
209
+ command: Command to run
210
+ timeout: Timeout in seconds
211
+
212
+ Returns:
213
+ Tuple of (exit_code, stdout, stderr)
214
+ """
215
+ timeout = timeout or self.config.timeout_seconds
216
+
217
+ process = await asyncio.create_subprocess_exec(
218
+ *command,
219
+ stdout=asyncio.subprocess.PIPE,
220
+ stderr=asyncio.subprocess.PIPE,
221
+ cwd=str(self.config.project_root),
222
+ env={**dict(__import__("os").environ), **self.config.env},
223
+ )
224
+
225
+ try:
226
+ stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=timeout)
227
+ return (
228
+ process.returncode or 0,
229
+ stdout.decode("utf-8", errors="replace"),
230
+ stderr.decode("utf-8", errors="replace"),
231
+ )
232
+ except asyncio.TimeoutError:
233
+ process.kill()
234
+ return (-1, "", f"Command timed out after {timeout}s")
@@ -0,0 +1,263 @@
1
+ """
2
+ E2E Test Framework Implementations.
3
+
4
+ Supports:
5
+ - Cypress
6
+ - Playwright
7
+ """
8
+
9
+ from datetime import datetime
10
+ from pathlib import Path
11
+ from typing import List, Optional
12
+ import json
13
+ import re
14
+
15
+ from .base import (
16
+ TestFramework,
17
+ FrameworkConfig,
18
+ TestResult,
19
+ TestSuite,
20
+ ExecutionResult,
21
+ TestStatus,
22
+ )
23
+
24
+
25
+ class CypressFramework(TestFramework):
26
+ """Cypress E2E test framework."""
27
+
28
+ NAME = "cypress"
29
+ DISPLAY_NAME = "Cypress"
30
+ LANGUAGE = "javascript"
31
+ FILE_PATTERNS = ["cypress/e2e/**/*.cy.js", "cypress/e2e/**/*.cy.ts"]
32
+
33
+ @classmethod
34
+ def detect(cls, project_root: Path) -> bool:
35
+ """Detect if Cypress is used."""
36
+ if (project_root / "cypress.config.js").exists():
37
+ return True
38
+ if (project_root / "cypress.config.ts").exists():
39
+ return True
40
+ if (project_root / "cypress.json").exists():
41
+ return True
42
+ if (project_root / "cypress").is_dir():
43
+ return True
44
+
45
+ package_json = project_root / "package.json"
46
+ if package_json.exists():
47
+ try:
48
+ data = json.loads(package_json.read_text())
49
+ deps = {**data.get("dependencies", {}), **data.get("devDependencies", {})}
50
+ if "cypress" in deps:
51
+ return True
52
+ except Exception:
53
+ pass
54
+
55
+ return False
56
+
57
+ async def discover(self) -> List[TestSuite]:
58
+ """Discover Cypress tests."""
59
+ suites = []
60
+ for pattern in self.FILE_PATTERNS:
61
+ for file_path in self.config.project_root.glob(pattern):
62
+ suites.append(TestSuite(name=file_path.stem, file_path=str(file_path), tests=[]))
63
+ return suites
64
+
65
+ async def execute(self, tests: Optional[List[str]] = None, **kwargs) -> ExecutionResult:
66
+ """Execute Cypress tests."""
67
+ started_at = datetime.now()
68
+
69
+ command = ["npx", "cypress", "run", "--reporter", "json"]
70
+
71
+ if self.config.parallel and self.config.workers > 1:
72
+ command.extend(["--parallel", "--record"])
73
+
74
+ if tests:
75
+ command.extend(["--spec", ",".join(tests)])
76
+
77
+ exit_code, stdout, stderr = await self.run_command(command)
78
+
79
+ ended_at = datetime.now()
80
+ duration = (ended_at - started_at).total_seconds()
81
+
82
+ # Parse results from output
83
+ test_results = []
84
+ total = passed = failed = skipped = 0
85
+
86
+ # Try to parse JSON output
87
+ try:
88
+ json_match = re.search(r'\{[\s\S]*"stats"[\s\S]*\}', stdout)
89
+ if json_match:
90
+ data = json.loads(json_match.group())
91
+ stats = data.get("stats", {})
92
+ total = stats.get("tests", 0)
93
+ passed = stats.get("passes", 0)
94
+ failed = stats.get("failures", 0)
95
+ skipped = stats.get("pending", 0)
96
+ except json.JSONDecodeError:
97
+ # Fallback to regex parsing
98
+ pass_match = re.search(r"(\d+) passing", stdout)
99
+ fail_match = re.search(r"(\d+) failing", stdout)
100
+ skip_match = re.search(r"(\d+) pending", stdout)
101
+
102
+ if pass_match:
103
+ passed = int(pass_match.group(1))
104
+ if fail_match:
105
+ failed = int(fail_match.group(1))
106
+ if skip_match:
107
+ skipped = int(skip_match.group(1))
108
+ total = passed + failed + skipped
109
+
110
+ return ExecutionResult(
111
+ framework=self.NAME,
112
+ started_at=started_at,
113
+ ended_at=ended_at,
114
+ duration_seconds=duration,
115
+ total=total,
116
+ passed=passed,
117
+ failed=failed,
118
+ skipped=skipped,
119
+ errors=0,
120
+ test_results=test_results,
121
+ output=stdout,
122
+ error_output=stderr,
123
+ )
124
+
125
+ def parse_results(self, output: str) -> List[TestResult]:
126
+ """Parse Cypress output."""
127
+ return []
128
+
129
+ def get_command(self, tests: Optional[List[str]] = None) -> List[str]:
130
+ """Get Cypress command."""
131
+ command = ["npx", "cypress", "run"]
132
+ if tests:
133
+ command.extend(["--spec", ",".join(tests)])
134
+ return command
135
+
136
+
137
+ class PlaywrightFramework(TestFramework):
138
+ """Playwright E2E test framework."""
139
+
140
+ NAME = "playwright"
141
+ DISPLAY_NAME = "Playwright"
142
+ LANGUAGE = "javascript"
143
+ FILE_PATTERNS = ["**/*.spec.ts", "**/*.spec.js", "tests/**/*.ts"]
144
+
145
+ @classmethod
146
+ def detect(cls, project_root: Path) -> bool:
147
+ """Detect if Playwright is used."""
148
+ if (project_root / "playwright.config.ts").exists():
149
+ return True
150
+ if (project_root / "playwright.config.js").exists():
151
+ return True
152
+
153
+ package_json = project_root / "package.json"
154
+ if package_json.exists():
155
+ try:
156
+ data = json.loads(package_json.read_text())
157
+ deps = {**data.get("dependencies", {}), **data.get("devDependencies", {})}
158
+ if "@playwright/test" in deps:
159
+ return True
160
+ except Exception:
161
+ pass
162
+
163
+ return False
164
+
165
+ async def discover(self) -> List[TestSuite]:
166
+ """Discover Playwright tests."""
167
+ command = ["npx", "playwright", "test", "--list"]
168
+
169
+ exit_code, stdout, stderr = await self.run_command(command, timeout=60)
170
+
171
+ suites = {}
172
+ for line in stdout.splitlines():
173
+ line = line.strip()
174
+ if " › " in line:
175
+ parts = line.split(" › ")
176
+ file_path = parts[0] if parts else ""
177
+ test_name = " › ".join(parts[1:]) if len(parts) > 1 else ""
178
+
179
+ if file_path not in suites:
180
+ suites[file_path] = []
181
+ if test_name:
182
+ suites[file_path].append(test_name)
183
+
184
+ return [
185
+ TestSuite(name=Path(fp).stem, file_path=fp, tests=tests) for fp, tests in suites.items()
186
+ ]
187
+
188
+ async def execute(self, tests: Optional[List[str]] = None, **kwargs) -> ExecutionResult:
189
+ """Execute Playwright tests."""
190
+ started_at = datetime.now()
191
+
192
+ command = ["npx", "playwright", "test", "--reporter=json"]
193
+
194
+ if self.config.parallel and self.config.workers > 1:
195
+ command.extend(["--workers", str(self.config.workers)])
196
+
197
+ if tests:
198
+ command.extend(tests)
199
+
200
+ exit_code, stdout, stderr = await self.run_command(command)
201
+
202
+ ended_at = datetime.now()
203
+ duration = (ended_at - started_at).total_seconds()
204
+
205
+ test_results = []
206
+ total = passed = failed = skipped = 0
207
+
208
+ try:
209
+ data = json.loads(stdout)
210
+ stats = data.get("stats", {})
211
+ total = stats.get("expected", 0) + stats.get("unexpected", 0) + stats.get("skipped", 0)
212
+ passed = stats.get("expected", 0)
213
+ failed = stats.get("unexpected", 0)
214
+ skipped = stats.get("skipped", 0)
215
+
216
+ for suite in data.get("suites", []):
217
+ for spec in suite.get("specs", []):
218
+ for test in spec.get("tests", []):
219
+ for result in test.get("results", []):
220
+ status = TestStatus.PASSED
221
+ if result.get("status") == "failed":
222
+ status = TestStatus.FAILED
223
+ elif result.get("status") == "skipped":
224
+ status = TestStatus.SKIPPED
225
+
226
+ test_results.append(
227
+ TestResult(
228
+ name=spec.get("title", ""),
229
+ status=status,
230
+ duration_ms=result.get("duration", 0),
231
+ file_path=spec.get("file"),
232
+ error_message=result.get("error", {}).get("message"),
233
+ )
234
+ )
235
+
236
+ except json.JSONDecodeError:
237
+ pass
238
+
239
+ return ExecutionResult(
240
+ framework=self.NAME,
241
+ started_at=started_at,
242
+ ended_at=ended_at,
243
+ duration_seconds=duration,
244
+ total=total,
245
+ passed=passed,
246
+ failed=failed,
247
+ skipped=skipped,
248
+ errors=0,
249
+ test_results=test_results,
250
+ output=stdout,
251
+ error_output=stderr,
252
+ )
253
+
254
+ def parse_results(self, output: str) -> List[TestResult]:
255
+ """Parse Playwright output."""
256
+ return []
257
+
258
+ def get_command(self, tests: Optional[List[str]] = None) -> List[str]:
259
+ """Get Playwright command."""
260
+ command = ["npx", "playwright", "test"]
261
+ if tests:
262
+ command.extend(tests)
263
+ return command