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.
- superqode/__init__.py +33 -0
- superqode/acp/__init__.py +23 -0
- superqode/acp/client.py +913 -0
- superqode/acp/permission_screen.py +457 -0
- superqode/acp/types.py +480 -0
- superqode/acp_discovery.py +856 -0
- superqode/agent/__init__.py +22 -0
- superqode/agent/edit_strategies.py +334 -0
- superqode/agent/loop.py +892 -0
- superqode/agent/qe_report_templates.py +39 -0
- superqode/agent/system_prompts.py +353 -0
- superqode/agent_output.py +721 -0
- superqode/agent_stream.py +953 -0
- superqode/agents/__init__.py +59 -0
- superqode/agents/acp_registry.py +305 -0
- superqode/agents/client.py +249 -0
- superqode/agents/data/augmentcode.com.toml +51 -0
- superqode/agents/data/cagent.dev.toml +51 -0
- superqode/agents/data/claude.com.toml +60 -0
- superqode/agents/data/codeassistant.dev.toml +51 -0
- superqode/agents/data/codex.openai.com.toml +57 -0
- superqode/agents/data/fastagent.ai.toml +66 -0
- superqode/agents/data/geminicli.com.toml +77 -0
- superqode/agents/data/goose.block.xyz.toml +54 -0
- superqode/agents/data/junie.jetbrains.com.toml +56 -0
- superqode/agents/data/kimi.moonshot.cn.toml +57 -0
- superqode/agents/data/llmlingagent.dev.toml +51 -0
- superqode/agents/data/molt.bot.toml +49 -0
- superqode/agents/data/opencode.ai.toml +60 -0
- superqode/agents/data/stakpak.dev.toml +51 -0
- superqode/agents/data/vtcode.dev.toml +51 -0
- superqode/agents/discovery.py +266 -0
- superqode/agents/messaging.py +160 -0
- superqode/agents/persona.py +166 -0
- superqode/agents/registry.py +421 -0
- superqode/agents/schema.py +72 -0
- superqode/agents/unified.py +367 -0
- superqode/app/__init__.py +111 -0
- superqode/app/constants.py +314 -0
- superqode/app/css.py +366 -0
- superqode/app/models.py +118 -0
- superqode/app/suggester.py +125 -0
- superqode/app/widgets.py +1591 -0
- superqode/app_enhanced.py +399 -0
- superqode/app_main.py +17187 -0
- superqode/approval.py +312 -0
- superqode/atomic.py +296 -0
- superqode/commands/__init__.py +1 -0
- superqode/commands/acp.py +965 -0
- superqode/commands/agents.py +180 -0
- superqode/commands/auth.py +278 -0
- superqode/commands/config.py +374 -0
- superqode/commands/init.py +826 -0
- superqode/commands/providers.py +819 -0
- superqode/commands/qe.py +1145 -0
- superqode/commands/roles.py +380 -0
- superqode/commands/serve.py +172 -0
- superqode/commands/suggestions.py +127 -0
- superqode/commands/superqe.py +460 -0
- superqode/config/__init__.py +51 -0
- superqode/config/loader.py +812 -0
- superqode/config/schema.py +498 -0
- superqode/core/__init__.py +111 -0
- superqode/core/roles.py +281 -0
- superqode/danger.py +386 -0
- superqode/data/superqode-template.yaml +1522 -0
- superqode/design_system.py +1080 -0
- superqode/dialogs/__init__.py +6 -0
- superqode/dialogs/base.py +39 -0
- superqode/dialogs/model.py +130 -0
- superqode/dialogs/provider.py +870 -0
- superqode/diff_view.py +919 -0
- superqode/enterprise.py +21 -0
- superqode/evaluation/__init__.py +25 -0
- superqode/evaluation/adapters.py +93 -0
- superqode/evaluation/behaviors.py +89 -0
- superqode/evaluation/engine.py +209 -0
- superqode/evaluation/scenarios.py +96 -0
- superqode/execution/__init__.py +36 -0
- superqode/execution/linter.py +538 -0
- superqode/execution/modes.py +347 -0
- superqode/execution/resolver.py +283 -0
- superqode/execution/runner.py +642 -0
- superqode/file_explorer.py +811 -0
- superqode/file_viewer.py +471 -0
- superqode/flash.py +183 -0
- superqode/guidance/__init__.py +58 -0
- superqode/guidance/config.py +203 -0
- superqode/guidance/prompts.py +71 -0
- superqode/harness/__init__.py +54 -0
- superqode/harness/accelerator.py +291 -0
- superqode/harness/config.py +319 -0
- superqode/harness/validator.py +147 -0
- superqode/history.py +279 -0
- superqode/integrations/superopt_runner.py +124 -0
- superqode/logging/__init__.py +49 -0
- superqode/logging/adapters.py +219 -0
- superqode/logging/formatter.py +923 -0
- superqode/logging/integration.py +341 -0
- superqode/logging/sinks.py +170 -0
- superqode/logging/unified_log.py +417 -0
- superqode/lsp/__init__.py +26 -0
- superqode/lsp/client.py +544 -0
- superqode/main.py +1069 -0
- superqode/mcp/__init__.py +89 -0
- superqode/mcp/auth_storage.py +380 -0
- superqode/mcp/client.py +1236 -0
- superqode/mcp/config.py +319 -0
- superqode/mcp/integration.py +337 -0
- superqode/mcp/oauth.py +436 -0
- superqode/mcp/oauth_callback.py +385 -0
- superqode/mcp/types.py +290 -0
- superqode/memory/__init__.py +31 -0
- superqode/memory/feedback.py +342 -0
- superqode/memory/store.py +522 -0
- superqode/notifications.py +369 -0
- superqode/optimization/__init__.py +5 -0
- superqode/optimization/config.py +33 -0
- superqode/permissions/__init__.py +25 -0
- superqode/permissions/rules.py +488 -0
- superqode/plan.py +323 -0
- superqode/providers/__init__.py +33 -0
- superqode/providers/gateway/__init__.py +165 -0
- superqode/providers/gateway/base.py +228 -0
- superqode/providers/gateway/litellm_gateway.py +1170 -0
- superqode/providers/gateway/openresponses_gateway.py +436 -0
- superqode/providers/health.py +297 -0
- superqode/providers/huggingface/__init__.py +74 -0
- superqode/providers/huggingface/downloader.py +472 -0
- superqode/providers/huggingface/endpoints.py +442 -0
- superqode/providers/huggingface/hub.py +531 -0
- superqode/providers/huggingface/inference.py +394 -0
- superqode/providers/huggingface/transformers_runner.py +516 -0
- superqode/providers/local/__init__.py +100 -0
- superqode/providers/local/base.py +438 -0
- superqode/providers/local/discovery.py +418 -0
- superqode/providers/local/lmstudio.py +256 -0
- superqode/providers/local/mlx.py +457 -0
- superqode/providers/local/ollama.py +486 -0
- superqode/providers/local/sglang.py +268 -0
- superqode/providers/local/tgi.py +260 -0
- superqode/providers/local/tool_support.py +477 -0
- superqode/providers/local/vllm.py +258 -0
- superqode/providers/manager.py +1338 -0
- superqode/providers/models.py +1016 -0
- superqode/providers/models_dev.py +578 -0
- superqode/providers/openresponses/__init__.py +87 -0
- superqode/providers/openresponses/converters/__init__.py +17 -0
- superqode/providers/openresponses/converters/messages.py +343 -0
- superqode/providers/openresponses/converters/tools.py +268 -0
- superqode/providers/openresponses/schema/__init__.py +56 -0
- superqode/providers/openresponses/schema/models.py +585 -0
- superqode/providers/openresponses/streaming/__init__.py +5 -0
- superqode/providers/openresponses/streaming/parser.py +338 -0
- superqode/providers/openresponses/tools/__init__.py +21 -0
- superqode/providers/openresponses/tools/apply_patch.py +352 -0
- superqode/providers/openresponses/tools/code_interpreter.py +290 -0
- superqode/providers/openresponses/tools/file_search.py +333 -0
- superqode/providers/openresponses/tools/mcp_adapter.py +252 -0
- superqode/providers/registry.py +716 -0
- superqode/providers/usage.py +332 -0
- superqode/pure_mode.py +384 -0
- superqode/qr/__init__.py +23 -0
- superqode/qr/dashboard.py +781 -0
- superqode/qr/generator.py +1018 -0
- superqode/qr/templates.py +135 -0
- superqode/safety/__init__.py +41 -0
- superqode/safety/sandbox.py +413 -0
- superqode/safety/warnings.py +256 -0
- superqode/server/__init__.py +33 -0
- superqode/server/lsp_server.py +775 -0
- superqode/server/web.py +250 -0
- superqode/session/__init__.py +25 -0
- superqode/session/persistence.py +580 -0
- superqode/session/sharing.py +477 -0
- superqode/session.py +475 -0
- superqode/sidebar.py +2991 -0
- superqode/stream_view.py +648 -0
- superqode/styles/__init__.py +3 -0
- superqode/superqe/__init__.py +184 -0
- superqode/superqe/acp_runner.py +1064 -0
- superqode/superqe/constitution/__init__.py +62 -0
- superqode/superqe/constitution/evaluator.py +308 -0
- superqode/superqe/constitution/loader.py +432 -0
- superqode/superqe/constitution/schema.py +250 -0
- superqode/superqe/events.py +591 -0
- superqode/superqe/frameworks/__init__.py +65 -0
- superqode/superqe/frameworks/base.py +234 -0
- superqode/superqe/frameworks/e2e.py +263 -0
- superqode/superqe/frameworks/executor.py +237 -0
- superqode/superqe/frameworks/javascript.py +409 -0
- superqode/superqe/frameworks/python.py +373 -0
- superqode/superqe/frameworks/registry.py +92 -0
- superqode/superqe/mcp_tools/__init__.py +47 -0
- superqode/superqe/mcp_tools/core_tools.py +418 -0
- superqode/superqe/mcp_tools/registry.py +230 -0
- superqode/superqe/mcp_tools/testing_tools.py +167 -0
- superqode/superqe/noise.py +89 -0
- superqode/superqe/orchestrator.py +778 -0
- superqode/superqe/roles.py +609 -0
- superqode/superqe/session.py +713 -0
- superqode/superqe/skills/__init__.py +57 -0
- superqode/superqe/skills/base.py +106 -0
- superqode/superqe/skills/core_skills.py +899 -0
- superqode/superqe/skills/registry.py +90 -0
- superqode/superqe/verifier.py +101 -0
- superqode/superqe_cli.py +76 -0
- superqode/tool_call.py +358 -0
- superqode/tools/__init__.py +93 -0
- superqode/tools/agent_tools.py +496 -0
- superqode/tools/base.py +324 -0
- superqode/tools/batch_tool.py +133 -0
- superqode/tools/diagnostics.py +311 -0
- superqode/tools/edit_tools.py +653 -0
- superqode/tools/enhanced_base.py +515 -0
- superqode/tools/file_tools.py +269 -0
- superqode/tools/file_tracking.py +45 -0
- superqode/tools/lsp_tools.py +610 -0
- superqode/tools/network_tools.py +350 -0
- superqode/tools/permissions.py +400 -0
- superqode/tools/question_tool.py +324 -0
- superqode/tools/search_tools.py +598 -0
- superqode/tools/shell_tools.py +259 -0
- superqode/tools/todo_tools.py +121 -0
- superqode/tools/validation.py +80 -0
- superqode/tools/web_tools.py +639 -0
- superqode/tui.py +1152 -0
- superqode/tui_integration.py +875 -0
- superqode/tui_widgets/__init__.py +27 -0
- superqode/tui_widgets/widgets/__init__.py +18 -0
- superqode/tui_widgets/widgets/progress.py +185 -0
- superqode/tui_widgets/widgets/tool_display.py +188 -0
- superqode/undo_manager.py +574 -0
- superqode/utils/__init__.py +5 -0
- superqode/utils/error_handling.py +323 -0
- superqode/utils/fuzzy.py +257 -0
- superqode/widgets/__init__.py +477 -0
- superqode/widgets/agent_collab.py +390 -0
- superqode/widgets/agent_store.py +936 -0
- superqode/widgets/agent_switcher.py +395 -0
- superqode/widgets/animation_manager.py +284 -0
- superqode/widgets/code_context.py +356 -0
- superqode/widgets/command_palette.py +412 -0
- superqode/widgets/connection_status.py +537 -0
- superqode/widgets/conversation_history.py +470 -0
- superqode/widgets/diff_indicator.py +155 -0
- superqode/widgets/enhanced_status_bar.py +385 -0
- superqode/widgets/enhanced_toast.py +476 -0
- superqode/widgets/file_browser.py +809 -0
- superqode/widgets/file_reference.py +585 -0
- superqode/widgets/issue_timeline.py +340 -0
- superqode/widgets/leader_key.py +264 -0
- superqode/widgets/mode_switcher.py +445 -0
- superqode/widgets/model_picker.py +234 -0
- superqode/widgets/permission_preview.py +1205 -0
- superqode/widgets/prompt.py +358 -0
- superqode/widgets/provider_connect.py +725 -0
- superqode/widgets/pty_shell.py +587 -0
- superqode/widgets/qe_dashboard.py +321 -0
- superqode/widgets/resizable_sidebar.py +377 -0
- superqode/widgets/response_changes.py +218 -0
- superqode/widgets/response_display.py +528 -0
- superqode/widgets/rich_tool_display.py +613 -0
- superqode/widgets/sidebar_panels.py +1180 -0
- superqode/widgets/slash_complete.py +356 -0
- superqode/widgets/split_view.py +612 -0
- superqode/widgets/status_bar.py +273 -0
- superqode/widgets/superqode_display.py +786 -0
- superqode/widgets/thinking_display.py +815 -0
- superqode/widgets/throbber.py +87 -0
- superqode/widgets/toast.py +206 -0
- superqode/widgets/unified_output.py +1073 -0
- superqode/workspace/__init__.py +75 -0
- superqode/workspace/artifacts.py +472 -0
- superqode/workspace/coordinator.py +353 -0
- superqode/workspace/diff_tracker.py +429 -0
- superqode/workspace/git_guard.py +373 -0
- superqode/workspace/git_snapshot.py +526 -0
- superqode/workspace/manager.py +750 -0
- superqode/workspace/snapshot.py +357 -0
- superqode/workspace/watcher.py +535 -0
- superqode/workspace/worktree.py +440 -0
- superqode-0.1.5.dist-info/METADATA +204 -0
- superqode-0.1.5.dist-info/RECORD +288 -0
- superqode-0.1.5.dist-info/WHEEL +5 -0
- superqode-0.1.5.dist-info/entry_points.txt +3 -0
- superqode-0.1.5.dist-info/licenses/LICENSE +648 -0
- 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
|