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,642 @@
1
+ """
2
+ Test Runner - Hard-constrained execution roles for existing tests.
3
+
4
+ These runners are "dumb" executors that only run existing tests.
5
+ They do NOT:
6
+ - Discover new tests beyond configured patterns
7
+ - Generate new tests
8
+ - Make inferences about test behavior
9
+ - Modify test files
10
+
11
+ They DO:
12
+ - Execute tests matching configured patterns
13
+ - Report pass/fail status
14
+ - Detect flaky tests (regression runner)
15
+ - Support fail-fast mode
16
+ """
17
+
18
+ import asyncio
19
+ import subprocess
20
+ import time
21
+ import json
22
+ from dataclasses import dataclass, field
23
+ from datetime import datetime
24
+ from enum import Enum
25
+ from pathlib import Path
26
+ from typing import Any, Dict, List, Optional, Tuple
27
+ import logging
28
+ import glob
29
+ import re
30
+
31
+ logger = logging.getLogger(__name__)
32
+
33
+
34
+ class TestStatus(Enum):
35
+ """Test execution status."""
36
+
37
+ PASSED = "passed"
38
+ FAILED = "failed"
39
+ SKIPPED = "skipped"
40
+ ERROR = "error"
41
+ FLAKY = "flaky" # Passed after retry
42
+
43
+
44
+ @dataclass
45
+ class TestResult:
46
+ """Result of a single test."""
47
+
48
+ name: str
49
+ status: TestStatus
50
+ duration_seconds: float
51
+ file_path: Optional[str] = None
52
+ line_number: Optional[int] = None
53
+ error_message: Optional[str] = None
54
+ stdout: Optional[str] = None
55
+ stderr: Optional[str] = None
56
+ retry_count: int = 0
57
+
58
+ def to_dict(self) -> Dict[str, Any]:
59
+ return {
60
+ "name": self.name,
61
+ "status": self.status.value,
62
+ "duration_seconds": self.duration_seconds,
63
+ "file_path": self.file_path,
64
+ "line_number": self.line_number,
65
+ "error_message": self.error_message,
66
+ "retry_count": self.retry_count,
67
+ }
68
+
69
+
70
+ @dataclass
71
+ class TestSuiteResult:
72
+ """Result of running a test suite."""
73
+
74
+ runner_type: str # "smoke", "sanity", "regression"
75
+ started_at: datetime
76
+ ended_at: datetime
77
+ duration_seconds: float
78
+
79
+ total_tests: int
80
+ passed: int
81
+ failed: int
82
+ skipped: int
83
+ errors: int
84
+ flaky: int
85
+
86
+ tests: List[TestResult] = field(default_factory=list)
87
+ summary: str = ""
88
+
89
+ @property
90
+ def success(self) -> bool:
91
+ """Did all tests pass (or skip)?"""
92
+ return self.failed == 0 and self.errors == 0
93
+
94
+ @property
95
+ def pass_rate(self) -> float:
96
+ """Percentage of tests that passed."""
97
+ if self.total_tests == 0:
98
+ return 100.0
99
+ return (self.passed / self.total_tests) * 100
100
+
101
+ def to_dict(self) -> Dict[str, Any]:
102
+ return {
103
+ "runner_type": self.runner_type,
104
+ "started_at": self.started_at.isoformat(),
105
+ "ended_at": self.ended_at.isoformat(),
106
+ "duration_seconds": self.duration_seconds,
107
+ "total_tests": self.total_tests,
108
+ "passed": self.passed,
109
+ "failed": self.failed,
110
+ "skipped": self.skipped,
111
+ "errors": self.errors,
112
+ "flaky": self.flaky,
113
+ "success": self.success,
114
+ "pass_rate": self.pass_rate,
115
+ "tests": [t.to_dict() for t in self.tests],
116
+ "summary": self.summary,
117
+ }
118
+
119
+
120
+ class TestRunner:
121
+ """
122
+ Base test runner - executes tests without any intelligence.
123
+
124
+ Hard constraints:
125
+ - No test discovery beyond configured patterns
126
+ - No test generation
127
+ - No inference or reasoning
128
+ """
129
+
130
+ def __init__(
131
+ self,
132
+ project_root: Path,
133
+ test_pattern: str = "**/test_*.py",
134
+ fail_fast: bool = False,
135
+ timeout_seconds: int = 300,
136
+ detect_flakes: bool = False,
137
+ retry_count: int = 0,
138
+ ):
139
+ self.project_root = project_root.resolve()
140
+ self.test_pattern = test_pattern
141
+ self.fail_fast = fail_fast
142
+ self.timeout_seconds = timeout_seconds
143
+ self.detect_flakes = detect_flakes
144
+ self.retry_count = retry_count if detect_flakes else 0
145
+
146
+ @property
147
+ def runner_type(self) -> str:
148
+ return "base"
149
+
150
+ def discover_tests(self) -> List[Path]:
151
+ """
152
+ Find test files matching the pattern(s).
153
+
154
+ This is NOT intelligent discovery - just pattern matching.
155
+ Supports multiple patterns separated by spaces.
156
+ """
157
+ all_files = []
158
+
159
+ # Handle multiple patterns separated by spaces
160
+ patterns = self.test_pattern.split()
161
+ for pattern_part in patterns:
162
+ full_pattern = str(self.project_root / pattern_part.strip())
163
+ matches = glob.glob(full_pattern, recursive=True)
164
+ # Filter out directories, only return files
165
+ files = [f for f in matches if Path(f).is_file()]
166
+ all_files.extend(files)
167
+
168
+ # Remove duplicates and sort
169
+ unique_files = list(set(all_files))
170
+ return [Path(f) for f in sorted(unique_files)]
171
+
172
+ async def run(self) -> TestSuiteResult:
173
+ """Run all tests matching the pattern."""
174
+ started_at = datetime.now()
175
+ start_time = time.monotonic()
176
+
177
+ test_files = self.discover_tests()
178
+ logger.info(f"[{self.runner_type}] Found {len(test_files)} test files")
179
+
180
+ results: List[TestResult] = []
181
+ passed = failed = skipped = errors = flaky = 0
182
+
183
+ for test_file in test_files:
184
+ if self.fail_fast and failed > 0:
185
+ logger.info(f"[{self.runner_type}] Stopping due to fail-fast")
186
+ break
187
+
188
+ result = await self._run_test_file(test_file)
189
+ results.extend(result)
190
+
191
+ for r in result:
192
+ if r.status == TestStatus.PASSED:
193
+ passed += 1
194
+ elif r.status == TestStatus.FAILED:
195
+ failed += 1
196
+ elif r.status == TestStatus.SKIPPED:
197
+ skipped += 1
198
+ elif r.status == TestStatus.ERROR:
199
+ errors += 1
200
+ elif r.status == TestStatus.FLAKY:
201
+ flaky += 1
202
+ passed += 1 # Flaky counts as passed
203
+
204
+ ended_at = datetime.now()
205
+ duration = time.monotonic() - start_time
206
+
207
+ total_tests = passed + failed + skipped + errors
208
+
209
+ # Build summary
210
+ status_emoji = "✅" if (failed == 0 and errors == 0) else "❌"
211
+ summary = (
212
+ f"{status_emoji} {self.runner_type.upper()}: "
213
+ f"{passed} passed, {failed} failed, {skipped} skipped"
214
+ )
215
+ if flaky > 0:
216
+ summary += f", {flaky} flaky"
217
+ summary += f" ({duration:.1f}s)"
218
+
219
+ return TestSuiteResult(
220
+ runner_type=self.runner_type,
221
+ started_at=started_at,
222
+ ended_at=ended_at,
223
+ duration_seconds=duration,
224
+ total_tests=total_tests,
225
+ passed=passed,
226
+ failed=failed,
227
+ skipped=skipped,
228
+ errors=errors,
229
+ flaky=flaky,
230
+ tests=results,
231
+ summary=summary,
232
+ )
233
+
234
+ async def _run_test_file(self, test_file: Path) -> List[TestResult]:
235
+ """
236
+ Run tests in a single file.
237
+
238
+ Returns list of TestResults.
239
+ """
240
+ results = []
241
+ rel_path = str(test_file.relative_to(self.project_root))
242
+
243
+ # Detect test framework
244
+ framework = self._detect_framework(test_file)
245
+
246
+ try:
247
+ if framework == "pytest":
248
+ results = await self._run_pytest(test_file)
249
+ elif framework == "jest":
250
+ results = await self._run_jest(test_file)
251
+ elif framework == "go":
252
+ results = await self._run_go_test(test_file)
253
+ else:
254
+ # Fallback: try pytest
255
+ results = await self._run_pytest(test_file)
256
+ except asyncio.TimeoutError:
257
+ results.append(
258
+ TestResult(
259
+ name=rel_path,
260
+ status=TestStatus.ERROR,
261
+ duration_seconds=self.timeout_seconds,
262
+ file_path=rel_path,
263
+ error_message=f"Test timed out after {self.timeout_seconds}s",
264
+ )
265
+ )
266
+ except Exception as e:
267
+ results.append(
268
+ TestResult(
269
+ name=rel_path,
270
+ status=TestStatus.ERROR,
271
+ duration_seconds=0,
272
+ file_path=rel_path,
273
+ error_message=str(e),
274
+ )
275
+ )
276
+
277
+ return results
278
+
279
+ def _detect_framework(self, test_file: Path) -> str:
280
+ """Detect test framework based on file extension and content."""
281
+ suffix = test_file.suffix
282
+
283
+ if suffix == ".py":
284
+ return "pytest"
285
+ elif suffix in (".js", ".ts", ".jsx", ".tsx"):
286
+ return "jest"
287
+ elif suffix == ".go":
288
+ return "go"
289
+ else:
290
+ return "unknown"
291
+
292
+ async def _run_pytest(self, test_file: Path) -> List[TestResult]:
293
+ """Run pytest on a test file."""
294
+ start_time = time.monotonic()
295
+ rel_path = str(test_file.relative_to(self.project_root))
296
+
297
+ cmd = [
298
+ "python",
299
+ "-m",
300
+ "pytest",
301
+ str(test_file),
302
+ "-v",
303
+ "--tb=short",
304
+ f"--timeout={self.timeout_seconds}",
305
+ "-q",
306
+ ]
307
+
308
+ if self.fail_fast:
309
+ cmd.append("-x")
310
+
311
+ # Add JSON output for parsing
312
+ json_output_file = (
313
+ self.project_root / ".superqode" / "temp" / f"pytest_{test_file.stem}.json"
314
+ )
315
+ json_output_file.parent.mkdir(parents=True, exist_ok=True)
316
+ cmd.extend(["--json-report", f"--json-report-file={json_output_file}"])
317
+
318
+ process = await asyncio.create_subprocess_exec(
319
+ *cmd,
320
+ cwd=str(self.project_root),
321
+ stdout=asyncio.subprocess.PIPE,
322
+ stderr=asyncio.subprocess.PIPE,
323
+ )
324
+
325
+ try:
326
+ stdout, stderr = await asyncio.wait_for(
327
+ process.communicate(), timeout=self.timeout_seconds
328
+ )
329
+ except asyncio.TimeoutError:
330
+ process.kill()
331
+ raise
332
+
333
+ duration = time.monotonic() - start_time
334
+
335
+ # Parse JSON report if available
336
+ results = []
337
+ if json_output_file.exists():
338
+ try:
339
+ report = json.loads(json_output_file.read_text())
340
+ for test in report.get("tests", []):
341
+ status = TestStatus.PASSED
342
+ if test.get("outcome") == "failed":
343
+ status = TestStatus.FAILED
344
+ elif test.get("outcome") == "skipped":
345
+ status = TestStatus.SKIPPED
346
+ elif test.get("outcome") == "error":
347
+ status = TestStatus.ERROR
348
+
349
+ results.append(
350
+ TestResult(
351
+ name=test.get("nodeid", "unknown"),
352
+ status=status,
353
+ duration_seconds=test.get("duration", 0),
354
+ file_path=rel_path,
355
+ error_message=test.get("call", {}).get("longrepr")
356
+ if status == TestStatus.FAILED
357
+ else None,
358
+ )
359
+ )
360
+ except Exception as e:
361
+ logger.warning(f"Failed to parse pytest JSON report: {e}")
362
+
363
+ # Fallback if no JSON report
364
+ if not results:
365
+ status = TestStatus.PASSED if process.returncode == 0 else TestStatus.FAILED
366
+ results.append(
367
+ TestResult(
368
+ name=rel_path,
369
+ status=status,
370
+ duration_seconds=duration,
371
+ file_path=rel_path,
372
+ stdout=stdout.decode() if stdout else None,
373
+ stderr=stderr.decode() if stderr else None,
374
+ )
375
+ )
376
+
377
+ return results
378
+
379
+ async def _run_jest(self, test_file: Path) -> List[TestResult]:
380
+ """Run jest on a test file."""
381
+ start_time = time.monotonic()
382
+ rel_path = str(test_file.relative_to(self.project_root))
383
+
384
+ cmd = [
385
+ "npx",
386
+ "jest",
387
+ str(test_file),
388
+ "--json",
389
+ "--verbose",
390
+ ]
391
+
392
+ if self.fail_fast:
393
+ cmd.append("--bail")
394
+
395
+ process = await asyncio.create_subprocess_exec(
396
+ *cmd,
397
+ cwd=str(self.project_root),
398
+ stdout=asyncio.subprocess.PIPE,
399
+ stderr=asyncio.subprocess.PIPE,
400
+ )
401
+
402
+ try:
403
+ stdout, stderr = await asyncio.wait_for(
404
+ process.communicate(), timeout=self.timeout_seconds
405
+ )
406
+ except asyncio.TimeoutError:
407
+ process.kill()
408
+ raise
409
+
410
+ duration = time.monotonic() - start_time
411
+
412
+ # Parse JSON output
413
+ results = []
414
+ try:
415
+ report = json.loads(stdout.decode())
416
+ for result in report.get("testResults", []):
417
+ for assertion in result.get("assertionResults", []):
418
+ status = TestStatus.PASSED
419
+ if assertion.get("status") == "failed":
420
+ status = TestStatus.FAILED
421
+ elif assertion.get("status") == "pending":
422
+ status = TestStatus.SKIPPED
423
+
424
+ results.append(
425
+ TestResult(
426
+ name=assertion.get("fullName", "unknown"),
427
+ status=status,
428
+ duration_seconds=assertion.get("duration", 0) / 1000,
429
+ file_path=rel_path,
430
+ error_message="\n".join(assertion.get("failureMessages", []))
431
+ if status == TestStatus.FAILED
432
+ else None,
433
+ )
434
+ )
435
+ except Exception as e:
436
+ # Jest JSON parsing often fails due to output format issues - use debug level
437
+ logger.debug(f"Failed to parse jest JSON output: {e}")
438
+ status = TestStatus.PASSED if process.returncode == 0 else TestStatus.FAILED
439
+ results.append(
440
+ TestResult(
441
+ name=rel_path,
442
+ status=status,
443
+ duration_seconds=duration,
444
+ file_path=rel_path,
445
+ )
446
+ )
447
+
448
+ return results
449
+
450
+ async def _run_go_test(self, test_file: Path) -> List[TestResult]:
451
+ """Run go test on a test file."""
452
+ start_time = time.monotonic()
453
+ rel_path = str(test_file.relative_to(self.project_root))
454
+
455
+ # Go tests run on package level
456
+ package_dir = test_file.parent
457
+
458
+ cmd = [
459
+ "go",
460
+ "test",
461
+ "-v",
462
+ "-json",
463
+ f"-timeout={self.timeout_seconds}s",
464
+ "./...",
465
+ ]
466
+
467
+ process = await asyncio.create_subprocess_exec(
468
+ *cmd,
469
+ cwd=str(package_dir),
470
+ stdout=asyncio.subprocess.PIPE,
471
+ stderr=asyncio.subprocess.PIPE,
472
+ )
473
+
474
+ try:
475
+ stdout, stderr = await asyncio.wait_for(
476
+ process.communicate(), timeout=self.timeout_seconds
477
+ )
478
+ except asyncio.TimeoutError:
479
+ process.kill()
480
+ raise
481
+
482
+ duration = time.monotonic() - start_time
483
+
484
+ # Parse JSON lines output
485
+ results = []
486
+ for line in stdout.decode().split("\n"):
487
+ if not line.strip():
488
+ continue
489
+ try:
490
+ event = json.loads(line)
491
+ if event.get("Action") == "pass" and event.get("Test"):
492
+ results.append(
493
+ TestResult(
494
+ name=event.get("Test"),
495
+ status=TestStatus.PASSED,
496
+ duration_seconds=event.get("Elapsed", 0),
497
+ file_path=rel_path,
498
+ )
499
+ )
500
+ elif event.get("Action") == "fail" and event.get("Test"):
501
+ results.append(
502
+ TestResult(
503
+ name=event.get("Test"),
504
+ status=TestStatus.FAILED,
505
+ duration_seconds=event.get("Elapsed", 0),
506
+ file_path=rel_path,
507
+ error_message=event.get("Output"),
508
+ )
509
+ )
510
+ except json.JSONDecodeError:
511
+ continue
512
+
513
+ if not results:
514
+ status = TestStatus.PASSED if process.returncode == 0 else TestStatus.FAILED
515
+ results.append(
516
+ TestResult(
517
+ name=rel_path,
518
+ status=status,
519
+ duration_seconds=duration,
520
+ file_path=rel_path,
521
+ )
522
+ )
523
+
524
+ return results
525
+
526
+
527
+ class SmokeRunner(TestRunner):
528
+ """
529
+ Smoke Test Runner.
530
+
531
+ Hard constraints:
532
+ - ❌ No discovery (only configured patterns)
533
+ - ❌ No inference
534
+ - ❌ No generation
535
+ - ✅ Fail-fast enabled
536
+ """
537
+
538
+ def __init__(
539
+ self,
540
+ project_root: Path,
541
+ test_pattern: str = "**/test_smoke*.py",
542
+ timeout_seconds: int = 60,
543
+ ):
544
+ super().__init__(
545
+ project_root=project_root,
546
+ test_pattern=test_pattern,
547
+ fail_fast=True,
548
+ timeout_seconds=timeout_seconds,
549
+ detect_flakes=False,
550
+ retry_count=0,
551
+ )
552
+
553
+ @property
554
+ def runner_type(self) -> str:
555
+ return "smoke"
556
+
557
+
558
+ class SanityRunner(TestRunner):
559
+ """
560
+ Sanity Test Runner.
561
+
562
+ Hard constraints:
563
+ - ❌ No discovery (only configured patterns)
564
+ - ❌ No generation
565
+ - ✅ Verifies recent changes didn't break basics
566
+ """
567
+
568
+ def __init__(
569
+ self,
570
+ project_root: Path,
571
+ test_pattern: str = "**/test_sanity*.py",
572
+ timeout_seconds: int = 120,
573
+ ):
574
+ super().__init__(
575
+ project_root=project_root,
576
+ test_pattern=test_pattern,
577
+ fail_fast=False,
578
+ timeout_seconds=timeout_seconds,
579
+ detect_flakes=False,
580
+ retry_count=0,
581
+ )
582
+
583
+ @property
584
+ def runner_type(self) -> str:
585
+ return "sanity"
586
+
587
+
588
+ class RegressionRunner(TestRunner):
589
+ """
590
+ Regression Test Runner.
591
+
592
+ Hard constraints:
593
+ - ❌ No new test creation
594
+ - ✅ Detects failures, flakes, performance regressions
595
+ - ✅ Runs full regression suite
596
+ """
597
+
598
+ def __init__(
599
+ self,
600
+ project_root: Path,
601
+ test_pattern: str = "**/test_*.py",
602
+ timeout_seconds: int = 600,
603
+ detect_flakes: bool = True,
604
+ retry_count: int = 2,
605
+ ):
606
+ super().__init__(
607
+ project_root=project_root,
608
+ test_pattern=test_pattern,
609
+ fail_fast=False,
610
+ timeout_seconds=timeout_seconds,
611
+ detect_flakes=detect_flakes,
612
+ retry_count=retry_count,
613
+ )
614
+
615
+ @property
616
+ def runner_type(self) -> str:
617
+ return "regression"
618
+
619
+ async def _run_test_file(self, test_file: Path) -> List[TestResult]:
620
+ """Run with flake detection."""
621
+ results = await super()._run_test_file(test_file)
622
+
623
+ if not self.detect_flakes:
624
+ return results
625
+
626
+ # Retry failed tests to detect flakes
627
+ flaky_results = []
628
+ for result in results:
629
+ if result.status == TestStatus.FAILED and self.retry_count > 0:
630
+ # Retry the test
631
+ for retry in range(1, self.retry_count + 1):
632
+ retry_results = await super()._run_test_file(test_file)
633
+ matching = [r for r in retry_results if r.name == result.name]
634
+ if matching and matching[0].status == TestStatus.PASSED:
635
+ # Test passed on retry - it's flaky
636
+ result.status = TestStatus.FLAKY
637
+ result.retry_count = retry
638
+ logger.info(f"Detected flaky test: {result.name} (passed on retry {retry})")
639
+ break
640
+ flaky_results.append(result)
641
+
642
+ return flaky_results