crackerjack 0.29.0__py3-none-any.whl โ†’ 0.31.4__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.

Files changed (158) hide show
  1. crackerjack/CLAUDE.md +1005 -0
  2. crackerjack/RULES.md +380 -0
  3. crackerjack/__init__.py +42 -13
  4. crackerjack/__main__.py +225 -253
  5. crackerjack/agents/__init__.py +41 -0
  6. crackerjack/agents/architect_agent.py +281 -0
  7. crackerjack/agents/base.py +169 -0
  8. crackerjack/agents/coordinator.py +512 -0
  9. crackerjack/agents/documentation_agent.py +498 -0
  10. crackerjack/agents/dry_agent.py +388 -0
  11. crackerjack/agents/formatting_agent.py +245 -0
  12. crackerjack/agents/import_optimization_agent.py +281 -0
  13. crackerjack/agents/performance_agent.py +669 -0
  14. crackerjack/agents/proactive_agent.py +104 -0
  15. crackerjack/agents/refactoring_agent.py +788 -0
  16. crackerjack/agents/security_agent.py +529 -0
  17. crackerjack/agents/test_creation_agent.py +652 -0
  18. crackerjack/agents/test_specialist_agent.py +486 -0
  19. crackerjack/agents/tracker.py +212 -0
  20. crackerjack/api.py +560 -0
  21. crackerjack/cli/__init__.py +24 -0
  22. crackerjack/cli/facade.py +104 -0
  23. crackerjack/cli/handlers.py +267 -0
  24. crackerjack/cli/interactive.py +471 -0
  25. crackerjack/cli/options.py +401 -0
  26. crackerjack/cli/utils.py +18 -0
  27. crackerjack/code_cleaner.py +670 -0
  28. crackerjack/config/__init__.py +19 -0
  29. crackerjack/config/hooks.py +218 -0
  30. crackerjack/core/__init__.py +0 -0
  31. crackerjack/core/async_workflow_orchestrator.py +406 -0
  32. crackerjack/core/autofix_coordinator.py +200 -0
  33. crackerjack/core/container.py +104 -0
  34. crackerjack/core/enhanced_container.py +542 -0
  35. crackerjack/core/performance.py +243 -0
  36. crackerjack/core/phase_coordinator.py +561 -0
  37. crackerjack/core/proactive_workflow.py +316 -0
  38. crackerjack/core/session_coordinator.py +289 -0
  39. crackerjack/core/workflow_orchestrator.py +640 -0
  40. crackerjack/dynamic_config.py +577 -0
  41. crackerjack/errors.py +263 -41
  42. crackerjack/executors/__init__.py +11 -0
  43. crackerjack/executors/async_hook_executor.py +431 -0
  44. crackerjack/executors/cached_hook_executor.py +242 -0
  45. crackerjack/executors/hook_executor.py +345 -0
  46. crackerjack/executors/individual_hook_executor.py +669 -0
  47. crackerjack/intelligence/__init__.py +44 -0
  48. crackerjack/intelligence/adaptive_learning.py +751 -0
  49. crackerjack/intelligence/agent_orchestrator.py +551 -0
  50. crackerjack/intelligence/agent_registry.py +414 -0
  51. crackerjack/intelligence/agent_selector.py +502 -0
  52. crackerjack/intelligence/integration.py +290 -0
  53. crackerjack/interactive.py +576 -315
  54. crackerjack/managers/__init__.py +11 -0
  55. crackerjack/managers/async_hook_manager.py +135 -0
  56. crackerjack/managers/hook_manager.py +137 -0
  57. crackerjack/managers/publish_manager.py +411 -0
  58. crackerjack/managers/test_command_builder.py +151 -0
  59. crackerjack/managers/test_executor.py +435 -0
  60. crackerjack/managers/test_manager.py +258 -0
  61. crackerjack/managers/test_manager_backup.py +1124 -0
  62. crackerjack/managers/test_progress.py +144 -0
  63. crackerjack/mcp/__init__.py +0 -0
  64. crackerjack/mcp/cache.py +336 -0
  65. crackerjack/mcp/client_runner.py +104 -0
  66. crackerjack/mcp/context.py +615 -0
  67. crackerjack/mcp/dashboard.py +636 -0
  68. crackerjack/mcp/enhanced_progress_monitor.py +479 -0
  69. crackerjack/mcp/file_monitor.py +336 -0
  70. crackerjack/mcp/progress_components.py +569 -0
  71. crackerjack/mcp/progress_monitor.py +949 -0
  72. crackerjack/mcp/rate_limiter.py +332 -0
  73. crackerjack/mcp/server.py +22 -0
  74. crackerjack/mcp/server_core.py +244 -0
  75. crackerjack/mcp/service_watchdog.py +501 -0
  76. crackerjack/mcp/state.py +395 -0
  77. crackerjack/mcp/task_manager.py +257 -0
  78. crackerjack/mcp/tools/__init__.py +17 -0
  79. crackerjack/mcp/tools/core_tools.py +249 -0
  80. crackerjack/mcp/tools/error_analyzer.py +308 -0
  81. crackerjack/mcp/tools/execution_tools.py +370 -0
  82. crackerjack/mcp/tools/execution_tools_backup.py +1097 -0
  83. crackerjack/mcp/tools/intelligence_tool_registry.py +80 -0
  84. crackerjack/mcp/tools/intelligence_tools.py +314 -0
  85. crackerjack/mcp/tools/monitoring_tools.py +502 -0
  86. crackerjack/mcp/tools/proactive_tools.py +384 -0
  87. crackerjack/mcp/tools/progress_tools.py +141 -0
  88. crackerjack/mcp/tools/utility_tools.py +341 -0
  89. crackerjack/mcp/tools/workflow_executor.py +360 -0
  90. crackerjack/mcp/websocket/__init__.py +14 -0
  91. crackerjack/mcp/websocket/app.py +39 -0
  92. crackerjack/mcp/websocket/endpoints.py +559 -0
  93. crackerjack/mcp/websocket/jobs.py +253 -0
  94. crackerjack/mcp/websocket/server.py +116 -0
  95. crackerjack/mcp/websocket/websocket_handler.py +78 -0
  96. crackerjack/mcp/websocket_server.py +10 -0
  97. crackerjack/models/__init__.py +31 -0
  98. crackerjack/models/config.py +93 -0
  99. crackerjack/models/config_adapter.py +230 -0
  100. crackerjack/models/protocols.py +118 -0
  101. crackerjack/models/task.py +154 -0
  102. crackerjack/monitoring/ai_agent_watchdog.py +450 -0
  103. crackerjack/monitoring/regression_prevention.py +638 -0
  104. crackerjack/orchestration/__init__.py +0 -0
  105. crackerjack/orchestration/advanced_orchestrator.py +970 -0
  106. crackerjack/orchestration/execution_strategies.py +341 -0
  107. crackerjack/orchestration/test_progress_streamer.py +636 -0
  108. crackerjack/plugins/__init__.py +15 -0
  109. crackerjack/plugins/base.py +200 -0
  110. crackerjack/plugins/hooks.py +246 -0
  111. crackerjack/plugins/loader.py +335 -0
  112. crackerjack/plugins/managers.py +259 -0
  113. crackerjack/py313.py +8 -3
  114. crackerjack/services/__init__.py +22 -0
  115. crackerjack/services/cache.py +314 -0
  116. crackerjack/services/config.py +347 -0
  117. crackerjack/services/config_integrity.py +99 -0
  118. crackerjack/services/contextual_ai_assistant.py +516 -0
  119. crackerjack/services/coverage_ratchet.py +347 -0
  120. crackerjack/services/debug.py +736 -0
  121. crackerjack/services/dependency_monitor.py +617 -0
  122. crackerjack/services/enhanced_filesystem.py +439 -0
  123. crackerjack/services/file_hasher.py +151 -0
  124. crackerjack/services/filesystem.py +395 -0
  125. crackerjack/services/git.py +165 -0
  126. crackerjack/services/health_metrics.py +611 -0
  127. crackerjack/services/initialization.py +847 -0
  128. crackerjack/services/log_manager.py +286 -0
  129. crackerjack/services/logging.py +174 -0
  130. crackerjack/services/metrics.py +578 -0
  131. crackerjack/services/pattern_cache.py +362 -0
  132. crackerjack/services/pattern_detector.py +515 -0
  133. crackerjack/services/performance_benchmarks.py +653 -0
  134. crackerjack/services/security.py +163 -0
  135. crackerjack/services/server_manager.py +234 -0
  136. crackerjack/services/smart_scheduling.py +144 -0
  137. crackerjack/services/tool_version_service.py +61 -0
  138. crackerjack/services/unified_config.py +437 -0
  139. crackerjack/services/version_checker.py +248 -0
  140. crackerjack/slash_commands/__init__.py +14 -0
  141. crackerjack/slash_commands/init.md +122 -0
  142. crackerjack/slash_commands/run.md +163 -0
  143. crackerjack/slash_commands/status.md +127 -0
  144. crackerjack-0.31.4.dist-info/METADATA +742 -0
  145. crackerjack-0.31.4.dist-info/RECORD +148 -0
  146. crackerjack-0.31.4.dist-info/entry_points.txt +2 -0
  147. crackerjack/.gitignore +0 -34
  148. crackerjack/.libcst.codemod.yaml +0 -18
  149. crackerjack/.pdm.toml +0 -1
  150. crackerjack/.pre-commit-config-ai.yaml +0 -149
  151. crackerjack/.pre-commit-config-fast.yaml +0 -69
  152. crackerjack/.pre-commit-config.yaml +0 -114
  153. crackerjack/crackerjack.py +0 -4140
  154. crackerjack/pyproject.toml +0 -285
  155. crackerjack-0.29.0.dist-info/METADATA +0 -1289
  156. crackerjack-0.29.0.dist-info/RECORD +0 -17
  157. {crackerjack-0.29.0.dist-info โ†’ crackerjack-0.31.4.dist-info}/WHEEL +0 -0
  158. {crackerjack-0.29.0.dist-info โ†’ crackerjack-0.31.4.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()