devsquad 3.6.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. devsquad-3.6.0.dist-info/METADATA +944 -0
  2. devsquad-3.6.0.dist-info/RECORD +95 -0
  3. devsquad-3.6.0.dist-info/WHEEL +5 -0
  4. devsquad-3.6.0.dist-info/entry_points.txt +2 -0
  5. devsquad-3.6.0.dist-info/licenses/LICENSE +21 -0
  6. devsquad-3.6.0.dist-info/top_level.txt +2 -0
  7. scripts/__init__.py +0 -0
  8. scripts/ai_semantic_matcher.py +512 -0
  9. scripts/alert_manager.py +505 -0
  10. scripts/api/__init__.py +43 -0
  11. scripts/api/models.py +386 -0
  12. scripts/api/routes/__init__.py +20 -0
  13. scripts/api/routes/dispatch.py +348 -0
  14. scripts/api/routes/lifecycle.py +330 -0
  15. scripts/api/routes/metrics_gates.py +347 -0
  16. scripts/api_server.py +318 -0
  17. scripts/auth.py +451 -0
  18. scripts/cli/__init__.py +1 -0
  19. scripts/cli/cli_visual.py +642 -0
  20. scripts/cli.py +1094 -0
  21. scripts/collaboration/__init__.py +212 -0
  22. scripts/collaboration/_version.py +1 -0
  23. scripts/collaboration/agent_briefing.py +656 -0
  24. scripts/collaboration/ai_semantic_matcher.py +260 -0
  25. scripts/collaboration/anchor_checker.py +281 -0
  26. scripts/collaboration/anti_rationalization.py +470 -0
  27. scripts/collaboration/async_integration_example.py +255 -0
  28. scripts/collaboration/batch_scheduler.py +149 -0
  29. scripts/collaboration/checkpoint_manager.py +561 -0
  30. scripts/collaboration/ci_feedback_adapter.py +351 -0
  31. scripts/collaboration/code_map_generator.py +247 -0
  32. scripts/collaboration/concern_pack_loader.py +352 -0
  33. scripts/collaboration/confidence_score.py +496 -0
  34. scripts/collaboration/config_loader.py +188 -0
  35. scripts/collaboration/consensus.py +244 -0
  36. scripts/collaboration/context_compressor.py +533 -0
  37. scripts/collaboration/coordinator.py +668 -0
  38. scripts/collaboration/dispatcher.py +1636 -0
  39. scripts/collaboration/dual_layer_context.py +128 -0
  40. scripts/collaboration/enhanced_worker.py +539 -0
  41. scripts/collaboration/feature_usage_tracker.py +206 -0
  42. scripts/collaboration/five_axis_consensus.py +334 -0
  43. scripts/collaboration/input_validator.py +401 -0
  44. scripts/collaboration/integration_example.py +287 -0
  45. scripts/collaboration/intent_workflow_mapper.py +350 -0
  46. scripts/collaboration/language_parsers.py +269 -0
  47. scripts/collaboration/lifecycle_protocol.py +1446 -0
  48. scripts/collaboration/llm_backend.py +453 -0
  49. scripts/collaboration/llm_cache.py +448 -0
  50. scripts/collaboration/llm_cache_async.py +347 -0
  51. scripts/collaboration/llm_retry.py +387 -0
  52. scripts/collaboration/llm_retry_async.py +389 -0
  53. scripts/collaboration/mce_adapter.py +597 -0
  54. scripts/collaboration/memory_bridge.py +1607 -0
  55. scripts/collaboration/models.py +537 -0
  56. scripts/collaboration/null_providers.py +297 -0
  57. scripts/collaboration/operation_classifier.py +289 -0
  58. scripts/collaboration/output_slicer.py +225 -0
  59. scripts/collaboration/performance_monitor.py +462 -0
  60. scripts/collaboration/permission_guard.py +865 -0
  61. scripts/collaboration/prompt_assembler.py +756 -0
  62. scripts/collaboration/prompt_variant_generator.py +483 -0
  63. scripts/collaboration/protocols.py +267 -0
  64. scripts/collaboration/report_formatter.py +352 -0
  65. scripts/collaboration/retrospective.py +279 -0
  66. scripts/collaboration/role_matcher.py +92 -0
  67. scripts/collaboration/role_template_market.py +352 -0
  68. scripts/collaboration/rule_collector.py +678 -0
  69. scripts/collaboration/scratchpad.py +346 -0
  70. scripts/collaboration/skill_registry.py +151 -0
  71. scripts/collaboration/skillifier.py +878 -0
  72. scripts/collaboration/standardized_role_template.py +317 -0
  73. scripts/collaboration/task_completion_checker.py +237 -0
  74. scripts/collaboration/test_quality_guard.py +695 -0
  75. scripts/collaboration/unified_gate_engine.py +598 -0
  76. scripts/collaboration/usage_tracker.py +309 -0
  77. scripts/collaboration/user_friendly_error.py +176 -0
  78. scripts/collaboration/verification_gate.py +312 -0
  79. scripts/collaboration/warmup_manager.py +635 -0
  80. scripts/collaboration/worker.py +513 -0
  81. scripts/collaboration/workflow_engine.py +684 -0
  82. scripts/dashboard.py +1088 -0
  83. scripts/generate_benchmark_report.py +786 -0
  84. scripts/history_manager.py +604 -0
  85. scripts/mcp_server.py +289 -0
  86. skills/__init__.py +32 -0
  87. skills/dispatch/handler.py +52 -0
  88. skills/intent/handler.py +59 -0
  89. skills/registry.py +67 -0
  90. skills/retrospective/__init__.py +0 -0
  91. skills/retrospective/handler.py +125 -0
  92. skills/review/handler.py +356 -0
  93. skills/security/handler.py +454 -0
  94. skills/test/__init__.py +0 -0
  95. skills/test/handler.py +78 -0
@@ -0,0 +1,695 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ TestQualityGuard - 测试质量守卫
5
+
6
+ 解决 AI 测试的三大顽疾:
7
+ 1) 不看 API 文档凭空写 → API 签名校验
8
+ 2) 为通过而改测试 → 测试目的声明 + 失败报告生成
9
+ 3) 覆盖率低缺维度 → 多维度覆盖强制检查
10
+
11
+ 核心能力:
12
+ - APISignatureValidator: 校验测试代码是否与实际 API 签名一致
13
+ - TestPurposeRegistry: 强制每个 test_ 函数声明验证目的
14
+ - CoverageDimensionChecker: 检查 happy/error/perf/config 四维度覆盖
15
+ - TestQualityReport: 生成质量报告(而非简单 pass/fail)
16
+ - AntiPatternDetector: 检测 "为通过而改" 的反模式
17
+
18
+ 使用示例:
19
+ from scripts.collaboration.test_quality_guard import TestQualityGuard
20
+
21
+ guard = TestQualityGuard(module_path="scripts/collaboration/coordinator.py",
22
+ test_path="scripts/collaboration/coordinator_test.py")
23
+ report = guard.audit()
24
+ print(report.to_markdown())
25
+ """
26
+
27
+ import os
28
+ import re
29
+ import ast
30
+ import time
31
+ import inspect
32
+ import textwrap
33
+ import importlib.util
34
+ from pathlib import Path
35
+ from dataclasses import dataclass, field
36
+ from typing import Dict, List, Optional, Any, Tuple, Set
37
+ from enum import Enum
38
+ from datetime import datetime
39
+
40
+
41
+ class Severity(Enum):
42
+ CRITICAL = "critical"
43
+ MAJOR = "major"
44
+ MINOR = "minor"
45
+ INFO = "info"
46
+ SUGGESTION = "suggestion"
47
+
48
+
49
+ class TestDimension(Enum):
50
+ HAPPY_PATH = "happy_path"
51
+ ERROR_CASE = "error_case"
52
+ BOUNDARY = "boundary"
53
+ PERFORMANCE = "performance"
54
+ CONFIGURATION = "configuration"
55
+ INTEGRATION = "integration"
56
+ SECURITY = "security"
57
+
58
+
59
+ TestDimension.__test__ = False # Tell pytest this is not a test class
60
+
61
+
62
+ @dataclass
63
+ class QualityIssue:
64
+ id: str
65
+ severity: Severity
66
+ category: str
67
+ message: str
68
+ file: str
69
+ line: int = 0
70
+ suggestion: str = ""
71
+ auto_fixable: bool = False
72
+
73
+ def to_dict(self) -> Dict[str, Any]:
74
+ return {
75
+ "id": self.id,
76
+ "severity": self.severity.value,
77
+ "category": self.category,
78
+ "message": self.message,
79
+ "file": self.file,
80
+ "line": self.line,
81
+ "suggestion": self.suggestion,
82
+ "auto_fixable": self.auto_fixable,
83
+ }
84
+
85
+
86
+ @dataclass
87
+ class TestFunctionMeta:
88
+ name: str
89
+ line: int
90
+ has_purpose: bool = False
91
+ purpose_text: str = ""
92
+ dimension: Optional[TestDimension] = None
93
+ assert_count: int = 0
94
+ assert_types: List[str] = field(default_factory=list)
95
+ has_error_test: bool = False
96
+ has_performance_check: bool = False
97
+ docstring: str = ""
98
+
99
+
100
+ TestFunctionMeta.__test__ = False # Tell pytest this is not a test class
101
+
102
+
103
+ @dataclass
104
+ class APISignature:
105
+ name: str
106
+ kind: str
107
+ params: List[Dict[str, str]] = field(default_factory=list)
108
+ return_type: str = ""
109
+ file: str = ""
110
+ line: int = 0
111
+
112
+
113
+ @dataclass
114
+ class QualityScore:
115
+ api_compliance: float = 0.0
116
+ purpose_coverage: float = 0.0
117
+ dimension_balance: float = 0.0
118
+ anti_pattern_free: float = 0.0
119
+ overall: float = 0.0
120
+
121
+ def to_dict(self) -> Dict[str, Any]:
122
+ return {
123
+ "api_compliance": round(self.api_compliance, 2),
124
+ "purpose_coverage": round(self.purpose_coverage, 2),
125
+ "dimension_balance": round(self.dimension_balance, 2),
126
+ "anti_pattern_free": round(self.anti_pattern_free, 2),
127
+ "overall": round(self.overall, 2),
128
+ }
129
+
130
+
131
+ @dataclass
132
+ class TestQualityReport:
133
+ module_name: str
134
+ test_file: str
135
+ source_file: str
136
+ total_tests: int = 0
137
+ issues: List[QualityIssue] = field(default_factory=list)
138
+ test_functions: List[TestFunctionMeta] = field(default_factory=list)
139
+ api_signatures: List[APISignature] = field(default_factory=list)
140
+ score: QualityScore = field(default_factory=QualityScore)
141
+ audit_time: float = 0.0
142
+ timestamp: str = ""
143
+
144
+ @property
145
+ def critical_count(self) -> int:
146
+ return sum(1 for i in self.issues if i.severity == Severity.CRITICAL)
147
+
148
+ @property
149
+ def major_count(self) -> int:
150
+ return sum(1 for i in self.issues if i.severity == Severity.MAJOR)
151
+
152
+ @property
153
+ def minor_count(self) -> int:
154
+ return sum(1 for i in self.issues if i.severity == Severity.MINOR)
155
+
156
+ def to_dict(self) -> Dict[str, Any]:
157
+ return {
158
+ "module_name": self.module_name,
159
+ "test_file": self.test_file,
160
+ "source_file": self.source_file,
161
+ "total_tests": self.total_tests,
162
+ "issue_count": len(self.issues),
163
+ "critical": self.critical_count,
164
+ "major": self.major_count,
165
+ "minor": self.minor_count,
166
+ "score": self.score.to_dict(),
167
+ "audit_time_s": round(self.audit_time, 3),
168
+ "timestamp": self.timestamp,
169
+ }
170
+
171
+ def to_markdown(self) -> str:
172
+ lines = [
173
+ f"# TestQualityGuard 审计报告",
174
+ "",
175
+ f"**模块**: {self.module_name}",
176
+ f"**测试文件**: {self.test_file}",
177
+ f"**源文件**: {self.source_file}",
178
+ f"**审计时间**: {self.timestamp}",
179
+ f"**总耗时**: {self.audit_time:.3f}s",
180
+ "",
181
+ "## 评分总览",
182
+ "",
183
+ f"| 维度 | 得分 | 说明 |",
184
+ f"|------|------|------|",
185
+ f"| API 合规性 | {self.score.api_compliance:.0%} | 测试调用是否匹配实际 API 签名 |",
186
+ f"| 目的声明率 | {self.score.purpose_coverage:.0%} | 测试函数是否声明了验证目的 |",
187
+ f"| 维度平衡度 | {self.score.dimension_balance:.0%} | 各测试维度是否均衡 |",
188
+ f"| 反模式检测 | {self.score.anti_pattern_free:.0%} | 是否存在 '为通过而改' 反模式 |",
189
+ f"| **综合得分** | **{self.score.overall:.0%}** | |",
190
+ "",
191
+ ]
192
+
193
+ if self.issues:
194
+ lines.extend([
195
+ "## 问题清单",
196
+ "",
197
+ f"🔴 **严重**: {self.critical_count} | 🟠 **主要**: {self.major_count} | 🟡 **次要**: {self.minor_count}",
198
+ "",
199
+ ])
200
+ for issue in self.issues:
201
+ icon = {"critical": "🔴", "major": "🟠", "minor": "🟡", "info": "🔵", "suggestion": "💡"}.get(issue.severity.value, "⚪")
202
+ lines.append(f"- {icon} **[{issue.category}]** {issue.message}")
203
+ if issue.suggestion:
204
+ lines.append(f" → 建议: {issue.suggestion}")
205
+ if issue.line:
206
+ lines.append(f" → 位置: {issue.file}:{issue.line}")
207
+ lines.append("")
208
+
209
+ if self.test_functions:
210
+ has_purpose = sum(1 for t in self.test_functions if t.has_purpose)
211
+ lines.extend([
212
+ "## 测试函数清单",
213
+ "",
214
+ f"总计: {len(self.test_functions)} 个 | 有目的声明: {has_purpose} ({has_purpose/max(len(self.test_functions),1):.0%})",
215
+ "",
216
+ "| # | 函数名 | 目的声明 | 维度 | 断言数 | 异常测试 | 性能测试 |",
217
+ "|---|--------|---------|------|--------|---------|---------|",
218
+ ])
219
+ for idx, tf in enumerate(self.test_functions, 1):
220
+ purpose_icon = "✅" if tf.has_purpose else "❌"
221
+ dim = tf.dimension.value if tf.dimension else "-"
222
+ err_icon = "✅" if tf.has_error_test else "-"
223
+ perf_icon = "✅" if tf.has_performance_check else "-"
224
+ lines.append(
225
+ f"| {idx} | `{tf.name}` | {purpose_icon} | {dim} | {tf.assert_count} | {err_icon} | {perf_icon} |"
226
+ )
227
+ lines.append("")
228
+
229
+ lines.extend([
230
+ "---",
231
+ f"*由 TestQualityGuard v1.0 自动生成*",
232
+ ])
233
+ return "\n".join(lines)
234
+
235
+
236
+ TestQualityReport.__test__ = False # Tell pytest this is not a test class
237
+
238
+
239
+ class APISignatureValidator:
240
+ """检查测试代码中的 API 调用是否与实际签名一致"""
241
+
242
+ PARAM_PATTERN = re.compile(r'(\w+)\s*=\s*')
243
+
244
+ def __init__(self):
245
+ self.api_cache: Dict[str, List[APISignature]] = {}
246
+
247
+ def extract_api_signatures(self, source_code: str, file_path: str) -> List[APISignature]:
248
+ signatures = []
249
+ try:
250
+ tree = ast.parse(source_code)
251
+ except SyntaxError:
252
+ return signatures
253
+
254
+ for node in ast.walk(tree):
255
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
256
+ has_self = any(arg.arg == 'self' for arg in node.args.args)
257
+ params = []
258
+ for arg in node.args.args:
259
+ if arg.arg != 'self':
260
+ annotation = ""
261
+ if arg.annotation:
262
+ try:
263
+ annotation = ast.unparse(arg.annotation)
264
+ except Exception:
265
+ annotation = "?"
266
+ params.append({"name": arg.arg, "type": annotation})
267
+ sig = APISignature(
268
+ name=node.name,
269
+ kind="method" if has_self else "function",
270
+ params=params,
271
+ file=file_path,
272
+ line=node.lineno,
273
+ )
274
+ signatures.append(sig)
275
+ return signatures
276
+
277
+ def validate_call_against_signature(self,
278
+ call_name: str,
279
+ call_kwargs: Set[str],
280
+ signatures: List[APISignature]) -> List[QualityIssue]:
281
+ issues = []
282
+ matching_sigs = [s for s in signatures if s.name == call_name]
283
+ if not matching_sigs:
284
+ return issues
285
+
286
+ for sig in matching_sigs:
287
+ param_names = {p['name'] for p in sig.params if p['name'] != 'self'}
288
+ for kw in call_kwargs:
289
+ if kw not in param_names and not kw.startswith('_'):
290
+ issues.append(QualityIssue(
291
+ id=f"api-param-{call_name}-{kw}",
292
+ severity=Severity.MAJOR,
293
+ category="API参数错误",
294
+ message=f"`{call_name}()` 使用了不存在的参数 `{kw}`,实际签名为: {[p['name'] for p in sig.params]}",
295
+ file=sig.file,
296
+ line=sig.line,
297
+ suggestion=f"检查 `{call_name}` 的实际签名,将 `{kw}` 改为正确的参数名",
298
+ auto_fixable=False,
299
+ ))
300
+ return issues
301
+
302
+
303
+ class AntiPatternDetector:
304
+ """检测 '为测试通过而修改测试' 的反模式"""
305
+
306
+ SUSPICIOUS_PATTERNS = [
307
+ {
308
+ "id": "anti-loose-assert",
309
+ "pattern": re.compile(r'assertTrue\(.+\)'),
310
+ "description": "使用 assertTrue 替代精确断言(可能为了绕过失败)",
311
+ "severity": Severity.MINOR,
312
+ "category": "宽松断言",
313
+ },
314
+ {
315
+ "id": "anti-relaxed-float",
316
+ "pattern": re.compile(r'assertGreater\(.*,\s*0\.0\)'),
317
+ "description": "浮点数比较用 0.0 作为下限(几乎必然通过)",
318
+ "severity": Severity.MINOR,
319
+ "category": "无效断言",
320
+ },
321
+ {
322
+ "id": "anti-no-error-test",
323
+ "pattern": None,
324
+ "description": "整个测试类无异常/错误测试用例",
325
+ "severity": Severity.MAJOR,
326
+ "category": "缺失错误测试",
327
+ },
328
+ {
329
+ "id": "anti-no-purpose-doc",
330
+ "pattern": None,
331
+ "description": "测试函数无目的声明注释或docstring",
332
+ "severity": Severity.INFO,
333
+ "category": "缺少目的声明",
334
+ },
335
+ {
336
+ "id": "anti-bare-except",
337
+ "pattern": re.compile(r'except\s*:'),
338
+ "description": "裸 except 子句(吞掉所有异常)",
339
+ "severity": Severity.MAJOR,
340
+ "category": "异常吞噬",
341
+ },
342
+ {
343
+ "id": "anti-magic-number",
344
+ "pattern": re.compile(r'(assertEqual|assertGreater|assertLess)\([^,]+,\s*\d{3,}\)'),
345
+ "description": "断言中使用大数字魔法值(可能是凑出来的阈值)",
346
+ "severity": Severity.MINOR,
347
+ "category": "魔法数字",
348
+ },
349
+ ]
350
+
351
+ def detect_in_source(self, source: str, file: str) -> List[QualityIssue]:
352
+ issues = []
353
+ lines = source.split('\n')
354
+ for idx, line in enumerate(lines, 1):
355
+ stripped = line.strip()
356
+ for pat in self.SUSPICIOUS_PATTERNS:
357
+ if pat["pattern"] and pat["pattern"].search(stripped):
358
+ issues.append(QualityIssue(
359
+ id=pat["id"],
360
+ severity=pat["severity"],
361
+ category=pat["category"],
362
+ message=pat["description"],
363
+ file=file,
364
+ line=idx,
365
+ suggestion=self._get_suggestion(pat["id"]),
366
+ auto_fixable=False,
367
+ ))
368
+ return issues
369
+
370
+ def _get_suggestion(self, pattern_id: str) -> str:
371
+ suggestions = {
372
+ "anti-loose-assert": "使用精确断言如 assertEqual/assertIn 替代 assertTrue",
373
+ "anti-relaxed-float": "设置有意义的性能阈值,如 assertGreater(score, 0.5)",
374
+ "anti-no-error-test": "添加至少一个异常场景测试用例",
375
+ "anti-no-purpose-doc": "在测试函数前添加注释说明验证目的",
376
+ "anti-bare-except": "指定具体异常类型如 except ValueError",
377
+ "anti-magic-number": "提取为命名常量并添加注释说明来源",
378
+ }
379
+ return suggestions.get(pattern_id, "")
380
+
381
+
382
+ class TestPurposeParser:
383
+ """解析测试函数的目的声明"""
384
+
385
+ PURPOSE_MARKERS = ['#', '"""', "'''"]
386
+ PURPOSE_KEYWORDS = ['verify', 'check', 'test', 'ensure', 'validate',
387
+ '验证', '检查', '测试', '确保', '确认',
388
+ 'should', 'expect', '当', 'when']
389
+
390
+ DIMENSION_KEYWORDS = {
391
+ TestDimension.ERROR_CASE: ['error', 'exception', 'invalid', 'fail',
392
+ '错误', '异常', '非法', '失败', 'raises'],
393
+ TestDimension.PERFORMANCE: ['performance', 'speed', 'latency', 'timing',
394
+ 'benchmark', 'perf', '性能', '延迟', '耗时'],
395
+ TestDimension.BOUNDARY: ['boundary', 'edge', 'empty', 'null', 'zero',
396
+ 'max', 'min', '边界', '空', '极限'],
397
+ TestDimension.CONFIGURATION: ['config', 'setting', 'option', 'env',
398
+ '配置', '设定', '环境变量'],
399
+ TestDimension.INTEGRATION: ['integration', 'e2e', 'end.to.end',
400
+ '集成', '端到端'],
401
+ TestDimension.SECURITY: ['security', 'auth', 'permission', 'inject',
402
+ '安全', '权限', '注入'],
403
+ }
404
+
405
+ def parse_function(self, func_node: ast.FunctionDef, source_lines: List[str]) -> TestFunctionMeta:
406
+ meta = TestFunctionMeta(
407
+ name=func_node.name,
408
+ line=func_node.lineno,
409
+ has_purpose=False,
410
+ )
411
+
412
+ docstring = ast.get_docstring(func_node)
413
+ if docstring:
414
+ meta.docstring = docstring
415
+ meta.has_purpose = True
416
+ meta.purpose_text = docstring.strip().split('\n')[0][:200]
417
+
418
+ start_line = func_node.lineno - 1
419
+ end_line = func_node.end_lineno or start_line + 1
420
+ func_source = '\n'.join(source_lines[start_line:end_line])
421
+
422
+ if not meta.has_purpose:
423
+ for i in range(start_line, min(end_line, start_line + 5, len(source_lines))):
424
+ if i >= 0 and i < len(source_lines):
425
+ line = source_lines[i].strip()
426
+ if line.startswith('#') and any(kw in line.lower() for kw in self.PURPOSE_KEYWORDS):
427
+ meta.has_purpose = True
428
+ meta.purpose_text = line.lstrip('#').strip()[:200]
429
+ break
430
+
431
+ lower_source = func_source.lower()
432
+ meta.has_error_test = ('assertRaises' in func_source or
433
+ 'except' in func_source or
434
+ 'error' in lower_source or
435
+ 'exception' in lower_source or
436
+ 'invalid' in lower_source)
437
+ meta.has_performance_check = any(kw in lower_source
438
+ for kw in ['time', 'duration', 'latency',
439
+ 'benchmark', 'performance',
440
+ 'timing'])
441
+
442
+ meta.dimension = self._infer_dimension(func_source)
443
+
444
+ assert_types = re.findall(r'self\.(assert\w+)', func_source)
445
+ meta.assert_types = list(set(assert_types))
446
+ meta.assert_count = len(re.findall(r'self\.assert\w+', func_source))
447
+
448
+ return meta
449
+
450
+ def _infer_dimension(self, source: str) -> Optional[TestDimension]:
451
+ lower = source.lower()
452
+ scores = {}
453
+ for dim, keywords in self.DIMENSION_KEYWORDS.items():
454
+ score = sum(1 for kw in keywords if kw in lower)
455
+ if score > 0:
456
+ scores[dim] = score
457
+ if scores:
458
+ return max(scores, key=scores.get)
459
+ return TestDimension.HAPPY_PATH
460
+
461
+
462
+ class TestQualityGuard:
463
+ """
464
+ 测试质量守卫 - 主入口
465
+
466
+ 对测试文件进行全方位质量审计,
467
+ 输出可操作的改进建议报告。
468
+ """
469
+
470
+ def __init__(self,
471
+ module_path: str,
472
+ test_path: str,
473
+ strict_mode: bool = False):
474
+ """
475
+ Args:
476
+ module_path: 被测模块路径 (如 scripts/collaboration/coordinator.py)
477
+ test_path: 测试文件路径 (如 scripts/collaboration/coordinator_test.py)
478
+ strict_mode: 严格模式(更多检查项)
479
+ """
480
+ self.module_path = Path(module_path)
481
+ self.test_path = Path(test_path)
482
+ self.strict_mode = strict_mode
483
+ self.api_validator = APISignatureValidator()
484
+ self.anti_detector = AntiPatternDetector()
485
+ self.purpose_parser = TestPurposeParser()
486
+
487
+ def audit(self) -> TestQualityReport:
488
+ """执行完整审计"""
489
+ start_time = time.time()
490
+ report = TestQualityReport(
491
+ module_name=self.module_path.stem,
492
+ test_file=str(self.test_path),
493
+ source_file=str(self.module_path),
494
+ timestamp=datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
495
+ )
496
+
497
+ source_code = self._read_file(self.module_path)
498
+ test_code = self._read_file(self.test_path)
499
+
500
+ if not source_code or not test_code:
501
+ report.issues.append(QualityIssue(
502
+ id="file-not-found", severity=Severity.CRITICAL,
503
+ category="文件读取", message="无法读取源文件或测试文件",
504
+ file=str(self.module_path),
505
+ ))
506
+ report.audit_time = time.time() - start_time
507
+ return report
508
+
509
+ source_lines = source_code.split('\n')
510
+
511
+ report.api_signatures = self.api_validator.extract_api_signatures(source_code, str(self.module_path))
512
+
513
+ test_tree = ast.parse(test_code)
514
+ test_funcs = [node for node in ast.walk(test_tree)
515
+ if isinstance(node, ast.FunctionDef) and node.name.startswith('test_')]
516
+ report.total_tests = len(test_funcs)
517
+
518
+ for func_node in test_funcs:
519
+ meta = self.purpose_parser.parse_function(func_node, source_lines)
520
+ report.test_functions.append(meta)
521
+
522
+ if not meta.has_purpose:
523
+ report.issues.append(QualityIssue(
524
+ id=f"no-purpose-{meta.name}",
525
+ severity=Severity.INFO,
526
+ category="缺少目的声明",
527
+ message=f"`{meta.name}` 缺少测试目的声明(注释或docstring)",
528
+ file=str(self.test_path),
529
+ line=meta.line,
530
+ suggestion="添加 '# 验证: ...' 注释或 docstring 说明此测试验证什么",
531
+ auto_fixable=True,
532
+ ))
533
+
534
+ anti_issues = self.anti_detector.detect_in_source(test_code, str(self.test_path))
535
+ report.issues.extend(anti_issues)
536
+
537
+ no_error_class = not any(tf.has_error_test for tf in report.test_functions)
538
+ if no_error_class and report.total_tests > 5:
539
+ report.issues.append(QualityIssue(
540
+ id="no-error-tests",
541
+ severity=Severity.MAJOR,
542
+ category="缺失错误测试",
543
+ message=f"{report.total_tests}个测试中无异常/错误场景测试",
544
+ file=str(self.test_path),
545
+ suggestion="添加至少 15% 的错误/异常测试用例(如非法输入、网络超时、权限不足等)",
546
+ ))
547
+
548
+ no_perf = not any(tf.has_performance_check for tf in report.test_functions)
549
+ if no_perf and report.total_tests > 5:
550
+ report.issues.append(QualityIssue(
551
+ id="no-perf-tests",
552
+ severity=Severity.MINOR,
553
+ category="缺失性能测试",
554
+ message="无性能/耗时相关测试",
555
+ file=str(self.test_path),
556
+ suggestion="添加关键路径的性能基准测试(如操作应在 Nms 内完成)",
557
+ ))
558
+
559
+ report.score = self._calculate_score(report)
560
+ report.audit_time = time.time() - start_time
561
+ return report
562
+
563
+ def _read_file(self, path: Path) -> Optional[str]:
564
+ try:
565
+ with open(path, 'r', encoding='utf-8') as f:
566
+ return f.read()
567
+ except FileNotFoundError:
568
+ return None
569
+
570
+ def _calculate_score(self, report: TestQualityReport) -> QualityScore:
571
+ score = QualityScore()
572
+
573
+ if report.total_tests > 0:
574
+ purpose_count = sum(1 for t in report.test_functions if t.has_purpose)
575
+ score.purpose_coverage = purpose_count / report.total_tests
576
+
577
+ dim_counts = {}
578
+ for tf in report.test_functions:
579
+ d = tf.dimension or TestDimension.HAPPY_PATH
580
+ dim_counts[d] = dim_counts.get(d, 0) + 1
581
+ if dim_counts:
582
+ values = list(dim_counts.values())
583
+ max_v = max(values)
584
+ min_v = min(values)
585
+ total = sum(values)
586
+ expected_per_dim = total / len(dim_counts)
587
+ variance = sum((v - expected_per_dim) ** 2 for v in values) / len(values)
588
+ ideal_variance = 0
589
+ score.dimension_balance = max(0, 1 - (variance / (total * total / 4)))
590
+
591
+ anti_critical = sum(1 for i in report.issues
592
+ if i.severity in (Severity.CRITICAL, Severity.MAJOR)
593
+ and i.category in ('宽松断言', '异常吞噬'))
594
+ score.anti_pattern_free = max(0, 1 - anti_critical / max(report.total_tests * 0.1, 1))
595
+
596
+ api_issues = sum(1 for i in report.issues if i.category == "API参数错误")
597
+ score.api_compliance = max(0, 1 - api_issues / max(report.total_tests * 0.05, 1))
598
+
599
+ score.overall = (
600
+ score.api_compliance * 0.25 +
601
+ score.purpose_coverage * 0.30 +
602
+ score.dimension_balance * 0.20 +
603
+ score.anti_pattern_free * 0.25
604
+ )
605
+
606
+ return score
607
+
608
+ def audit_project(self, project_root: str,
609
+ pattern: str = "**/*_test.py") -> List[TestQualityReport]:
610
+ """审计整个项目的所有测试文件"""
611
+ reports = []
612
+ root = Path(project_root)
613
+ test_files = sorted(root.glob(pattern))
614
+
615
+ for test_file in test_files:
616
+ module_name = test_file.name.replace("_test.py", ".py")
617
+ candidates = [
618
+ test_file.parent / module_name,
619
+ test_file.parent / module_name.replace(".py", "/__init__.py"),
620
+ ]
621
+ for candidate in candidates:
622
+ if candidate.exists():
623
+ try:
624
+ r = self.__class__(str(candidate), str(test_file)).audit()
625
+ reports.append(r)
626
+ except Exception:
627
+ pass
628
+ break
629
+ return reports
630
+
631
+ def generate_test_template(self,
632
+ api_sig: APISignature,
633
+ dimensions: List[TestDimension] = None) -> str:
634
+ """根据 API 签名生成高质量测试模板"""
635
+ dims = dimensions or [TestDimension.HAPPY_PATH, TestDimension.ERROR_CASE]
636
+ params_str = ", ".join(p['name'] for p in api_sig.params if p['name'] != 'self')
637
+ param_docs = "\n".join(f" {p['name']}: {p['type'] or 'Any'}" for p in api_sig.params if p['name'] != 'self')
638
+
639
+ template = f'''\
640
+ def test_{api_sig.name}_happy_path(self):
641
+ """验证: {api_sig.name} 正常输入应返回预期结果"""
642
+ # Arrange
643
+ {param_docs if param_docs else " # TODO: 设置正常输入参数"}
644
+
645
+ # Act
646
+ result = self.target.{api_sig.name}({params_str})
647
+
648
+ # Assert
649
+ self.assertIsNotNone(result)
650
+ '''
651
+
652
+ if TestDimension.ERROR_CASE in dims:
653
+ template += f'''
654
+ def test_{api_sig.name}_invalid_input(self):
655
+ """验证: {api_sig.name} 非法输入应抛出异常或返回错误"""
656
+ # TODO: 测试非法参数、空值、越界等情况
657
+ with self.assertRaises((ValueError, TypeError)):
658
+ self.target.{api_sig.name}(invalid_param="bad_value")
659
+ '''
660
+
661
+ if TestDimension.PERFORMANCE in dims:
662
+ template += f'''
663
+ def test_{api_sig.name}_performance(self):
664
+ """验证: {api_sig.name} 应在合理时间内完成"""
665
+ import time
666
+ start = time.perf_counter()
667
+ self.target.{api_sig.name}({params_str})
668
+ elapsed = (time.perf_counter() - start) * 1000
669
+ self.assertLess(elapsed, 100, f"耗时 {{elapsed:.1f}}ms 超过 100ms 阈值")
670
+ '''
671
+ return template
672
+
673
+
674
+ TestQualityGuard.__test__ = False # Tell pytest this is not a test class
675
+
676
+
677
+ def quick_audit(module_path: str, test_path: str) -> TestQualityReport:
678
+ """便捷函数:单文件审计"""
679
+ return TestQualityGuard(module_path, test_path).audit()
680
+
681
+
682
+ def project_audit(project_root: str) -> str:
683
+ """便捷函数:项目级审计,返回 Markdown 报告"""
684
+ guard = TestQualityGuard("", "")
685
+ reports = guard.audit_project(project_root)
686
+ lines = ["# 项目级测试质量审计报告\n"]
687
+ total_score = 0
688
+ for r in reports:
689
+ lines.append(r.to_markdown())
690
+ lines.append("\n---\n")
691
+ total_score += r.score.overall
692
+ if reports:
693
+ avg = total_score / len(reports)
694
+ lines.append(f"\n## 项目总体评分: **{avg:.0%}** ({len(reports)} 个模块)")
695
+ return "\n".join(lines)