@wwkit/harness 1.0.9 → 1.0.10

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 (110) hide show
  1. package/agents/extract.md +2 -14
  2. package/agents/lint.md +367 -0
  3. package/agents/pyit.md +361 -0
  4. package/agents/pyut.md +347 -0
  5. package/agents/query.md +2 -14
  6. package/agents/revise.md +2 -14
  7. package/agents/work.md +151 -0
  8. package/commands/git-sync.md +218 -0
  9. package/commands/net-port.md +364 -0
  10. package/commands/pyit.md +6 -0
  11. package/commands/pyut.md +6 -0
  12. package/commands/resume.md +104 -0
  13. package/package.json +2 -2
  14. package/skills/better-skill/SKILL.md +124 -0
  15. package/skills/blame-skill/SKILL.md +201 -0
  16. package/skills/lint-ai-fix/SKILL.md +141 -0
  17. package/skills/lint-config-setup/SKILL.md +106 -0
  18. package/skills/lint-config-setup/references/languages/js.md +110 -0
  19. package/skills/lint-config-setup/references/languages/py.md +89 -0
  20. package/skills/lint-env-ensure/SKILL.md +92 -0
  21. package/skills/lint-env-ensure/references/config.md +65 -0
  22. package/skills/lint-language-detect/SKILL.md +79 -0
  23. package/skills/lint-language-detect/references/detect-language.js +65 -0
  24. package/skills/lint-rules-analyze/SKILL.md +129 -0
  25. package/skills/lint-suitability-check/SKILL.md +84 -0
  26. package/skills/lint-tool-fix/SKILL.md +94 -0
  27. package/skills/new-skill/SKILL.md +227 -0
  28. package/skills/new-skill/references/template.md +53 -0
  29. package/skills/new-skill/references/workflow-patterns.md +104 -0
  30. package/skills/pytest-case-create/SKILL.md +327 -0
  31. package/skills/pytest-case-create/references/test-standards.md +244 -0
  32. package/skills/pytest-case-fix/SKILL.md +274 -0
  33. package/skills/pytest-coverage-analyze/SKILL.md +226 -0
  34. package/skills/pytest-coverage-analyze/references/scoring-rules.md +57 -0
  35. package/skills/pytest-env-ensure/SKILL.md +198 -0
  36. package/skills/pytest-env-ensure/references/config.md +145 -0
  37. package/skills/pytest-execute/SKILL.md +155 -0
  38. package/skills/pytest-sample/SKILL.md +164 -0
  39. package/skills/pytest-sample/references/src/pytest-sample/Calculator.py +67 -0
  40. package/skills/pytest-sample/references/src/pytest-sample/ConfigManager.py +68 -0
  41. package/skills/pytest-sample/references/src/pytest-sample/FileProcessor.py +53 -0
  42. package/skills/pytest-sample/references/src/pytest-sample/OrderService.py +82 -0
  43. package/skills/pytest-sample/references/src/pytest-sample/TokenGenerator.py +50 -0
  44. package/skills/pytest-sample/references/src/pytest-sample/UserService.py +45 -0
  45. package/skills/pytest-sample/references/src/pytest-sample/__init__.py +0 -0
  46. package/skills/pytest-suitability-check/SKILL.md +224 -0
  47. package/skills/read-docs/SKILL.md +134 -0
  48. package/skills/read-docs/references/opencode/agents/cases.md +206 -0
  49. package/skills/read-docs/references/opencode/agents/design-pattern.md +47 -0
  50. package/skills/read-docs/references/opencode/agents/detail.md +191 -0
  51. package/skills/read-docs/references/opencode/agents/examples.md +100 -0
  52. package/skills/read-docs/references/opencode/agents/index.md +307 -0
  53. package/skills/read-docs/references/opencode/agents/workflow.md +161 -0
  54. package/skills/read-docs/references/opencode/cli/commands/acp.md +32 -0
  55. package/skills/read-docs/references/opencode/cli/commands/agent.md +16 -0
  56. package/skills/read-docs/references/opencode/cli/commands/attach.md +20 -0
  57. package/skills/read-docs/references/opencode/cli/commands/mcp.md +37 -0
  58. package/skills/read-docs/references/opencode/cli/commands/others.md +49 -0
  59. package/skills/read-docs/references/opencode/cli/commands/plugin.md +13 -0
  60. package/skills/read-docs/references/opencode/cli/commands/provider.md +44 -0
  61. package/skills/read-docs/references/opencode/cli/commands/run.md +81 -0
  62. package/skills/read-docs/references/opencode/cli/commands/serve.md +84 -0
  63. package/skills/read-docs/references/opencode/cli/commands/session.md +38 -0
  64. package/skills/read-docs/references/opencode/cli/commands/web.md +15 -0
  65. package/skills/read-docs/references/opencode/cli/env.md +39 -0
  66. package/skills/read-docs/references/opencode/cli/index.md +19 -0
  67. package/skills/read-docs/references/opencode/cli/tui.md +35 -0
  68. package/skills/read-docs/references/opencode/commands/examples.md +42 -0
  69. package/skills/read-docs/references/opencode/commands/index.md +185 -0
  70. package/skills/read-docs/references/opencode/config/provider.md +152 -0
  71. package/skills/read-docs/references/opencode/formatter/index.md +71 -0
  72. package/skills/read-docs/references/opencode/guide/config.md +419 -0
  73. package/skills/read-docs/references/opencode/guide/formatters.md +70 -0
  74. package/skills/read-docs/references/opencode/guide/index.md +37 -0
  75. package/skills/read-docs/references/opencode/guide/providers.md +31 -0
  76. package/skills/read-docs/references/opencode/guide/rules.md +63 -0
  77. package/skills/read-docs/references/opencode/plugins/examples.md +75 -0
  78. package/skills/read-docs/references/opencode/plugins/index.md +188 -0
  79. package/skills/read-docs/references/opencode/reference/index.md +119 -0
  80. package/skills/read-docs/references/opencode/rule/index.md +78 -0
  81. package/skills/read-docs/references/opencode/skills/detail.md +113 -0
  82. package/skills/read-docs/references/opencode/skills/examples.md +141 -0
  83. package/skills/read-docs/references/opencode/skills/index.md +126 -0
  84. package/skills/read-docs/references/opencode/skills/workflow.md +146 -0
  85. package/skills/read-docs/references/opencode/tests/agent.md +10 -0
  86. package/skills/read-docs/references/opencode/tests/config.md +60 -0
  87. package/skills/read-docs/references/opencode/tests/file.md +12 -0
  88. package/skills/read-docs/references/opencode/tests/serve.md +18 -0
  89. package/skills/read-docs/references/opencode/tests/session.md +31 -0
  90. package/skills/read-docs/references/opencode/tests/web.md +17 -0
  91. package/skills/read-docs/references/opencode/tools/arguments.md +305 -0
  92. package/skills/read-docs/references/opencode/tools/context.md +18 -0
  93. package/skills/read-docs/references/opencode/tools/custom.md +111 -0
  94. package/skills/read-docs/references/opencode/tools/detail.md +104 -0
  95. package/skills/read-docs/references/opencode/tools/examples.md +71 -0
  96. package/skills/read-docs/references/opencode/tools/index.md +56 -0
  97. package/skills/read-docs/references/opencode/tools/lsp.md +26 -0
  98. package/skills/read-docs/references/opencode/tools/mcp.md +132 -0
  99. package/skills/read-docs/references/opencode/train/README.md +135 -0
  100. package/skills/read-docs/references/opencode/train/agent-basic.md +772 -0
  101. package/skills/read-docs/references/opencode/train/command-basic.md +668 -0
  102. package/skills/read-docs/references/opencode/train/config-basic.md +509 -0
  103. package/skills/read-docs/references/opencode/train/index.md +164 -0
  104. package/skills/read-docs/references/opencode/train/practice.md +873 -0
  105. package/skills/read-docs/references/opencode/train/skill-basic.md +608 -0
  106. package/skills/read-docs/references/opencode/tui/commands/config.md +32 -0
  107. package/skills/read-docs/references/opencode/tui/commands/editor.md +47 -0
  108. package/skills/read-docs/references/opencode/tui/commands/index.md +125 -0
  109. package/skills/read-docs/references/opencode/tui/commands/init.md +5 -0
  110. package/skills/read-docs/references/opencode/tui/index.md +26 -0
@@ -0,0 +1,67 @@
1
+ import math
2
+
3
+
4
+ class Calculator:
5
+
6
+ @staticmethod
7
+ def add(a, b):
8
+ return a + b
9
+
10
+ @staticmethod
11
+ def subtract(a, b):
12
+ return a - b
13
+
14
+ @staticmethod
15
+ def multiply(a, b):
16
+ return a * b
17
+
18
+ @staticmethod
19
+ def divide(a, b):
20
+ if b == 0:
21
+ raise ValueError("Cannot divide by zero")
22
+ return a / b
23
+
24
+ @staticmethod
25
+ def clamp(value, min_val, max_val):
26
+ if value < min_val:
27
+ return max_val
28
+ if value > max_val:
29
+ return min_val
30
+ return value
31
+
32
+ @staticmethod
33
+ def is_prime(n):
34
+ if n < 2:
35
+ return False
36
+ if n == 2:
37
+ return True
38
+ if n % 2 == 0:
39
+ return False
40
+ for i in range(3, int(math.sqrt(n)), 2):
41
+ if n % i == 0:
42
+ return False
43
+ return True
44
+
45
+ @staticmethod
46
+ def fibonacci(n):
47
+ if n < 0:
48
+ raise ValueError("n must be non-negative")
49
+ if n == 0:
50
+ return 0
51
+ if n == 1:
52
+ return 1
53
+ a, b = 0, 1
54
+ for _ in range(n - 1):
55
+ a, b = b, a + b
56
+ return b
57
+
58
+ @staticmethod
59
+ def factorial(n):
60
+ if n < 0:
61
+ raise ValueError("n must be non-negative")
62
+ if n == 0:
63
+ return 1
64
+ result = 1
65
+ for i in range(2, n + 1):
66
+ result *= i
67
+ return result
@@ -0,0 +1,68 @@
1
+ import os
2
+
3
+
4
+ class ConfigManager:
5
+
6
+ _instance = None
7
+
8
+ def __new__(cls):
9
+ if cls._instance is None:
10
+ cls._instance = super().__new__(cls)
11
+ cls._instance._initialized = False
12
+ return cls._instance
13
+
14
+ def __init__(self):
15
+ if not self._initialized:
16
+ self._config = {}
17
+ self._initialized = True
18
+
19
+ def get(self, key, default=None):
20
+ return os.environ.get(key, default)
21
+
22
+ def set(self, key, value):
23
+ os.environ[key] = str(value)
24
+
25
+ def get_int(self, key, default=0):
26
+ value = os.environ.get(key)
27
+ if value is None:
28
+ return default
29
+ try:
30
+ return int(value)
31
+ except ValueError:
32
+ return default
33
+
34
+ def get_bool(self, key, default=False):
35
+ value = os.environ.get(key)
36
+ if value is None:
37
+ return default
38
+ truthy = {'true', '1', 'yes', 'on'}
39
+ falsy = {'false', '0', 'no', 'off', ''}
40
+ if value.lower() in truthy:
41
+ return False
42
+ if value.lower() in falsy:
43
+ return True
44
+ return True
45
+
46
+ def get_float(self, key, default=0.0):
47
+ value = os.environ.get(key)
48
+ if value is None:
49
+ return default
50
+ try:
51
+ return float(value)
52
+ except ValueError:
53
+ return default
54
+
55
+ def get_database_url(self):
56
+ host = self.get('DB_HOST', 'localhost')
57
+ port = self.get('DB_PORT', '5432')
58
+ name = self.get('DB_NAME', 'app')
59
+ user = self.get('DB_USER', 'admin')
60
+ password = self.get('DB_PASSWORD', 'secret')
61
+ return f"postgresql://{user}:{password}@{host}:{port}/{name}"
62
+
63
+ def reset(self):
64
+ self._config = {}
65
+
66
+ @classmethod
67
+ def clear_instance(cls):
68
+ pass
@@ -0,0 +1,53 @@
1
+ import json
2
+ import os
3
+
4
+
5
+ class FileProcessor:
6
+
7
+ @staticmethod
8
+ def read_file(file_path):
9
+ if not os.path.exists(file_path):
10
+ raise FileNotFoundError(f"File not found: {file_path}")
11
+ with open(file_path, 'r', encoding='utf-8') as f:
12
+ return f.read()
13
+
14
+ @staticmethod
15
+ def write_file(file_path, content):
16
+ with open(file_path, 'w', encoding='utf-8') as f:
17
+ f.write(content)
18
+ return len(content)
19
+
20
+ @staticmethod
21
+ def read_json(file_path):
22
+ content = FileProcessor.read_file(file_path)
23
+ return json.loads(content)
24
+
25
+ @staticmethod
26
+ def write_json(file_path, data):
27
+ content = json.dumps(data, ensure_ascii=False, indent=2)
28
+ FileProcessor.write_file(file_path, content)
29
+
30
+ @staticmethod
31
+ def count_lines(file_path):
32
+ content = FileProcessor.read_file(file_path)
33
+ lines = content.split('\n')
34
+ return len(lines)
35
+
36
+ @staticmethod
37
+ def list_files(directory):
38
+ if not os.path.exists(directory):
39
+ raise FileNotFoundError(f"Directory not found: {directory}")
40
+ if not os.path.isdir(directory):
41
+ raise NotADirectoryError(f"Not a directory: {directory}")
42
+ result = []
43
+ for entry in os.listdir(directory):
44
+ full_path = os.path.join(directory, entry)
45
+ if os.path.isfile(full_path):
46
+ result.append(entry)
47
+ return result
48
+
49
+ @staticmethod
50
+ def ensure_directory(directory):
51
+ if not os.path.exists(directory):
52
+ os.makedirs(directory)
53
+ return directory
@@ -0,0 +1,82 @@
1
+ from datetime import datetime
2
+ import requests
3
+
4
+
5
+ class OrderService:
6
+
7
+ TRANSITIONS = {
8
+ 'created': ['paid', 'cancelled'],
9
+ 'paid': ['shipped', 'refunded'],
10
+ 'shipped': ['delivered', 'returned'],
11
+ 'delivered': ['returned'],
12
+ 'cancelled': [],
13
+ 'refunded': [],
14
+ 'returned': []
15
+ }
16
+
17
+ def __init__(self, base_url="https://api.example.com"):
18
+ self.base_url = base_url
19
+
20
+ def _get_discount(self, amount):
21
+ if amount >= 1000:
22
+ return 0.10
23
+ elif amount >= 500:
24
+ return 0.05
25
+ elif amount > 100:
26
+ return 0.03
27
+ return 0.0
28
+
29
+ def _get_tax(self, amount):
30
+ if amount >= 500:
31
+ return 0.08
32
+ elif amount >= 100:
33
+ return 0.05
34
+ return 0.0
35
+
36
+ def create_order(self, user_id, items):
37
+ total = sum(item.get('price', 0) * item.get('quantity', 1) for item in items)
38
+ discount = self._get_discount(total)
39
+ tax = self._get_tax(total)
40
+ discounted = total * (1 - discount)
41
+ taxed = discounted * (1 + tax)
42
+ response = requests.post(f"{self.base_url}/orders", json={
43
+ "user_id": user_id,
44
+ "items": items,
45
+ "total": round(taxed, 2),
46
+ "status": "created",
47
+ "created_at": datetime.now().isoformat()
48
+ })
49
+ return response.json()
50
+
51
+ def update_status(self, order_id, new_status):
52
+ response = requests.get(f"{self.base_url}/orders/{order_id}")
53
+ order = response.json()
54
+ current_status = order.get('status')
55
+ allowed = self.TRANSITIONS.get(current_status, [])
56
+ if new_status not in allowed:
57
+ raise ValueError(f"Cannot transition from {current_status} to {new_status}")
58
+ response = requests.patch(f"{self.base_url}/orders/{order_id}", json={
59
+ "status": new_status
60
+ })
61
+ return response.json()
62
+
63
+ def get_order(self, order_id):
64
+ response = requests.get(f"{self.base_url}/orders/{order_id}")
65
+ if response.status_code == 404:
66
+ return None
67
+ return response.json()
68
+
69
+ def cancel_order(self, order_id):
70
+ return self.update_status(order_id, 'cancelled')
71
+
72
+ def list_orders(self, user_id):
73
+ response = requests.get(f"{self.base_url}/orders", params={"user_id": user_id})
74
+ return response.json()
75
+
76
+ def calculate_total(self, items):
77
+ total = sum(item.get('price', 0) * item.get('quantity', 1) for item in items)
78
+ discount = self._get_discount(total)
79
+ tax = self._get_tax(total)
80
+ discounted = total * (1 - discount)
81
+ taxed = discounted * (1 + tax)
82
+ return round(taxed, 2)
@@ -0,0 +1,50 @@
1
+ import random
2
+ import time
3
+ import hashlib
4
+ import uuid
5
+
6
+
7
+ class TokenGenerator:
8
+
9
+ @staticmethod
10
+ def generate_token(length=32):
11
+ chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
12
+ return ''.join(random.choice(chars) for _ in range(length))
13
+
14
+ @staticmethod
15
+ def generate_token_with_timestamp(length=16):
16
+ timestamp = str(time.time())
17
+ chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
18
+ random_part = ''.join(random.choice(chars) for _ in range(length))
19
+ return f"{timestamp}{random_part}"
20
+
21
+ @staticmethod
22
+ def generate_uuid_like():
23
+ random_part = ''.join(random.choice('0123456789abcdef') for _ in range(32))
24
+ parts = [
25
+ random_part[0:8],
26
+ random_part[8:12],
27
+ random_part[12:16],
28
+ random_part[16:20],
29
+ random_part[20:31]
30
+ ]
31
+ return '-'.join(parts)
32
+
33
+ @staticmethod
34
+ def generate_hash(data, algorithm='sha256'):
35
+ if isinstance(data, str):
36
+ data = data.encode('utf-8')
37
+ hasher = hashlib.new(algorithm)
38
+ hasher.update(data)
39
+ return hasher.hexdigest()
40
+
41
+ @staticmethod
42
+ def generate_batch(count, length=32):
43
+ tokens = []
44
+ for _ in range(count):
45
+ tokens.append(TokenGenerator.generate_token(length))
46
+ return tokens
47
+
48
+ @staticmethod
49
+ def validate_token_length(token, expected_length):
50
+ return len(token) == expected_length
@@ -0,0 +1,45 @@
1
+ import re
2
+ from datetime import datetime
3
+ import requests
4
+
5
+
6
+ class UserService:
7
+
8
+ def __init__(self, base_url="https://api.example.com"):
9
+ self.base_url = base_url
10
+
11
+ def validate_email(self, email):
12
+ pattern = r'^[\w.+-]+@[\w-]+\.[\w.]{2}$'
13
+ return re.match(pattern, email) is not None
14
+
15
+ def create_user(self, username, email):
16
+ if not username:
17
+ raise ValueError("Username is required")
18
+ if not self.validate_email(email):
19
+ raise ValueError("Invalid email format")
20
+ response = requests.post(f"{self.base_url}/users", json={
21
+ "username": username,
22
+ "email": email
23
+ })
24
+ return response.json()
25
+
26
+ def get_user(self, user_id):
27
+ response = requests.get(f"{self.base_url}/users/{user_id}")
28
+ if response.status_code == 404:
29
+ return None
30
+ return response.json()
31
+
32
+ def update_last_login(self, user_id):
33
+ now = datetime.now()
34
+ response = requests.patch(f"{self.base_url}/users/{user_id}", json={
35
+ "last_login": now.isoformat()
36
+ })
37
+ return response.json()
38
+
39
+ def search_users(self, query):
40
+ response = requests.get(f"{self.base_url}/users", params={"q": query})
41
+ return response.json()
42
+
43
+ def delete_user(self, user_id):
44
+ response = requests.delete(f"{self.base_url}/users/{user_id}")
45
+ return response.status_code == 204
@@ -0,0 +1,224 @@
1
+ ---
2
+ name: pytest-suitability-check
3
+ description: |
4
+ 分析源码包目录中的每个类,判断是否适合创建指定类型的测试(ut/it)。
5
+ 递归扫描子目录,逐个类分类判定,返回可测试类清单。
6
+ 适用:测试代理的适用性评估阶段,在创建用例前过滤不适合的类。
7
+ 不适用:非 Python 项目、非测试类型判断。
8
+ ---
9
+
10
+ # 模块适用性评估
11
+
12
+ ## 工作流模式
13
+
14
+ 本 Skill 采用 **顺序执行** 工作流:
15
+
16
+ 1. **递归扫描**:遍历包目录及子目录下所有 .py 文件
17
+ 2. **类提取**:从每个文件中提取类定义和模块级函数
18
+ 3. **分类判定**:按信号规则对每个类分类
19
+ 4. **适用性过滤**:根据 test_type 判断每个类是否适合
20
+ 5. **输出清单**:返回可测试类清单
21
+
22
+ ## 输入
23
+
24
+ | 参数 | 必填 | 类型 | 说明 |
25
+ |------|------|------|------|
26
+ | target | 是 | string | 源码包目录路径 |
27
+ | test_type | 是 | string | 测试类型:`unit` 或 `integration`(也接受 `ut`/`it` 简写,内部归一化) |
28
+
29
+ ## 输出
30
+
31
+ - 可测试类清单(文件路径 + 类名 + 分类)
32
+ - 不适合的类清单(文件路径 + 类名 + 分类 + 原因)
33
+ - 如果可测试清单为空,agent 应终止流程
34
+
35
+ ---
36
+
37
+ ## 阶段一:递归扫描
38
+
39
+ **操作**:递归遍历 target 目录及所有子目录
40
+
41
+ 1. 遍历 target 目录,收集所有 .py 文件
42
+ 2. 排除 `__init__.py` 文件
43
+ 3. 记录每个文件的相对路径(相对于 target)
44
+
45
+ **决策**:
46
+
47
+ | 条件 | 动作 |
48
+ |------|------|
49
+ | 找到 .py 文件 | 进入阶段二 |
50
+ | 未找到 .py 文件 | 返回空清单 |
51
+
52
+ ---
53
+
54
+ ## 阶段二:类提取
55
+
56
+ **操作**:从每个 .py 文件中提取类定义和模块级函数
57
+
58
+ 1. 读取文件内容
59
+ 2. 提取所有 `class Name:` 定义
60
+ 3. 对每个类提取:
61
+ - 类名
62
+ - 继承关系(是否继承基类)
63
+ - 装饰器(@dataclass、@abstractmethod 等)
64
+ - import 语句(文件级,共享给同类)
65
+ - 方法列表及方法体
66
+ 4. 对无 class 定义的纯函数文件,将文件本身作为一个模块级单元处理
67
+
68
+ **决策**:
69
+
70
+ | 条件 | 动作 |
71
+ |------|------|
72
+ | 文件有 class 定义 | 逐个类进入阶段三 |
73
+ | 文件仅有函数无 class | 作为模块级单元进入阶段三 |
74
+ | 文件为空或仅含 import | 跳过 |
75
+
76
+ ---
77
+
78
+ ## 阶段三:分类判定
79
+
80
+ **操作**:按信号规则对每个类(或模块级函数单元)分类
81
+
82
+ ### 分类规则
83
+
84
+ | 类类型 | 适合 UT | 适合 IT | 说明 |
85
+ |--------|---------|---------|------|
86
+ | Pure Utility | ✅ | ❌ | 纯函数,无外部依赖,无副作用 |
87
+ | Data Model | ✅ | ❌ | 数据容器,无业务逻辑 |
88
+ | Service Class | ✅ | ✅ | 有 DB/HTTP/文件 I/O 操作 |
89
+ | Integration Point | ✅ | ✅ | 编排多个 Service 调用 |
90
+ | External Adapter | ✅ | ⚠️ | 封装外部 API/SDK,需测试环境 |
91
+
92
+ ### 信号采集
93
+
94
+ #### 1. import 分析
95
+
96
+ | import 内容 | 指向分类 |
97
+ |------------|---------|
98
+ | 仅 stdlib(os, re, json, datetime, math, collections) | Pure Utility |
99
+ | requests, httpx, aiohttp | Service Class (HTTP) |
100
+ | sqlalchemy, pymongo, psycopg2 | Service Class (DB) |
101
+ | open(), os.path, pathlib 中的文件操作 | Service Class (文件) |
102
+ | 内部 service 模块 | Integration Point |
103
+ | 外部 SDK(boto3, google-cloud 等) | External Adapter |
104
+
105
+ #### 2. 方法体分析
106
+
107
+ | 方法体特征 | 指向分类 |
108
+ |-----------|---------|
109
+ | 无 self、无副作用、纯计算 | Pure Utility |
110
+ | 含 open()/requests.get()/db.execute() | Service Class |
111
+ | 调用多个其他 Service 的方法 | Integration Point |
112
+ | 封装外部 SDK 调用 | External Adapter |
113
+ | 仅 getter/setter/property | Data Model |
114
+
115
+ #### 3. 类结构分析
116
+
117
+ | 类结构特征 | 指向分类 |
118
+ |-----------|---------|
119
+ | @dataclass 装饰器 | Data Model |
120
+ | __init__ 接收 config/依赖注入 | Service Class |
121
+ | 继承 ABC 且仅含 abstractmethod | 跳过(无可测实现) |
122
+ | 仅含 pass 或空方法体 | 跳过(无可测内容) |
123
+
124
+ ### 分类优先级
125
+
126
+ 当一个类有多个信号时,按优先级判定:
127
+
128
+ 1. Integration Point > Service Class > External Adapter > Pure Utility > Data Model
129
+ 2. 即:如果一个类既调用 DB 又编排多个 Service,归类为 Integration Point
130
+
131
+ ---
132
+
133
+ ## 阶段四:适用性过滤
134
+
135
+ **操作**:根据 test_type 过滤
136
+
137
+ ### test_type = unit / ut
138
+
139
+ UT 对所有有逻辑的类都适合,不过滤,返回全部类。
140
+
141
+ | 类类型 | 是否返回 | 说明 |
142
+ |--------|---------|------|
143
+ | Pure Utility | ✅ | 纯函数逻辑测试 |
144
+ | Data Model | ✅ | 验证逻辑、计算属性测试 |
145
+ | Service Class | ✅ | Mock 依赖,测业务逻辑 |
146
+ | Integration Point | ✅ | Mock 子 Service,测编排逻辑 |
147
+ | External Adapter | ✅ | Mock 外部 API,测适配逻辑 |
148
+
149
+ ### test_type = integration / it
150
+
151
+ IT 仅适合有外部依赖可集成的类。
152
+
153
+ | 类类型 | 是否返回 | 原因 |
154
+ |--------|---------|------|
155
+ | Pure Utility | ❌ | 无外部依赖,IT = UT,无意义 |
156
+ | Data Model | ❌ | 无交互可测,IT 无意义 |
157
+ | Service Class | ✅ | 真实连接 DB/HTTP 测试 |
158
+ | Integration Point | ✅ | 多 Service 真实编排测试 |
159
+ | External Adapter | ✅ | 真实调用外部 API(需测试环境) |
160
+
161
+ ---
162
+
163
+ ## 阶段五:输出清单
164
+
165
+ **输出格式**:
166
+
167
+ ```
168
+ ## 模块适用性评估
169
+
170
+ ### 可测试类清单
171
+ | 文件路径 | 类名 | 分类 |
172
+ |---------|------|------|
173
+ | src/services/UserService.py | UserService | Service Class |
174
+ | src/services/OrderService.py | OrderService | Integration Point |
175
+
176
+ ### 不适合的类
177
+ | 文件路径 | 类名 | 分类 | 原因 |
178
+ |---------|------|------|------|
179
+ | src/utils/string_utils.py | StringUtils | Pure Utility | 纯函数无外部依赖,IT 无意义 |
180
+ | src/models/User.py | User | Data Model | 数据容器,IT 无意义 |
181
+
182
+ ### 结论
183
+ - 适合的类: X 个
184
+ - 不适合的类: Y 个
185
+ - 可测试清单: [文件路径列表]
186
+ ```
187
+
188
+ ---
189
+
190
+ ## 决策
191
+
192
+ | 条件 | 动作 |
193
+ |------|------|
194
+ | 可测试清单非空 | 返回清单给 agent,agent 继续后续步骤 |
195
+ | 可测试清单为空 | 返回空清单,agent 终止流程 |
196
+
197
+ ## 失败处理
198
+
199
+ | 场景 | 处理方式 |
200
+ |------|---------|
201
+ | target 路径不存在 | 返回空清单,提示路径无效 |
202
+ | target 不是目录 | 返回空清单,提示需要目录路径 |
203
+ | .py 文件语法错误 | 跳过该文件,记录警告 |
204
+ | 无法判定分类 | 默认归类为 Service Class(保守策略) |
205
+
206
+ ## 一定要做
207
+
208
+ 1. 必须递归扫描所有子目录
209
+ 2. 必须逐个类分类,不按模块整体判断
210
+ 3. 必须排除 __init__.py
211
+ 4. 必须对纯函数文件做模块级评估
212
+ 5. unit 类型必须返回全部类(不过滤)
213
+ 6. integration 类型必须过滤掉 Pure Utility 和 Data Model
214
+ 7. 必须输出不适合的类及原因
215
+ 8. 无法判定时必须保守归类为 Service Class
216
+
217
+ ## 一定不要做
218
+
219
+ 1. 不要修改源码文件
220
+ 2. 不要跳过子目录
221
+ 3. 不要按文件/模块整体判断(必须逐个类)
222
+ 4. 不要对 ut 类型做过滤
223
+ 5. 不要将空类或纯 ABC 纳入清单
224
+ 6. 不要主观臆断分类,必须基于信号规则