crackerjack 0.30.3__py3-none-any.whl โ 0.31.7__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.
Potentially problematic release.
This version of crackerjack might be problematic. Click here for more details.
- crackerjack/CLAUDE.md +1005 -0
- crackerjack/RULES.md +380 -0
- crackerjack/__init__.py +42 -13
- crackerjack/__main__.py +227 -299
- crackerjack/agents/__init__.py +41 -0
- crackerjack/agents/architect_agent.py +281 -0
- crackerjack/agents/base.py +170 -0
- crackerjack/agents/coordinator.py +512 -0
- crackerjack/agents/documentation_agent.py +498 -0
- crackerjack/agents/dry_agent.py +388 -0
- crackerjack/agents/formatting_agent.py +245 -0
- crackerjack/agents/import_optimization_agent.py +281 -0
- crackerjack/agents/performance_agent.py +669 -0
- crackerjack/agents/proactive_agent.py +104 -0
- crackerjack/agents/refactoring_agent.py +788 -0
- crackerjack/agents/security_agent.py +529 -0
- crackerjack/agents/test_creation_agent.py +657 -0
- crackerjack/agents/test_specialist_agent.py +486 -0
- crackerjack/agents/tracker.py +212 -0
- crackerjack/api.py +560 -0
- crackerjack/cli/__init__.py +24 -0
- crackerjack/cli/facade.py +104 -0
- crackerjack/cli/handlers.py +267 -0
- crackerjack/cli/interactive.py +471 -0
- crackerjack/cli/options.py +409 -0
- crackerjack/cli/utils.py +18 -0
- crackerjack/code_cleaner.py +618 -928
- crackerjack/config/__init__.py +19 -0
- crackerjack/config/hooks.py +218 -0
- crackerjack/core/__init__.py +0 -0
- crackerjack/core/async_workflow_orchestrator.py +406 -0
- crackerjack/core/autofix_coordinator.py +200 -0
- crackerjack/core/container.py +104 -0
- crackerjack/core/enhanced_container.py +542 -0
- crackerjack/core/performance.py +243 -0
- crackerjack/core/phase_coordinator.py +585 -0
- crackerjack/core/proactive_workflow.py +316 -0
- crackerjack/core/session_coordinator.py +289 -0
- crackerjack/core/workflow_orchestrator.py +826 -0
- crackerjack/dynamic_config.py +94 -103
- crackerjack/errors.py +263 -41
- crackerjack/executors/__init__.py +11 -0
- crackerjack/executors/async_hook_executor.py +431 -0
- crackerjack/executors/cached_hook_executor.py +242 -0
- crackerjack/executors/hook_executor.py +345 -0
- crackerjack/executors/individual_hook_executor.py +669 -0
- crackerjack/intelligence/__init__.py +44 -0
- crackerjack/intelligence/adaptive_learning.py +751 -0
- crackerjack/intelligence/agent_orchestrator.py +551 -0
- crackerjack/intelligence/agent_registry.py +414 -0
- crackerjack/intelligence/agent_selector.py +502 -0
- crackerjack/intelligence/integration.py +290 -0
- crackerjack/interactive.py +576 -315
- crackerjack/managers/__init__.py +11 -0
- crackerjack/managers/async_hook_manager.py +135 -0
- crackerjack/managers/hook_manager.py +137 -0
- crackerjack/managers/publish_manager.py +433 -0
- crackerjack/managers/test_command_builder.py +151 -0
- crackerjack/managers/test_executor.py +443 -0
- crackerjack/managers/test_manager.py +258 -0
- crackerjack/managers/test_manager_backup.py +1124 -0
- crackerjack/managers/test_progress.py +114 -0
- crackerjack/mcp/__init__.py +0 -0
- crackerjack/mcp/cache.py +336 -0
- crackerjack/mcp/client_runner.py +104 -0
- crackerjack/mcp/context.py +621 -0
- crackerjack/mcp/dashboard.py +636 -0
- crackerjack/mcp/enhanced_progress_monitor.py +479 -0
- crackerjack/mcp/file_monitor.py +336 -0
- crackerjack/mcp/progress_components.py +569 -0
- crackerjack/mcp/progress_monitor.py +949 -0
- crackerjack/mcp/rate_limiter.py +332 -0
- crackerjack/mcp/server.py +22 -0
- crackerjack/mcp/server_core.py +244 -0
- crackerjack/mcp/service_watchdog.py +501 -0
- crackerjack/mcp/state.py +395 -0
- crackerjack/mcp/task_manager.py +257 -0
- crackerjack/mcp/tools/__init__.py +17 -0
- crackerjack/mcp/tools/core_tools.py +249 -0
- crackerjack/mcp/tools/error_analyzer.py +308 -0
- crackerjack/mcp/tools/execution_tools.py +372 -0
- crackerjack/mcp/tools/execution_tools_backup.py +1097 -0
- crackerjack/mcp/tools/intelligence_tool_registry.py +80 -0
- crackerjack/mcp/tools/intelligence_tools.py +314 -0
- crackerjack/mcp/tools/monitoring_tools.py +502 -0
- crackerjack/mcp/tools/proactive_tools.py +384 -0
- crackerjack/mcp/tools/progress_tools.py +217 -0
- crackerjack/mcp/tools/utility_tools.py +341 -0
- crackerjack/mcp/tools/workflow_executor.py +565 -0
- crackerjack/mcp/websocket/__init__.py +14 -0
- crackerjack/mcp/websocket/app.py +39 -0
- crackerjack/mcp/websocket/endpoints.py +559 -0
- crackerjack/mcp/websocket/jobs.py +253 -0
- crackerjack/mcp/websocket/server.py +116 -0
- crackerjack/mcp/websocket/websocket_handler.py +78 -0
- crackerjack/mcp/websocket_server.py +10 -0
- crackerjack/models/__init__.py +31 -0
- crackerjack/models/config.py +93 -0
- crackerjack/models/config_adapter.py +230 -0
- crackerjack/models/protocols.py +118 -0
- crackerjack/models/task.py +154 -0
- crackerjack/monitoring/ai_agent_watchdog.py +450 -0
- crackerjack/monitoring/regression_prevention.py +638 -0
- crackerjack/orchestration/__init__.py +0 -0
- crackerjack/orchestration/advanced_orchestrator.py +970 -0
- crackerjack/orchestration/coverage_improvement.py +223 -0
- crackerjack/orchestration/execution_strategies.py +341 -0
- crackerjack/orchestration/test_progress_streamer.py +636 -0
- crackerjack/plugins/__init__.py +15 -0
- crackerjack/plugins/base.py +200 -0
- crackerjack/plugins/hooks.py +246 -0
- crackerjack/plugins/loader.py +335 -0
- crackerjack/plugins/managers.py +259 -0
- crackerjack/py313.py +8 -3
- crackerjack/services/__init__.py +22 -0
- crackerjack/services/cache.py +314 -0
- crackerjack/services/config.py +358 -0
- crackerjack/services/config_integrity.py +99 -0
- crackerjack/services/contextual_ai_assistant.py +516 -0
- crackerjack/services/coverage_ratchet.py +356 -0
- crackerjack/services/debug.py +736 -0
- crackerjack/services/dependency_monitor.py +617 -0
- crackerjack/services/enhanced_filesystem.py +439 -0
- crackerjack/services/file_hasher.py +151 -0
- crackerjack/services/filesystem.py +421 -0
- crackerjack/services/git.py +176 -0
- crackerjack/services/health_metrics.py +611 -0
- crackerjack/services/initialization.py +873 -0
- crackerjack/services/log_manager.py +286 -0
- crackerjack/services/logging.py +174 -0
- crackerjack/services/metrics.py +578 -0
- crackerjack/services/pattern_cache.py +362 -0
- crackerjack/services/pattern_detector.py +515 -0
- crackerjack/services/performance_benchmarks.py +653 -0
- crackerjack/services/security.py +163 -0
- crackerjack/services/server_manager.py +234 -0
- crackerjack/services/smart_scheduling.py +144 -0
- crackerjack/services/tool_version_service.py +61 -0
- crackerjack/services/unified_config.py +437 -0
- crackerjack/services/version_checker.py +248 -0
- crackerjack/slash_commands/__init__.py +14 -0
- crackerjack/slash_commands/init.md +122 -0
- crackerjack/slash_commands/run.md +163 -0
- crackerjack/slash_commands/status.md +127 -0
- crackerjack-0.31.7.dist-info/METADATA +742 -0
- crackerjack-0.31.7.dist-info/RECORD +149 -0
- crackerjack-0.31.7.dist-info/entry_points.txt +2 -0
- crackerjack/.gitignore +0 -34
- crackerjack/.libcst.codemod.yaml +0 -18
- crackerjack/.pdm.toml +0 -1
- crackerjack/crackerjack.py +0 -3805
- crackerjack/pyproject.toml +0 -286
- crackerjack-0.30.3.dist-info/METADATA +0 -1290
- crackerjack-0.30.3.dist-info/RECORD +0 -16
- {crackerjack-0.30.3.dist-info โ crackerjack-0.31.7.dist-info}/WHEEL +0 -0
- {crackerjack-0.30.3.dist-info โ crackerjack-0.31.7.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from .base import (
|
|
5
|
+
AgentContext,
|
|
6
|
+
FixResult,
|
|
7
|
+
Issue,
|
|
8
|
+
IssueType,
|
|
9
|
+
SubAgent,
|
|
10
|
+
agent_registry,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TestSpecialistAgent(SubAgent):
|
|
15
|
+
def __init__(self, context: AgentContext) -> None:
|
|
16
|
+
super().__init__(context)
|
|
17
|
+
self.common_test_patterns = {
|
|
18
|
+
"fixture_not_found": r"fixture '(\w+)' not found",
|
|
19
|
+
"import_error": r"ImportError|ModuleNotFoundError",
|
|
20
|
+
"assertion_error": r"AssertionError|assert .+ ==",
|
|
21
|
+
"attribute_error": r"AttributeError: .+ has no attribute",
|
|
22
|
+
"mock_spec_error": r"MockSpec|spec.*Mock",
|
|
23
|
+
"hardcoded_path": r"'/test/path'|/test/path",
|
|
24
|
+
"missing_import": r"name '(\w+)' is not defined",
|
|
25
|
+
"pydantic_validation": r"ValidationError|validation error",
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
def get_supported_types(self) -> set[IssueType]:
|
|
29
|
+
return {IssueType.TEST_FAILURE, IssueType.IMPORT_ERROR}
|
|
30
|
+
|
|
31
|
+
async def can_handle(self, issue: Issue) -> float:
|
|
32
|
+
if issue.type not in self.get_supported_types():
|
|
33
|
+
return 0.0
|
|
34
|
+
|
|
35
|
+
perfect_match_score = self._check_perfect_test_matches(issue.message)
|
|
36
|
+
if perfect_match_score > 0:
|
|
37
|
+
return perfect_match_score
|
|
38
|
+
|
|
39
|
+
pattern_score = self._check_test_patterns(issue.message)
|
|
40
|
+
if pattern_score > 0:
|
|
41
|
+
return pattern_score
|
|
42
|
+
|
|
43
|
+
file_score = self._check_test_file_path(issue.file_path)
|
|
44
|
+
if file_score > 0:
|
|
45
|
+
return file_score
|
|
46
|
+
|
|
47
|
+
return self._check_general_test_failure(issue.type)
|
|
48
|
+
|
|
49
|
+
def _check_perfect_test_matches(self, message: str) -> float:
|
|
50
|
+
message_lower = message.lower()
|
|
51
|
+
test_keywords = [
|
|
52
|
+
"failed test",
|
|
53
|
+
"test failed",
|
|
54
|
+
"pytest",
|
|
55
|
+
"fixture",
|
|
56
|
+
"assertion",
|
|
57
|
+
"mock",
|
|
58
|
+
"conftest",
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
1.0 if any(keyword in message_lower for keyword in test_keywords) else 0.0
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
def _check_test_patterns(self, message: str) -> float:
|
|
66
|
+
for pattern in self.common_test_patterns.values():
|
|
67
|
+
if re.search(pattern, message, re.IGNORECASE):
|
|
68
|
+
return 0.9
|
|
69
|
+
return 0.0
|
|
70
|
+
|
|
71
|
+
def _check_test_file_path(self, file_path: str | None) -> float:
|
|
72
|
+
if file_path and ("test_" in file_path or "/tests/" in file_path):
|
|
73
|
+
return 0.8
|
|
74
|
+
return 0.0
|
|
75
|
+
|
|
76
|
+
def _check_general_test_failure(self, issue_type: IssueType) -> float:
|
|
77
|
+
return 0.7 if issue_type == IssueType.TEST_FAILURE else 0.0
|
|
78
|
+
|
|
79
|
+
async def analyze_and_fix(self, issue: Issue) -> FixResult:
|
|
80
|
+
self.log(f"Analyzing test issue: {issue.message}")
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
fixes_applied, files_modified = await self._apply_issue_fixes(issue)
|
|
84
|
+
recommendations = self._get_failure_recommendations(fixes_applied)
|
|
85
|
+
|
|
86
|
+
return self._create_fix_result(
|
|
87
|
+
fixes_applied,
|
|
88
|
+
files_modified,
|
|
89
|
+
recommendations,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
except Exception as e:
|
|
93
|
+
self.log(f"Error fixing test issue: {e}", "ERROR")
|
|
94
|
+
return self._create_error_fix_result(e)
|
|
95
|
+
|
|
96
|
+
async def _apply_issue_fixes(self, issue: Issue) -> tuple[list[str], list[str]]:
|
|
97
|
+
fixes_applied: list[str] = []
|
|
98
|
+
files_modified: list[str] = []
|
|
99
|
+
|
|
100
|
+
failure_type = self._identify_failure_type(issue)
|
|
101
|
+
self.log(f"Identified failure type: {failure_type}")
|
|
102
|
+
|
|
103
|
+
targeted_fixes = await self._apply_targeted_fixes(failure_type, issue)
|
|
104
|
+
fixes_applied.extend(targeted_fixes)
|
|
105
|
+
|
|
106
|
+
file_fixes, file_modified = await self._apply_file_fixes(issue)
|
|
107
|
+
fixes_applied.extend(file_fixes)
|
|
108
|
+
if file_modified and issue.file_path:
|
|
109
|
+
files_modified.append(issue.file_path)
|
|
110
|
+
|
|
111
|
+
general_fixes = await self._apply_general_test_fixes()
|
|
112
|
+
fixes_applied.extend(general_fixes)
|
|
113
|
+
|
|
114
|
+
return fixes_applied, files_modified
|
|
115
|
+
|
|
116
|
+
async def _apply_targeted_fixes(self, failure_type: str, issue: Issue) -> list[str]:
|
|
117
|
+
if failure_type == "fixture_not_found":
|
|
118
|
+
return await self._fix_missing_fixtures(issue)
|
|
119
|
+
if failure_type == "import_error":
|
|
120
|
+
return await self._fix_import_errors(issue)
|
|
121
|
+
if failure_type == "hardcoded_path":
|
|
122
|
+
return await self._fix_hardcoded_paths(issue)
|
|
123
|
+
if failure_type == "mock_spec_error":
|
|
124
|
+
return await self._fix_mock_issues(issue)
|
|
125
|
+
if failure_type == "pydantic_validation":
|
|
126
|
+
return await self._fix_pydantic_issues(issue)
|
|
127
|
+
return []
|
|
128
|
+
|
|
129
|
+
async def _apply_file_fixes(self, issue: Issue) -> tuple[list[str], bool]:
|
|
130
|
+
if not issue.file_path or not (
|
|
131
|
+
"test_" in issue.file_path or "/tests/" in issue.file_path
|
|
132
|
+
):
|
|
133
|
+
return [], False
|
|
134
|
+
|
|
135
|
+
file_fixes = await self._fix_test_file_issues(issue.file_path)
|
|
136
|
+
return file_fixes, len(file_fixes) > 0
|
|
137
|
+
|
|
138
|
+
def _get_failure_recommendations(self, fixes_applied: list[str]) -> list[str]:
|
|
139
|
+
if fixes_applied:
|
|
140
|
+
return []
|
|
141
|
+
|
|
142
|
+
return [
|
|
143
|
+
"Check test file imports and fixture definitions",
|
|
144
|
+
"Verify mock objects are properly configured",
|
|
145
|
+
"Ensure test data paths use tmp_path fixture",
|
|
146
|
+
"Review assertion statements for correctness",
|
|
147
|
+
]
|
|
148
|
+
|
|
149
|
+
def _create_fix_result(
|
|
150
|
+
self,
|
|
151
|
+
fixes_applied: list[str],
|
|
152
|
+
files_modified: list[str],
|
|
153
|
+
recommendations: list[str],
|
|
154
|
+
) -> FixResult:
|
|
155
|
+
success = len(fixes_applied) > 0
|
|
156
|
+
confidence = 0.8 if success else 0.4
|
|
157
|
+
|
|
158
|
+
return FixResult(
|
|
159
|
+
success=success,
|
|
160
|
+
confidence=confidence,
|
|
161
|
+
fixes_applied=fixes_applied,
|
|
162
|
+
files_modified=files_modified,
|
|
163
|
+
recommendations=recommendations,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
def _create_error_fix_result(self, error: Exception) -> FixResult:
|
|
167
|
+
return FixResult(
|
|
168
|
+
success=False,
|
|
169
|
+
confidence=0.0,
|
|
170
|
+
remaining_issues=[f"Failed to fix test issue: {error}"],
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
def _identify_failure_type(self, issue: Issue) -> str:
|
|
174
|
+
message = issue.message
|
|
175
|
+
|
|
176
|
+
for pattern_name, pattern in self.common_test_patterns.items():
|
|
177
|
+
if re.search(pattern, message, re.IGNORECASE):
|
|
178
|
+
return pattern_name
|
|
179
|
+
|
|
180
|
+
return "unknown"
|
|
181
|
+
|
|
182
|
+
async def _fix_missing_fixtures(self, issue: Issue) -> list[str]:
|
|
183
|
+
fixes: list[str] = []
|
|
184
|
+
|
|
185
|
+
match = re.search(r"fixture '(\w+)' not found", issue.message)
|
|
186
|
+
if not match:
|
|
187
|
+
return fixes
|
|
188
|
+
|
|
189
|
+
fixture_name = match.group(1)
|
|
190
|
+
self.log(f"Attempting to fix missing fixture: {fixture_name}")
|
|
191
|
+
|
|
192
|
+
if fixture_name == "temp_pkg_path":
|
|
193
|
+
fixes.extend(await self._add_temp_pkg_path_fixture(issue.file_path))
|
|
194
|
+
elif fixture_name == "console":
|
|
195
|
+
fixes.extend(await self._add_console_fixture(issue.file_path))
|
|
196
|
+
elif fixture_name in ("tmp_path", "tmpdir"):
|
|
197
|
+
fixes.extend(await self._add_temp_path_fixture(issue.file_path))
|
|
198
|
+
|
|
199
|
+
return fixes
|
|
200
|
+
|
|
201
|
+
async def _fix_import_errors(self, issue: Issue) -> list[str]:
|
|
202
|
+
if not self._is_valid_file_path(issue.file_path) or issue.file_path is None:
|
|
203
|
+
return []
|
|
204
|
+
|
|
205
|
+
file_path = Path(issue.file_path)
|
|
206
|
+
content = self.context.get_file_content(file_path)
|
|
207
|
+
if not content:
|
|
208
|
+
return []
|
|
209
|
+
|
|
210
|
+
lines = content.split("\n")
|
|
211
|
+
fixes, modified = self._apply_import_fixes(lines, content, issue.file_path)
|
|
212
|
+
|
|
213
|
+
if modified:
|
|
214
|
+
self._save_import_fixes(file_path, lines, issue.file_path)
|
|
215
|
+
|
|
216
|
+
return fixes
|
|
217
|
+
|
|
218
|
+
def _is_valid_file_path(self, file_path: str | None) -> bool:
|
|
219
|
+
return file_path is not None and Path(file_path).exists()
|
|
220
|
+
|
|
221
|
+
def _apply_import_fixes(
|
|
222
|
+
self,
|
|
223
|
+
lines: list[str],
|
|
224
|
+
content: str,
|
|
225
|
+
file_path: str,
|
|
226
|
+
) -> tuple[list[str], bool]:
|
|
227
|
+
fixes: list[str] = []
|
|
228
|
+
modified = False
|
|
229
|
+
|
|
230
|
+
if self._needs_pytest_import(content):
|
|
231
|
+
self._add_pytest_import(lines)
|
|
232
|
+
fixes.append(f"Added missing pytest import to {file_path}")
|
|
233
|
+
modified = True
|
|
234
|
+
|
|
235
|
+
if self._needs_pathlib_import(content):
|
|
236
|
+
lines.insert(0, "from pathlib import Path")
|
|
237
|
+
fixes.append(f"Added missing pathlib import to {file_path}")
|
|
238
|
+
modified = True
|
|
239
|
+
|
|
240
|
+
if self._needs_mock_import(content):
|
|
241
|
+
lines.insert(0, "from unittest.mock import Mock")
|
|
242
|
+
fixes.append(f"Added missing Mock import to {file_path}")
|
|
243
|
+
modified = True
|
|
244
|
+
|
|
245
|
+
return fixes, modified
|
|
246
|
+
|
|
247
|
+
def _needs_pytest_import(self, content: str) -> bool:
|
|
248
|
+
return "pytest" not in content and "import pytest" not in content
|
|
249
|
+
|
|
250
|
+
def _needs_pathlib_import(self, content: str) -> bool:
|
|
251
|
+
return "Path(" in content and "from pathlib import Path" not in content
|
|
252
|
+
|
|
253
|
+
def _needs_mock_import(self, content: str) -> bool:
|
|
254
|
+
return "Mock()" in content and "from unittest.mock import Mock" not in content
|
|
255
|
+
|
|
256
|
+
def _add_pytest_import(self, lines: list[str]) -> None:
|
|
257
|
+
import_section_end = self._find_import_section_end(lines)
|
|
258
|
+
lines.insert(import_section_end, "import pytest")
|
|
259
|
+
|
|
260
|
+
def _find_import_section_end(self, lines: list[str]) -> int:
|
|
261
|
+
import_section_end = 0
|
|
262
|
+
for i, line in enumerate(lines):
|
|
263
|
+
if line.strip().startswith(("import ", "from ")):
|
|
264
|
+
import_section_end = i + 1
|
|
265
|
+
elif line.strip() == "" and import_section_end > 0:
|
|
266
|
+
break
|
|
267
|
+
return import_section_end
|
|
268
|
+
|
|
269
|
+
def _save_import_fixes(
|
|
270
|
+
self,
|
|
271
|
+
file_path: Path,
|
|
272
|
+
lines: list[str],
|
|
273
|
+
file_path_str: str,
|
|
274
|
+
) -> None:
|
|
275
|
+
if self.context.write_file_content(file_path, "\n".join(lines)):
|
|
276
|
+
self.log(f"Fixed imports in {file_path_str}")
|
|
277
|
+
|
|
278
|
+
async def _fix_hardcoded_paths(self, issue: Issue) -> list[str]:
|
|
279
|
+
fixes: list[str] = []
|
|
280
|
+
|
|
281
|
+
if not issue.file_path:
|
|
282
|
+
return fixes
|
|
283
|
+
|
|
284
|
+
file_path = Path(issue.file_path)
|
|
285
|
+
content = self.context.get_file_content(file_path)
|
|
286
|
+
if not content:
|
|
287
|
+
return fixes
|
|
288
|
+
|
|
289
|
+
original_content = content
|
|
290
|
+
|
|
291
|
+
content = (
|
|
292
|
+
content.replace('Path("/test/path")', "tmp_path")
|
|
293
|
+
.replace('"/test/path"', "str(tmp_path)")
|
|
294
|
+
.replace("'/test/path'", "str(tmp_path)")
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
if content != original_content:
|
|
298
|
+
if self.context.write_file_content(file_path, content):
|
|
299
|
+
fixes.append(f"Fixed hardcoded paths in {issue.file_path}")
|
|
300
|
+
self.log(f"Fixed hardcoded paths in {issue.file_path}")
|
|
301
|
+
|
|
302
|
+
return fixes
|
|
303
|
+
|
|
304
|
+
async def _fix_mock_issues(self, issue: Issue) -> list[str]:
|
|
305
|
+
fixes: list[str] = []
|
|
306
|
+
|
|
307
|
+
if (
|
|
308
|
+
not self._is_valid_mock_issue_file(issue.file_path)
|
|
309
|
+
or issue.file_path is None
|
|
310
|
+
):
|
|
311
|
+
return fixes
|
|
312
|
+
|
|
313
|
+
file_path = Path(issue.file_path)
|
|
314
|
+
content = self.context.get_file_content(file_path)
|
|
315
|
+
if not content:
|
|
316
|
+
return fixes
|
|
317
|
+
|
|
318
|
+
original_content = content
|
|
319
|
+
content, mock_fixes = self._apply_mock_fixes_to_content(
|
|
320
|
+
content,
|
|
321
|
+
issue.file_path,
|
|
322
|
+
)
|
|
323
|
+
fixes.extend(mock_fixes)
|
|
324
|
+
|
|
325
|
+
if content != original_content:
|
|
326
|
+
self._save_mock_fixes(file_path, content, issue.file_path)
|
|
327
|
+
|
|
328
|
+
return fixes
|
|
329
|
+
|
|
330
|
+
def _is_valid_mock_issue_file(self, file_path: str | None) -> bool:
|
|
331
|
+
return file_path is not None
|
|
332
|
+
|
|
333
|
+
def _apply_mock_fixes_to_content(
|
|
334
|
+
self,
|
|
335
|
+
content: str,
|
|
336
|
+
file_path: str,
|
|
337
|
+
) -> tuple[str, list[str]]:
|
|
338
|
+
fixes: list[str] = []
|
|
339
|
+
|
|
340
|
+
if self._needs_console_mock_fix(content):
|
|
341
|
+
content = self._fix_console_mock_usage(content)
|
|
342
|
+
fixes.append(f"Fixed Mock usage in {file_path}")
|
|
343
|
+
|
|
344
|
+
return content, fixes
|
|
345
|
+
|
|
346
|
+
def _needs_console_mock_fix(self, content: str) -> bool:
|
|
347
|
+
return "console = Mock()" in content and "Console" not in content
|
|
348
|
+
|
|
349
|
+
def _fix_console_mock_usage(self, content: str) -> str:
|
|
350
|
+
content = content.replace("console = Mock()", "console = Console()")
|
|
351
|
+
return self._ensure_console_import(content)
|
|
352
|
+
|
|
353
|
+
def _ensure_console_import(self, content: str) -> str:
|
|
354
|
+
if "from rich.console import Console" in content:
|
|
355
|
+
return content
|
|
356
|
+
|
|
357
|
+
lines = content.split("\n")
|
|
358
|
+
lines = self._add_console_import_to_lines(lines)
|
|
359
|
+
return "\n".join(lines)
|
|
360
|
+
|
|
361
|
+
def _add_console_import_to_lines(self, lines: list[str]) -> list[str]:
|
|
362
|
+
for i, line in enumerate(lines):
|
|
363
|
+
if line.startswith("from rich") or "rich" in line:
|
|
364
|
+
lines.insert(i + 1, "from rich.console import Console")
|
|
365
|
+
return lines
|
|
366
|
+
|
|
367
|
+
lines.insert(0, "from rich.console import Console")
|
|
368
|
+
return lines
|
|
369
|
+
|
|
370
|
+
def _save_mock_fixes(
|
|
371
|
+
self,
|
|
372
|
+
file_path: Path,
|
|
373
|
+
content: str,
|
|
374
|
+
file_path_str: str,
|
|
375
|
+
) -> None:
|
|
376
|
+
if self.context.write_file_content(file_path, content):
|
|
377
|
+
self.log(f"Fixed Mock issues in {file_path_str}")
|
|
378
|
+
|
|
379
|
+
async def _fix_pydantic_issues(self, issue: Issue) -> list[str]:
|
|
380
|
+
return []
|
|
381
|
+
|
|
382
|
+
async def _add_temp_pkg_path_fixture(self, file_path: str | None) -> list[str]:
|
|
383
|
+
if not file_path:
|
|
384
|
+
return []
|
|
385
|
+
|
|
386
|
+
fixes: list[str] = []
|
|
387
|
+
path = Path(file_path)
|
|
388
|
+
content = self.context.get_file_content(path)
|
|
389
|
+
if not content:
|
|
390
|
+
return fixes
|
|
391
|
+
|
|
392
|
+
if "@pytest.fixture" in content and "temp_pkg_path" in content:
|
|
393
|
+
return fixes
|
|
394
|
+
|
|
395
|
+
fixture_code = '''
|
|
396
|
+
@pytest.fixture
|
|
397
|
+
def temp_pkg_path(tmp_path) -> Path:
|
|
398
|
+
"""Create temporary package path."""
|
|
399
|
+
return tmp_path
|
|
400
|
+
'''
|
|
401
|
+
|
|
402
|
+
lines = content.split("\n")
|
|
403
|
+
|
|
404
|
+
for i, line in enumerate(lines):
|
|
405
|
+
if line.startswith("class Test") and i > 0:
|
|
406
|
+
lines.insert(i + 1, fixture_code)
|
|
407
|
+
break
|
|
408
|
+
else:
|
|
409
|
+
lines.append(fixture_code)
|
|
410
|
+
|
|
411
|
+
if self.context.write_file_content(path, "\n".join(lines)):
|
|
412
|
+
fixes.append(f"Added temp_pkg_path fixture to {file_path}")
|
|
413
|
+
|
|
414
|
+
return fixes
|
|
415
|
+
|
|
416
|
+
async def _add_console_fixture(self, file_path: str | None) -> list[str]:
|
|
417
|
+
if not file_path:
|
|
418
|
+
return []
|
|
419
|
+
|
|
420
|
+
fixes: list[str] = []
|
|
421
|
+
path = Path(file_path)
|
|
422
|
+
content = self.context.get_file_content(path)
|
|
423
|
+
if not content:
|
|
424
|
+
return fixes
|
|
425
|
+
|
|
426
|
+
fixture_code = '''
|
|
427
|
+
@pytest.fixture
|
|
428
|
+
def console() -> Console:
|
|
429
|
+
"""Create console instance for testing."""
|
|
430
|
+
return Console()
|
|
431
|
+
'''
|
|
432
|
+
|
|
433
|
+
lines = content.split("\n")
|
|
434
|
+
lines.append(fixture_code)
|
|
435
|
+
|
|
436
|
+
if "from rich.console import Console" not in content:
|
|
437
|
+
lines.insert(0, "from rich.console import Console")
|
|
438
|
+
|
|
439
|
+
if self.context.write_file_content(path, "\n".join(lines)):
|
|
440
|
+
fixes.append(f"Added console fixture to {file_path}")
|
|
441
|
+
|
|
442
|
+
return fixes
|
|
443
|
+
|
|
444
|
+
async def _add_temp_path_fixture(self, file_path: str | None) -> list[str]:
|
|
445
|
+
return [
|
|
446
|
+
f"Note: tmp_path is a built-in pytest fixture - check fixture usage in {file_path}",
|
|
447
|
+
]
|
|
448
|
+
|
|
449
|
+
async def _fix_test_file_issues(self, file_path: str) -> list[str]:
|
|
450
|
+
fixes: list[str] = []
|
|
451
|
+
|
|
452
|
+
path = Path(file_path)
|
|
453
|
+
content = self.context.get_file_content(path)
|
|
454
|
+
if not content:
|
|
455
|
+
return fixes
|
|
456
|
+
|
|
457
|
+
original_content = content
|
|
458
|
+
|
|
459
|
+
if "import pytest" not in content:
|
|
460
|
+
lines = content.split("\n")
|
|
461
|
+
lines.insert(0, "import pytest")
|
|
462
|
+
content = "\n".join(lines)
|
|
463
|
+
fixes.append(f"Added pytest import to {file_path}")
|
|
464
|
+
|
|
465
|
+
content = re.sub(r"assert (.+) == (.+)", r"assert \1 == \2", content)
|
|
466
|
+
|
|
467
|
+
if content != original_content:
|
|
468
|
+
if self.context.write_file_content(path, content):
|
|
469
|
+
self.log(f"Applied general fixes to {file_path}")
|
|
470
|
+
|
|
471
|
+
return fixes
|
|
472
|
+
|
|
473
|
+
async def _apply_general_test_fixes(self) -> list[str]:
|
|
474
|
+
fixes: list[str] = []
|
|
475
|
+
|
|
476
|
+
returncode, _, stderr = await self.run_command(
|
|
477
|
+
["uv", "run", "python", "-m", "pytest", "--collect-only", "-q"],
|
|
478
|
+
)
|
|
479
|
+
|
|
480
|
+
if returncode != 0 and "ImportError" in stderr:
|
|
481
|
+
fixes.append("Identified import issues in test collection")
|
|
482
|
+
|
|
483
|
+
return fixes
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
agent_registry.register(TestSpecialistAgent)
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import typing as t
|
|
3
|
+
from collections import defaultdict
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from .base import FixResult, Issue
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class AgentActivity:
|
|
12
|
+
agent_type: str
|
|
13
|
+
confidence: float
|
|
14
|
+
status: str
|
|
15
|
+
current_issue: Issue | None = None
|
|
16
|
+
start_time: float = field(default_factory=time.time)
|
|
17
|
+
processing_time: float = 0.0
|
|
18
|
+
result: FixResult | None = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AgentTracker:
|
|
22
|
+
def __init__(self) -> None:
|
|
23
|
+
self.active_agents: dict[str, AgentActivity] = {}
|
|
24
|
+
self.completed_activities: list[AgentActivity] = []
|
|
25
|
+
self.performance_metrics: defaultdict[str, list[float]] = defaultdict(list)
|
|
26
|
+
self.cache_stats = {"hits": 0, "misses": 0}
|
|
27
|
+
self.total_issues_processed = 0
|
|
28
|
+
self.coordinator_status = "idle"
|
|
29
|
+
self.agent_registry: dict[str, t.Any] = {
|
|
30
|
+
"total_agents": 0,
|
|
31
|
+
"initialized_agents": 0,
|
|
32
|
+
"agent_types": [],
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
def set_coordinator_status(self, status: str) -> None:
|
|
36
|
+
self.coordinator_status = status
|
|
37
|
+
|
|
38
|
+
def register_agents(self, agent_types: list[str]) -> None:
|
|
39
|
+
self.agent_registry = {
|
|
40
|
+
"total_agents": len(agent_types),
|
|
41
|
+
"initialized_agents": len(agent_types),
|
|
42
|
+
"agent_types": agent_types,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
def track_agent_evaluation(
|
|
46
|
+
self,
|
|
47
|
+
agent_type: str,
|
|
48
|
+
issue: Issue,
|
|
49
|
+
confidence: float,
|
|
50
|
+
) -> None:
|
|
51
|
+
self.active_agents[agent_type] = AgentActivity(
|
|
52
|
+
agent_type=agent_type,
|
|
53
|
+
confidence=confidence,
|
|
54
|
+
status="evaluating",
|
|
55
|
+
current_issue=issue,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
def track_agent_processing(
|
|
59
|
+
self,
|
|
60
|
+
agent_type: str,
|
|
61
|
+
issue: Issue,
|
|
62
|
+
confidence: float,
|
|
63
|
+
) -> None:
|
|
64
|
+
if agent_type in self.active_agents:
|
|
65
|
+
activity = self.active_agents[agent_type]
|
|
66
|
+
activity.status = "processing"
|
|
67
|
+
activity.current_issue = issue
|
|
68
|
+
activity.confidence = confidence
|
|
69
|
+
else:
|
|
70
|
+
self.active_agents[agent_type] = AgentActivity(
|
|
71
|
+
agent_type=agent_type,
|
|
72
|
+
confidence=confidence,
|
|
73
|
+
status="processing",
|
|
74
|
+
current_issue=issue,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
def track_agent_complete(self, agent_type: str, result: FixResult) -> None:
|
|
78
|
+
if agent_type not in self.active_agents:
|
|
79
|
+
return
|
|
80
|
+
|
|
81
|
+
activity = self.active_agents[agent_type]
|
|
82
|
+
activity.status = "completed" if result.success else "failed"
|
|
83
|
+
activity.result = result
|
|
84
|
+
activity.processing_time = time.time() - activity.start_time
|
|
85
|
+
|
|
86
|
+
self.performance_metrics[agent_type].append(activity.processing_time)
|
|
87
|
+
self.total_issues_processed += 1
|
|
88
|
+
|
|
89
|
+
self.completed_activities.append(activity)
|
|
90
|
+
del self.active_agents[agent_type]
|
|
91
|
+
|
|
92
|
+
def track_cache_hit(self) -> None:
|
|
93
|
+
self.cache_stats["hits"] += 1
|
|
94
|
+
|
|
95
|
+
def track_cache_miss(self) -> None:
|
|
96
|
+
self.cache_stats["misses"] += 1
|
|
97
|
+
|
|
98
|
+
def get_status(self) -> dict[str, Any]:
|
|
99
|
+
active_agents: list[dict[str, Any]] = []
|
|
100
|
+
|
|
101
|
+
for agent_type, activity in self.active_agents.items():
|
|
102
|
+
agent_data: dict[str, Any] = {
|
|
103
|
+
"agent_type": agent_type,
|
|
104
|
+
"confidence": activity.confidence,
|
|
105
|
+
"status": activity.status,
|
|
106
|
+
"processing_time": time.time() - activity.start_time,
|
|
107
|
+
"start_time": activity.start_time,
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if activity.current_issue:
|
|
111
|
+
agent_data["current_issue"] = {
|
|
112
|
+
"type": activity.current_issue.type.value,
|
|
113
|
+
"message": activity.current_issue.message,
|
|
114
|
+
"priority": activity.current_issue.severity.value,
|
|
115
|
+
"file_path": activity.current_issue.file_path,
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
active_agents.append(agent_data)
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
"coordinator_status": self.coordinator_status,
|
|
122
|
+
"active_agents": active_agents,
|
|
123
|
+
"agent_registry": self.agent_registry.copy(),
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
def get_metrics(self) -> dict[str, Any]:
|
|
127
|
+
total_completed = len(self.completed_activities)
|
|
128
|
+
successful = sum(
|
|
129
|
+
1
|
|
130
|
+
for activity in self.completed_activities
|
|
131
|
+
if activity.result and activity.result.success
|
|
132
|
+
)
|
|
133
|
+
success_rate = successful / total_completed if total_completed > 0 else 0.0
|
|
134
|
+
|
|
135
|
+
all_times: list[float] = []
|
|
136
|
+
for times in self.performance_metrics.values():
|
|
137
|
+
all_times.extend(times)
|
|
138
|
+
|
|
139
|
+
avg_processing_time = sum(all_times) / len(all_times) if all_times else 0.0
|
|
140
|
+
|
|
141
|
+
total_requests = self.cache_stats["hits"] + self.cache_stats["misses"]
|
|
142
|
+
cache_hit_rate = (
|
|
143
|
+
self.cache_stats["hits"] / total_requests if total_requests > 0 else 0.0
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
"total_issues_processed": self.total_issues_processed,
|
|
148
|
+
"cache_hits": self.cache_stats["hits"],
|
|
149
|
+
"cache_misses": self.cache_stats["misses"],
|
|
150
|
+
"cache_hit_rate": cache_hit_rate,
|
|
151
|
+
"average_processing_time": avg_processing_time,
|
|
152
|
+
"success_rate": success_rate,
|
|
153
|
+
"completed_activities": total_completed,
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
def get_agent_summary(self) -> dict[str, Any]:
|
|
157
|
+
active_count = len(self.active_agents)
|
|
158
|
+
cache_hits = self.cache_stats["hits"]
|
|
159
|
+
|
|
160
|
+
active_summary: list[dict[str, Any]] = []
|
|
161
|
+
for agent_type, activity in self.active_agents.items():
|
|
162
|
+
emoji = self._get_agent_emoji(agent_type)
|
|
163
|
+
processing_time = time.time() - activity.start_time
|
|
164
|
+
|
|
165
|
+
active_summary.append(
|
|
166
|
+
{
|
|
167
|
+
"display": f"{emoji} {agent_type}: {activity.status.title()} ({processing_time:.1f}s)",
|
|
168
|
+
"agent_type": agent_type,
|
|
169
|
+
"status": activity.status,
|
|
170
|
+
"processing_time": processing_time,
|
|
171
|
+
},
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
"active_count": active_count,
|
|
176
|
+
"cached_fixes": cache_hits,
|
|
177
|
+
"active_agents": active_summary,
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
def _get_agent_emoji(self, agent_type: str) -> str:
|
|
181
|
+
return {
|
|
182
|
+
"FormattingAgent": "๐จ",
|
|
183
|
+
"SecurityAgent": "๐",
|
|
184
|
+
"TestSpecialistAgent": "๐งช",
|
|
185
|
+
"TestCreationAgent": "โ",
|
|
186
|
+
}.get(agent_type, "๐ค")
|
|
187
|
+
|
|
188
|
+
def reset(self) -> None:
|
|
189
|
+
self.active_agents.clear()
|
|
190
|
+
self.completed_activities.clear()
|
|
191
|
+
self.performance_metrics.clear()
|
|
192
|
+
self.cache_stats = {"hits": 0, "misses": 0}
|
|
193
|
+
self.total_issues_processed = 0
|
|
194
|
+
self.coordinator_status = "idle"
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
_global_tracker = None
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def get_agent_tracker() -> AgentTracker:
|
|
201
|
+
global _global_tracker
|
|
202
|
+
if _global_tracker is None:
|
|
203
|
+
_global_tracker = AgentTracker()
|
|
204
|
+
return _global_tracker
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def reset_agent_tracker() -> None:
|
|
208
|
+
global _global_tracker
|
|
209
|
+
if _global_tracker:
|
|
210
|
+
_global_tracker.reset()
|
|
211
|
+
else:
|
|
212
|
+
_global_tracker = AgentTracker()
|