jsharness 1.12.4 → 1.12.5
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.
- package/.harness/agents/code-reviewer/contract.yaml +15 -4
- package/.harness/agents/developer/contract.yaml +6 -0
- package/.harness/agents/developer/prompt.md +11 -6
- package/.harness/agents/tester/contract.yaml +3 -0
- package/.harness/agents/tester/prompt.md +11 -0
- package/.harness/gate/checks/build-gates-frontend.js +94 -0
- package/.harness/gate/checks/test-compliance.js +170 -4
- package/.harness/skills/code-review/SKILL.md +10 -0
- package/.harness/workflow/definition.yaml +58 -9
- package/lib/index.mjs +1 -0
- package/package.json +1 -1
|
@@ -40,10 +40,10 @@ responsibilities:
|
|
|
40
40
|
|
|
41
41
|
review_dimensions:
|
|
42
42
|
- name: "A. 代码质量"
|
|
43
|
-
weight:
|
|
43
|
+
weight: 25
|
|
44
44
|
items: [命名, 复杂度, 类型, 错误处理, DRY]
|
|
45
45
|
- name: "B. 规范遵循"
|
|
46
|
-
weight:
|
|
46
|
+
weight: 10
|
|
47
47
|
items: [Commit, 分支, PR描述, 文档]
|
|
48
48
|
- name: "C. 安全与风险"
|
|
49
49
|
weight: 25
|
|
@@ -52,8 +52,19 @@ review_dimensions:
|
|
|
52
52
|
weight: 10
|
|
53
53
|
items: [N+1, 内存, 体积]
|
|
54
54
|
- name: "E. 测试覆盖"
|
|
55
|
-
weight:
|
|
56
|
-
items: [
|
|
55
|
+
weight: 30
|
|
56
|
+
items: [单元测试, 覆盖率, 测试质量, 测试用例文档, 前端组件测试, 后端Service测试]
|
|
57
|
+
|
|
58
|
+
fail_fast_conditions:
|
|
59
|
+
- code: "E4-NO-TEST-CASES"
|
|
60
|
+
condition: "缺少测试用例文档(.harness/doc/test-cases/ 下无 feature-points.md)"
|
|
61
|
+
action: "直接 FAIL"
|
|
62
|
+
- code: "E5-NO-VUE-TEST"
|
|
63
|
+
condition: "新增/修改的 Vue 组件缺少对应测试文件"
|
|
64
|
+
action: "直接 FAIL"
|
|
65
|
+
- code: "E5-NO-JAVA-TEST"
|
|
66
|
+
condition: "新增/修改的 Service 类缺少对应测试类"
|
|
67
|
+
action: "直接 FAIL"
|
|
57
68
|
|
|
58
69
|
constraints:
|
|
59
70
|
- 不改代码(只评论)
|
|
@@ -41,6 +41,9 @@ dependencies:
|
|
|
41
41
|
responsibilities:
|
|
42
42
|
- 按设计文档编写代码
|
|
43
43
|
- 编写/更新对应单元测试
|
|
44
|
+
- 前端组件必须有 .test.ts/.spec.ts 文件(Vitest/Jest)
|
|
45
|
+
- 后端 Service 必须有对应 Test 类(JUnit5+Mockito)
|
|
46
|
+
- 生成测试用例文档(test-case-designer Skill → .harness/doc/test-cases/)
|
|
44
47
|
- 运行 Build/Test/Lint 三步自检
|
|
45
48
|
- 更新 dev-map(如有结构性变化)
|
|
46
49
|
- 规范 Commit(Conventional Commits + 关联 Issue)
|
|
@@ -57,3 +60,6 @@ quality_redlines:
|
|
|
57
60
|
- 裸any类型(无注释说明)
|
|
58
61
|
- console.log/debugger残留
|
|
59
62
|
- 跳过任意一步自检流程
|
|
63
|
+
- 前端组件无测试文件
|
|
64
|
+
- 后端 Service 无测试类
|
|
65
|
+
- 缺少测试用例文档
|
|
@@ -51,18 +51,23 @@ dependencies:
|
|
|
51
51
|
1. 阅读设计文档 + dev-map 相关部分
|
|
52
52
|
2. 实现核心逻辑
|
|
53
53
|
3. 编写/更新对应单元测试
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
54
|
+
- **前端**: 每个 .vue 组件必须有 .test.ts/.spec.ts 文件(Vitest/Jest)
|
|
55
|
+
- **后端**: 每个 Service/ServiceImpl 必须有对应 Test 类(JUnit5+Mockito)
|
|
56
|
+
4. 生成测试用例文档(test-case-designer Skill)
|
|
57
|
+
5. 运行 Build Skill → 编译通过?
|
|
58
|
+
6. 运行 Test Unit Skill → 测试通过 + 覆盖率达标?
|
|
59
|
+
7. 运行 Lint Check Skill → 零 warning?
|
|
60
|
+
8. 更新 dev-map(如有结构性变化)
|
|
61
|
+
9. 规范 Commit(Conventional Commits + 关联 Issue)
|
|
62
|
+
10. 创建 PR/MR
|
|
60
63
|
|
|
61
64
|
## 质量红线(触碰即打回)
|
|
62
65
|
- 硬编码密钥/token
|
|
63
66
|
- 裸 `any` 类型(无注释说明)
|
|
64
67
|
- console.log / debugger 残留
|
|
65
68
|
- 跳过任意一步自检流程
|
|
69
|
+
- **缺少单元测试文件**(前端组件无 .test.ts / 后端 Service 无 Test 类)
|
|
70
|
+
- **缺少测试用例文档**(.harness/doc/test-cases/ 下无对应 feature-points.md)
|
|
66
71
|
|
|
67
72
|
## 你的约束
|
|
68
73
|
- ❌ 不改需求和设计文档
|
|
@@ -40,6 +40,9 @@ outputFormat: .harness/doc/test-report/test-report-{task-id}.md
|
|
|
40
40
|
|
|
41
41
|
responsibilities:
|
|
42
42
|
- 制定测试策略和计划
|
|
43
|
+
- 验证测试用例文档完整性(.harness/doc/test-cases/ 下有 feature-points.md)
|
|
44
|
+
- 验证前端组件测试覆盖(每个 .vue 有对应 .test.ts/.spec.ts)
|
|
45
|
+
- 验证后端 Service 测试覆盖(每个 Service 有对应 Test 类)
|
|
43
46
|
- 设计和执行各类测试
|
|
44
47
|
- 记录和跟踪缺陷
|
|
45
48
|
- 输出完整的测试报告
|
|
@@ -50,7 +50,9 @@ outputFormat: .harness/doc/test-report/test-report-{task-id}.md
|
|
|
50
50
|
## 测试覆盖矩阵
|
|
51
51
|
| 测试类型 | 执行时机 | 负责人 | 必须? | 引用Skill |
|
|
52
52
|
|----------|----------|--------|-------|-----------|
|
|
53
|
+
| 测试用例验证 | 测试开始前 | Tester | ✅ 必须 | test-case-designer |
|
|
53
54
|
| 单元测试 | 开发时 | Developer | ✅ 必须 | test-unit |
|
|
55
|
+
| 前端组件测试 | 开发时 | Developer | ✅ 必须 | test-unit |
|
|
54
56
|
| 静态检查 | 每次 commit | Dev/CI | ✅ 必须 | lint-check |
|
|
55
57
|
| API 集成测试 | PR 前 | Tester | ✅ 必须 | test-api |
|
|
56
58
|
| E2E 关键路径 | 预发布前 | Tester | ✅ 必须 | test-e2e |
|
|
@@ -73,6 +75,15 @@ outputFormat: .harness/doc/test-report/test-report-{task-id}.md
|
|
|
73
75
|
- 核心 E2E 路径全部通过
|
|
74
76
|
- API 契约无破坏性变更
|
|
75
77
|
- 测试覆盖率不低于基线
|
|
78
|
+
- **测试用例文档存在且与验收标准对齐**(.harness/doc/test-cases/ 下有 feature-points.md)
|
|
79
|
+
- **前端组件测试覆盖 ≥80%**(每个 .vue 有对应 .test.ts/.spec.ts)
|
|
80
|
+
|
|
81
|
+
## 测试执行前置检查
|
|
82
|
+
在正式执行测试前,必须验证以下前置条件:
|
|
83
|
+
1. **测试用例文档检查** — 确认 `.harness/doc/test-cases/{需求名称}/feature-points.md` 存在
|
|
84
|
+
2. **前端测试文件存在性** — 扫描 src/ 下 .vue 文件,确认每个组件有对应测试文件
|
|
85
|
+
3. **后端测试类存在性** — 扫描 Service/ServiceImpl 类,确认有对应 Test 类
|
|
86
|
+
4. 任何前置条件不满足 → 记录为 P1 缺陷并要求开发补充
|
|
76
87
|
|
|
77
88
|
## 你的约束
|
|
78
89
|
- ❌ 不改被测代码
|
|
@@ -134,6 +134,66 @@ async function run(options = {}) {
|
|
|
134
134
|
);
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
// === F-B6: 前端单元测试执行 ===
|
|
138
|
+
if (pkgJson?.scripts?.test || pkgJson?.scripts?.['test:unit'] || pkgJson?.scripts?.['test:coverage']) {
|
|
139
|
+
const testCmd = pkgJson.scripts['test:coverage'] || pkgJson.scripts['test:unit'] || pkgJson.scripts.test;
|
|
140
|
+
await runCheck(
|
|
141
|
+
'前端单元测试 (npm run test)',
|
|
142
|
+
`${pm} run test 2>&1`,
|
|
143
|
+
{ timeout: 180000, blocking: true, suggestion: '前端项目必须有单元测试且全部通过。使用 Vitest 或 Jest 编写组件测试。参考 skills/test-unit/SKILL.md' }
|
|
144
|
+
);
|
|
145
|
+
} else {
|
|
146
|
+
// 无测试脚本 → 严重问题
|
|
147
|
+
issues.push({
|
|
148
|
+
code: 'F-B6',
|
|
149
|
+
severity: 'error',
|
|
150
|
+
message: '前端项目缺少测试脚本 (package.json 中无 test/test:unit/test:coverage 脚本)',
|
|
151
|
+
suggestion: '必须添加测试脚本。推荐配置: "test": "vitest run", "test:coverage": "vitest run --coverage"'
|
|
152
|
+
});
|
|
153
|
+
checks.push({ name: '前端单元测试', status: 'fail', duration_ms: 0, error: '无测试脚本' });
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// === F-B7: 前端组件测试文件存在性检查 ===
|
|
157
|
+
const srcDir = path.join(process.cwd(), 'src');
|
|
158
|
+
if (fs.existsSync(srcDir)) {
|
|
159
|
+
const vueFiles = findVueFiles(srcDir);
|
|
160
|
+
const testFileNames = new Set(
|
|
161
|
+
findTestFiles(srcDir).map(f => {
|
|
162
|
+
const base = path.basename(f);
|
|
163
|
+
return base.replace(/\.(test|spec)\.(ts|js)$/, '.vue');
|
|
164
|
+
})
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
const untestedVue = vueFiles.filter(vf => {
|
|
168
|
+
const base = path.basename(vf);
|
|
169
|
+
return !testFileNames.has(base);
|
|
170
|
+
}).map(vf => vf.replace(srcDir + path.sep, ''));
|
|
171
|
+
|
|
172
|
+
if (untestedVue.length > 0 && vueFiles.length > 0) {
|
|
173
|
+
const coveragePercent = ((vueFiles.length - untestedVue.length) / vueFiles.length * 100).toFixed(1);
|
|
174
|
+
if (parseFloat(coveragePercent) < 80) {
|
|
175
|
+
issues.push({
|
|
176
|
+
code: 'F-B7',
|
|
177
|
+
severity: 'error',
|
|
178
|
+
message: `前端组件测试覆盖不足: ${coveragePercent}% — ${untestedVue.length} 个组件缺少测试文件`,
|
|
179
|
+
details: untestedVue.slice(0, 10),
|
|
180
|
+
suggestion: '每个 .vue 组件必须有对应的 .test.ts 或 .spec.ts 文件。这是 Gate 强制检查项。'
|
|
181
|
+
});
|
|
182
|
+
} else {
|
|
183
|
+
issues.push({
|
|
184
|
+
code: 'F-B7',
|
|
185
|
+
severity: 'warning',
|
|
186
|
+
message: `前端组件测试覆盖 ${coveragePercent}%,${untestedVue.length} 个组件缺少测试`,
|
|
187
|
+
details: untestedVue.slice(0, 5),
|
|
188
|
+
suggestion: '建议为所有组件补充测试'
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
checks.push({ name: '前端组件测试覆盖', status: parseFloat(coveragePercent) < 80 ? 'fail' : 'pass', duration_ms: 0 });
|
|
192
|
+
} else if (vueFiles.length > 0) {
|
|
193
|
+
checks.push({ name: '前端组件测试覆盖', status: 'pass', duration_ms: 0 });
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
137
197
|
// 计算结果
|
|
138
198
|
const passed = checks.filter(c => c.status === 'pass').length;
|
|
139
199
|
const failed = checks.filter(c => c.status === 'fail').length;
|
|
@@ -150,3 +210,37 @@ async function run(options = {}) {
|
|
|
150
210
|
}
|
|
151
211
|
|
|
152
212
|
module.exports = run;
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* 递归查找 .vue 文件
|
|
216
|
+
*/
|
|
217
|
+
function findVueFiles(dir, results = []) {
|
|
218
|
+
if (!fs.existsSync(dir)) return results;
|
|
219
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
220
|
+
for (const entry of entries) {
|
|
221
|
+
const fullPath = path.join(dir, entry.name);
|
|
222
|
+
if (entry.isDirectory() && !['node_modules', '.git', 'dist', 'coverage'].includes(entry.name)) {
|
|
223
|
+
findVueFiles(fullPath, results);
|
|
224
|
+
} else if (entry.name.endsWith('.vue')) {
|
|
225
|
+
results.push(fullPath);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return results;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* 递归查找测试文件 (.test.ts / .spec.ts / .test.js / .spec.js)
|
|
233
|
+
*/
|
|
234
|
+
function findTestFiles(dir, results = []) {
|
|
235
|
+
if (!fs.existsSync(dir)) return results;
|
|
236
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
237
|
+
for (const entry of entries) {
|
|
238
|
+
const fullPath = path.join(dir, entry.name);
|
|
239
|
+
if (entry.isDirectory() && !['node_modules', '.git', 'dist', 'coverage'].includes(entry.name)) {
|
|
240
|
+
findTestFiles(fullPath, results);
|
|
241
|
+
} else if (/\.(test|spec)\.(ts|js)$/.test(entry.name)) {
|
|
242
|
+
results.push(fullPath);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return results;
|
|
246
|
+
}
|
|
@@ -2,13 +2,16 @@
|
|
|
2
2
|
* Gate Check Category C: 测试合规 (test-compliance)
|
|
3
3
|
*
|
|
4
4
|
* 检查项:
|
|
5
|
-
* - 单元测试执行与覆盖率收集
|
|
6
|
-
* -
|
|
7
|
-
* -
|
|
8
|
-
* -
|
|
5
|
+
* - C1: 单元测试执行与覆盖率收集
|
|
6
|
+
* - C2: 测试数量趋势(如果有基线)
|
|
7
|
+
* - C3: 测试用例文档完整性(.harness/doc/test-cases/)
|
|
8
|
+
* - C4: 前端组件测试覆盖(每个 .vue 有对应 .test.ts/.spec.ts)
|
|
9
|
+
* - C5: 后端 Service 测试覆盖(每个 Service 有对应 Test 类)
|
|
9
10
|
*/
|
|
10
11
|
|
|
11
12
|
const { execSync } = require('child_process');
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
12
15
|
|
|
13
16
|
async function run(options = {}) {
|
|
14
17
|
const issues = [];
|
|
@@ -100,6 +103,151 @@ async function run(options = {}) {
|
|
|
100
103
|
testResults.test_count = testResults.unit.total;
|
|
101
104
|
}
|
|
102
105
|
|
|
106
|
+
// C3: 测试用例文档完整性检查
|
|
107
|
+
const cwd = process.cwd();
|
|
108
|
+
const testCasesDir = path.join(cwd, '.harness', 'doc', 'test-cases');
|
|
109
|
+
let testCaseStatus = { exists: false, files: [] };
|
|
110
|
+
|
|
111
|
+
if (fs.existsSync(testCasesDir)) {
|
|
112
|
+
const entries = fs.readdirSync(testCasesDir, { withFileTypes: true });
|
|
113
|
+
const requirementDirs = entries.filter(e => e.isDirectory());
|
|
114
|
+
|
|
115
|
+
for (const dir of requirementDirs) {
|
|
116
|
+
const dirPath = path.join(testCasesDir, dir.name);
|
|
117
|
+
const files = fs.readdirSync(dirPath);
|
|
118
|
+
const hasFeaturePoints = files.some(f => f === 'feature-points.md');
|
|
119
|
+
const hasCyclePlan = files.some(f => f === 'cycle-plan.md');
|
|
120
|
+
const hasSpecialPlan = files.some(f => f === 'special-plan.md');
|
|
121
|
+
|
|
122
|
+
testCaseStatus.files.push({
|
|
123
|
+
requirement: dir.name,
|
|
124
|
+
feature_points: hasFeaturePoints,
|
|
125
|
+
cycle_plan: hasCyclePlan,
|
|
126
|
+
special_plan: hasSpecialPlan
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
if (!hasFeaturePoints) {
|
|
130
|
+
issues.push({
|
|
131
|
+
code: 'C3',
|
|
132
|
+
severity: 'error',
|
|
133
|
+
message: `需求 "${dir.name}" 缺少功能点测试用例文档 (feature-points.md)`,
|
|
134
|
+
suggestion: '使用 test-case-designer Skill 生成标准化测试用例文档'
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
testCaseStatus.exists = requirementDirs.length > 0;
|
|
140
|
+
|
|
141
|
+
if (requirementDirs.length === 0) {
|
|
142
|
+
issues.push({
|
|
143
|
+
code: 'C3',
|
|
144
|
+
severity: 'warning',
|
|
145
|
+
message: '测试用例文档目录为空,没有找到任何需求的测试用例',
|
|
146
|
+
suggestion: '在开发阶段应使用 test-case-designer Skill 生成测试用例文档'
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
} else {
|
|
150
|
+
issues.push({
|
|
151
|
+
code: 'C3',
|
|
152
|
+
severity: 'error',
|
|
153
|
+
message: '测试用例文档目录不存在 (.harness/doc/test-cases/)',
|
|
154
|
+
suggestion: '开发阶段必须使用 test-case-designer Skill 生成测试用例文档。这是 Gate C3 强制检查项。'
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
testResults.test_cases = testCaseStatus;
|
|
158
|
+
|
|
159
|
+
// C4: 前端组件测试覆盖检查
|
|
160
|
+
const srcDir = path.join(cwd, 'src');
|
|
161
|
+
if (fs.existsSync(srcDir)) {
|
|
162
|
+
const vueFiles = findFiles(srcDir, '.vue');
|
|
163
|
+
const testFiles = new Set(findFiles(srcDir, '.test.ts')
|
|
164
|
+
.concat(findFiles(srcDir, '.spec.ts'))
|
|
165
|
+
.concat(findFiles(srcDir, '.test.js'))
|
|
166
|
+
.concat(findFiles(srcDir, '.spec.js'))
|
|
167
|
+
.map(f => path.basename(f, path.extname(f)).replace(/\.(test|spec)$/, '')));
|
|
168
|
+
|
|
169
|
+
const untestedVueComponents = [];
|
|
170
|
+
for (const vueFile of vueFiles) {
|
|
171
|
+
const componentName = path.basename(vueFile, '.vue');
|
|
172
|
+
// 检查是否有对应的测试文件(同名或含组件名的测试文件)
|
|
173
|
+
const hasTest = testFiles.has(componentName) ||
|
|
174
|
+
Array.from(testFiles).some(t => t.includes(componentName));
|
|
175
|
+
if (!hasTest) {
|
|
176
|
+
untestedVueComponents.push(vueFile.replace(srcDir + path.sep, ''));
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
testResults.frontend_coverage = {
|
|
181
|
+
total_components: vueFiles.length,
|
|
182
|
+
tested_components: vueFiles.length - untestedVueComponents.length,
|
|
183
|
+
untested: untestedVueComponents
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
if (untestedVueComponents.length > 0 && vueFiles.length > 0) {
|
|
187
|
+
const coveragePercent = ((vueFiles.length - untestedVueComponents.length) / vueFiles.length * 100).toFixed(1);
|
|
188
|
+
|
|
189
|
+
if (parseFloat(coveragePercent) < 80) {
|
|
190
|
+
issues.push({
|
|
191
|
+
code: 'C4',
|
|
192
|
+
severity: 'error',
|
|
193
|
+
message: `前端组件测试覆盖不足: ${coveragePercent}% (${vueFiles.length - untestedVueComponents.length}/${vueFiles.length}) — ${untestedVueComponents.length} 个组件缺少测试`,
|
|
194
|
+
details: untestedVueComponents.slice(0, 10),
|
|
195
|
+
suggestion: '每个 Vue 组件必须有对应的 .test.ts 或 .spec.ts 文件。请使用 Vitest/Jest 编写组件测试。'
|
|
196
|
+
});
|
|
197
|
+
} else {
|
|
198
|
+
issues.push({
|
|
199
|
+
code: 'C4',
|
|
200
|
+
severity: 'warning',
|
|
201
|
+
message: `前端组件测试覆盖 ${coveragePercent}%,${untestedVueComponents.length} 个组件缺少测试`,
|
|
202
|
+
details: untestedVueComponents.slice(0, 5),
|
|
203
|
+
suggestion: '建议为所有组件补充测试文件'
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// C5: 后端 Service 测试覆盖检查
|
|
210
|
+
const javaSrcDir = path.join(cwd, 'src', 'main', 'java');
|
|
211
|
+
if (fs.existsSync(javaSrcDir)) {
|
|
212
|
+
const serviceFiles = findFiles(javaSrcDir, 'ServiceImpl.java')
|
|
213
|
+
.concat(findFiles(javaSrcDir, 'Service.java').filter(f => !f.includes('Impl')));
|
|
214
|
+
const testSrcDir = path.join(cwd, 'src', 'test', 'java');
|
|
215
|
+
|
|
216
|
+
let testClassNames = new Set();
|
|
217
|
+
if (fs.existsSync(testSrcDir)) {
|
|
218
|
+
const testJavaFiles = findFiles(testSrcDir, 'Test.java')
|
|
219
|
+
.concat(findFiles(testSrcDir, 'Tests.java'));
|
|
220
|
+
testClassNames = new Set(testJavaFiles.map(f => path.basename(f)));
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const untestedServices = [];
|
|
224
|
+
for (const svcFile of serviceFiles) {
|
|
225
|
+
const svcName = path.basename(svcFile, '.java');
|
|
226
|
+
const expectedTestName = `${svcName}Test.java`;
|
|
227
|
+
const expectedTestNameAlt = `${svcName}Tests.java`;
|
|
228
|
+
const hasTest = testClassNames.has(expectedTestName) || testClassNames.has(expectedTestNameAlt);
|
|
229
|
+
if (!hasTest) {
|
|
230
|
+
untestedServices.push(svcFile.replace(javaSrcDir + path.sep, ''));
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
testResults.backend_coverage = {
|
|
235
|
+
total_services: serviceFiles.length,
|
|
236
|
+
tested_services: serviceFiles.length - untestedServices.length,
|
|
237
|
+
untested: untestedServices
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
if (untestedServices.length > 0 && serviceFiles.length > 0) {
|
|
241
|
+
issues.push({
|
|
242
|
+
code: 'C5',
|
|
243
|
+
severity: 'error',
|
|
244
|
+
message: `后端 Service 测试覆盖不足: ${serviceFiles.length - untestedServices.length}/${serviceFiles.length} — ${untestedServices.length} 个 Service 缺少测试类`,
|
|
245
|
+
details: untestedServices.slice(0, 10),
|
|
246
|
+
suggestion: '每个 Service/ServiceImpl 必须有对应的 Test 类(JUnit5+Mockito)。参考 skills/test-unit/SKILL.md'
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
103
251
|
// 计算状态
|
|
104
252
|
const hasError = issues.some(i => i.severity === 'error');
|
|
105
253
|
|
|
@@ -111,4 +259,22 @@ async function run(options = {}) {
|
|
|
111
259
|
};
|
|
112
260
|
}
|
|
113
261
|
|
|
262
|
+
/**
|
|
263
|
+
* 递归查找指定扩展名的文件
|
|
264
|
+
*/
|
|
265
|
+
function findFiles(dir, extension, results = []) {
|
|
266
|
+
if (!fs.existsSync(dir)) return results;
|
|
267
|
+
|
|
268
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
269
|
+
for (const entry of entries) {
|
|
270
|
+
const fullPath = path.join(dir, entry.name);
|
|
271
|
+
if (entry.isDirectory() && !['node_modules', '.git', 'dist', 'target', 'coverage'].includes(entry.name)) {
|
|
272
|
+
findFiles(fullPath, extension, results);
|
|
273
|
+
} else if (entry.name.endsWith(extension)) {
|
|
274
|
+
results.push(fullPath);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return results;
|
|
278
|
+
}
|
|
279
|
+
|
|
114
280
|
module.exports = run;
|
|
@@ -61,6 +61,16 @@ enabled: true
|
|
|
61
61
|
| E1 | **单元测试** | 新增/修改代码有对应单测 | ⭐⭐⭐ |
|
|
62
62
|
| E2 | **覆盖率** | 新代码覆盖率 ≥ 85% | ⭐⭐⭐ |
|
|
63
63
|
| E3 | **测试质量** | 测试有意义(非假断言)| ⭐⭐ |
|
|
64
|
+
| E4 | **测试用例文档** | `.harness/doc/test-cases/` 下有 feature-points.md | ⭐⭐⭐ |
|
|
65
|
+
| E5 | **前端组件测试** | 每个 .vue 文件有对应 .test.ts/.spec.ts | ⭐⭐⭐ |
|
|
66
|
+
| E5 | **后端 Service 测试** | 每个 Service/ServiceImpl 有对应 Test 类 | ⭐⭐⭐ |
|
|
67
|
+
|
|
68
|
+
### E 类一票否决条件
|
|
69
|
+
|
|
70
|
+
以下情况直接 FAIL,无需计算总分:
|
|
71
|
+
- **E4-NO-TEST-CASES**: 缺少测试用例文档(.harness/doc/test-cases/ 下无 feature-points.md)
|
|
72
|
+
- **E5-NO-VUE-TEST**: 新增/修改的 Vue 组件缺少对应测试文件
|
|
73
|
+
- **E5-NO-JAVA-TEST**: 新增/修改的 Service 类缺少对应测试类
|
|
64
74
|
|
|
65
75
|
---
|
|
66
76
|
|
|
@@ -291,8 +291,16 @@ workflow:
|
|
|
291
291
|
required: true
|
|
292
292
|
- name: "单元测试"
|
|
293
293
|
format: "source code (.test.*)"
|
|
294
|
-
location: "src/"
|
|
294
|
+
location: "src/ 或 test/"
|
|
295
|
+
required: true
|
|
296
|
+
validation:
|
|
297
|
+
frontend: "每个 Vue 组件必须有对应 .test.ts/.spec.ts 文件(Vitest/Jest)"
|
|
298
|
+
backend: "每个 Service 类必须有对应 Test 类(JUnit5+Mockito),覆盖率 ≥80%"
|
|
299
|
+
- name: "测试用例"
|
|
300
|
+
format: "markdown (.harness/doc/test-cases/{需求名称}/)"
|
|
295
301
|
required: true
|
|
302
|
+
description: "基于 test-case-designer Skill 生成的标准化测试用例文档"
|
|
303
|
+
validation: "必须包含功能点测试用例(feature-points.md),可选拓展周期计划和专项计划"
|
|
296
304
|
- name: "Git Commit 历史"
|
|
297
305
|
source: "git_log"
|
|
298
306
|
required: true
|
|
@@ -315,18 +323,22 @@ workflow:
|
|
|
315
323
|
action: "实现核心逻辑"
|
|
316
324
|
skill: null
|
|
317
325
|
- step: 3
|
|
318
|
-
action: "
|
|
326
|
+
action: "编写/更新单元测试(前端 Vitest/Jest + 后端 JUnit5/Mockito)"
|
|
319
327
|
skill: "test-unit"
|
|
328
|
+
gate: "无测试文件 → 阻断提交,必须补充后再继续"
|
|
329
|
+
- step: 3.5
|
|
330
|
+
action: "验证测试用例文档完整性"
|
|
331
|
+
skill: "test-case-designer"
|
|
332
|
+
gate: "缺少测试用例文档 → 阻断提交,必须生成 feature-points.md 后再继续"
|
|
320
333
|
- step: 4
|
|
321
334
|
action: "运行构建 Skill(按技术栈选择)"
|
|
322
335
|
# 条件引用:检测 pom.xml → java-build;检测 package.json → vue-frontend-build
|
|
323
|
-
# 详见 skills/build.md(已废弃,保留为路由参考)
|
|
324
336
|
skill: "java-build | vue-frontend-build"
|
|
325
337
|
gate: "编译失败 → 返回 Step 2"
|
|
326
338
|
- step: 5
|
|
327
|
-
action: "运行 Test Unit Skill"
|
|
339
|
+
action: "运行 Test Unit Skill(前端 Vitest + 后端 JUnit5)"
|
|
328
340
|
skill: "test-unit"
|
|
329
|
-
gate: "测试失败 → 返回 Step 2/3"
|
|
341
|
+
gate: "测试失败 → 返回 Step 2/3 | 前端无测试 → FAIL | 后端覆盖率 <80% → WARNING"
|
|
330
342
|
- step: 6
|
|
331
343
|
action: "运行 Lint Check Skill"
|
|
332
344
|
skill: "lint-check"
|
|
@@ -406,11 +418,11 @@ workflow:
|
|
|
406
418
|
review_dimensions:
|
|
407
419
|
- id: quality
|
|
408
420
|
name: "代码质量"
|
|
409
|
-
weight:
|
|
421
|
+
weight: 25
|
|
410
422
|
checks: ["A1-A6"]
|
|
411
423
|
- id: compliance
|
|
412
424
|
name: "规范遵循"
|
|
413
|
-
weight:
|
|
425
|
+
weight: 10
|
|
414
426
|
checks: ["B1-B5"]
|
|
415
427
|
- id: security
|
|
416
428
|
name: "安全与风险"
|
|
@@ -422,8 +434,21 @@ workflow:
|
|
|
422
434
|
checks: ["D1-D4"]
|
|
423
435
|
- id: testing
|
|
424
436
|
name: "测试覆盖"
|
|
425
|
-
weight:
|
|
426
|
-
checks: ["E1-
|
|
437
|
+
weight: 30
|
|
438
|
+
checks: ["E1-E5"]
|
|
439
|
+
# E4: 测试用例文档完整性(.harness/doc/test-cases/ 下有对应文件)
|
|
440
|
+
# E5: 前端组件测试存在性(每个 .vue 文件有对应 .test.ts/.spec.ts)
|
|
441
|
+
|
|
442
|
+
fail_fast_conditions:
|
|
443
|
+
- condition: "新增/修改的 Vue 组件缺少对应测试文件"
|
|
444
|
+
action: "FAIL — 前端组件必须有单元测试"
|
|
445
|
+
code: "E5-NO-VUE-TEST"
|
|
446
|
+
- condition: "新增/修改的 Service 类缺少对应测试类"
|
|
447
|
+
action: "FAIL — 后端 Service 必须有单元测试"
|
|
448
|
+
code: "E5-NO-JAVA-TEST"
|
|
449
|
+
- condition: "缺少测试用例文档(.harness/doc/test-cases/)"
|
|
450
|
+
action: "FAIL — 必须有标准化测试用例文档"
|
|
451
|
+
code: "E4-NO-TEST-CASES"
|
|
427
452
|
|
|
428
453
|
- id: testing
|
|
429
454
|
name: "测试验证"
|
|
@@ -448,6 +473,10 @@ workflow:
|
|
|
448
473
|
- name: "审查报告"
|
|
449
474
|
from: "code_review"
|
|
450
475
|
required: true
|
|
476
|
+
- name: "测试用例文档"
|
|
477
|
+
from: "development"
|
|
478
|
+
required: true
|
|
479
|
+
path: ".harness/doc/test-cases/{需求名称}/"
|
|
451
480
|
- name: "Skill 测试动作集"
|
|
452
481
|
source: "skills/test-*.md"
|
|
453
482
|
required: true
|
|
@@ -476,6 +505,26 @@ workflow:
|
|
|
476
505
|
required: true
|
|
477
506
|
# 前端: Vitest/Jest + coverage ≥80% | 后端: JUnit5/Mockito + JaCoCo ≥80%
|
|
478
507
|
threshold: "coverage >= baseline (前端 Vitest / 后端 JaCoCo)"
|
|
508
|
+
frontend_specific:
|
|
509
|
+
framework: "Vitest 或 Jest"
|
|
510
|
+
component_test: "每个 .vue 组件必须有对应 .test.ts/.spec.ts 文件"
|
|
511
|
+
coverage_threshold: "branches ≥80%, functions ≥80%, lines ≥80%"
|
|
512
|
+
blocking: "无测试文件 → Gate FAIL"
|
|
513
|
+
backend_specific:
|
|
514
|
+
framework: "JUnit5 + Mockito"
|
|
515
|
+
service_test: "每个 Service 类必须有对应 Test 类"
|
|
516
|
+
coverage_threshold: "JaCoCo 行覆盖率 ≥80%, 分支覆盖率 ≥80%"
|
|
517
|
+
blocking: "无测试类 → Gate FAIL"
|
|
518
|
+
- name: "test_case_validation"
|
|
519
|
+
skill: "test-case-designer"
|
|
520
|
+
executor: "tester_agent"
|
|
521
|
+
required: true
|
|
522
|
+
# 验证开发阶段产出的测试用例文档与代码实现的覆盖对齐
|
|
523
|
+
threshold: "功能点测试用例覆盖 ≥ 90% 的验收标准"
|
|
524
|
+
validation_checks:
|
|
525
|
+
- "feature-points.md 文件存在且非空"
|
|
526
|
+
- "功能点测试用例与验收标准对齐"
|
|
527
|
+
- "代码中的单元测试覆盖测试用例中的关键场景"
|
|
479
528
|
- name: "api_integration"
|
|
480
529
|
skill: "test-api"
|
|
481
530
|
executor: "tester_agent"
|
package/lib/index.mjs
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 多适配器架构:检测 AI 工具 → 转换规则格式 → 注入到对应位置
|
|
5
5
|
*
|
|
6
|
+
* v1.12.5: 强制测试用例和前端单元测试验证 — workflow definition 增加 step 3.5 测试用例检查、E4/E5 审查项、fail_fast_conditions;Gate C3 测试用例文档检查、C4 前端组件测试覆盖、C5 后端 Service 测试覆盖;build-gates-frontend 增加 F-B6 前端单元测试执行、F-B7 组件测试文件检查
|
|
6
7
|
* v1.12.4: 修复 Qoder commands 输出路径从扁平结构改为 commands/js/ 二级目录,与 codebuddy/cursor/trae 保持一致
|
|
7
8
|
* v1.12.3: 新增 test-case-designer Skill(功能点/周期计划/专项计划三种测试用例模板);修改 DDB-02 规则细化测试执行方案与测试用例设计边界;tester Agent 增加设计阶段测试用例编写职责
|
|
8
9
|
* v1.12.1: 操作手册新增 8 张 Mermaid 可视化图表(三层约束架构图、jsspec三步流程图、标准流程总览图、Agent协作时序图、门禁检查流程图、init注入流程图、Agent调度架构图、交付物流转图)
|
package/package.json
CHANGED