jsharness 1.0.0
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/README.md +199 -0
- package/.harness/agents/code-reviewer/contract.yaml +64 -0
- package/.harness/agents/developer/contract.yaml +72 -0
- package/.harness/agents/gate-controller/contract.yaml +64 -0
- package/.harness/agents/project-manager/contract.yaml +77 -0
- package/.harness/agents/prompt-templates.md +352 -0
- package/.harness/agents/requirements-analyst/contract.yaml +64 -0
- package/.harness/agents/solution-designer/contract.yaml +75 -0
- package/.harness/agents/tester/contract.yaml +92 -0
- package/.harness/config/models.yaml +67 -0
- package/.harness/dev-map/backend/api-definition.md +131 -0
- package/.harness/dev-map/backend/auth-security.md +131 -0
- package/.harness/dev-map/backend/conventions-java.md +471 -0
- package/.harness/dev-map/backend/conventions.md +192 -0
- package/.harness/dev-map/backend/database.md +106 -0
- package/.harness/dev-map/backend/structure.md +140 -0
- package/.harness/dev-map/decisions.md +275 -0
- package/.harness/dev-map/frontend/api-integration.md +139 -0
- package/.harness/dev-map/frontend/components.md +178 -0
- package/.harness/dev-map/frontend/conventions.md +416 -0
- package/.harness/dev-map/frontend/state-management.md +170 -0
- package/.harness/dev-map/frontend/structure.md +103 -0
- package/.harness/dev-map/overview.md +267 -0
- package/.harness/docs/integration-test-plan.md +248 -0
- package/.harness/docs/team-guidelines/README.md +161 -0
- package/.harness/docs/team-guidelines/arch-team.md +811 -0
- package/.harness/docs/team-guidelines/collaboration.md +556 -0
- package/.harness/docs/team-guidelines/pm-team.md +337 -0
- package/.harness/docs/team-guidelines/qa-team.md +562 -0
- package/.harness/docs/team-guidelines/rd-team.md +714 -0
- package/.harness/docs/training-materials.md +280 -0
- package/.harness/gate/baseline.js +220 -0
- package/.harness/gate/checks/build-gates-frontend.js +152 -0
- package/.harness/gate/checks/build-gates-java.js +155 -0
- package/.harness/gate/checks/build-gates.js +119 -0
- package/.harness/gate/checks/engineering-consistency.js +138 -0
- package/.harness/gate/checks/security-quality.js +129 -0
- package/.harness/gate/checks/static-compliance.js +313 -0
- package/.harness/gate/checks/test-compliance.js +114 -0
- package/.harness/gate/index.js +315 -0
- package/.harness/mcp/config.yaml +435 -0
- package/.harness/rules/global/coding-standard.md +232 -0
- package/.harness/rules/global/commit-convention.md +165 -0
- package/.harness/rules/global/process-discipline.md +192 -0
- package/.harness/rules/global/security-baseline.md +306 -0
- package/.harness/rules/project/frontend-vue3.md +293 -0
- package/.harness/rules/project/java-backend.md +460 -0
- package/.harness/rules/project/web-specific.md +231 -0
- package/.harness/skills/build.md +192 -0
- package/.harness/skills/code-review.md +251 -0
- package/.harness/skills/docker-build.md +227 -0
- package/.harness/skills/docs-update.md +164 -0
- package/.harness/skills/java-build.md +261 -0
- package/.harness/skills/lint-check.md +482 -0
- package/.harness/skills/task-board-maintenance.md +105 -0
- package/.harness/skills/test-api.md +461 -0
- package/.harness/skills/test-e2e.md +431 -0
- package/.harness/skills/test-unit.md +649 -0
- package/.harness/skills/vue-frontend-build.md +344 -0
- package/.harness/specs/quality-feedback/implementation-guide.md +350 -0
- package/.harness/task-board.md +121 -0
- package/.harness/workflow/definition.yaml +504 -0
- package/.harness/workflow/validate.js +320 -0
- package/.harness/workflow/variants.yaml +253 -0
- package/README.md +237 -0
- package/bin/jsharness.js +53 -0
- package/lib/index.mjs +778 -0
- package/package.json +1 -0
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gate Check Category A: 静态合规检查 (static-compliance)
|
|
3
|
+
*
|
|
4
|
+
* 检查项:
|
|
5
|
+
* - ESLint 通过
|
|
6
|
+
* - Prettier 格式化一致
|
|
7
|
+
* - console.log / debugger 残留检测
|
|
8
|
+
* - 硬编码字符串检测
|
|
9
|
+
* - TODO/FIXME 残留检测
|
|
10
|
+
* - 命名规范检查
|
|
11
|
+
* - 文件长度限制检查
|
|
12
|
+
* - Secret 泄露检测
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const { execSync } = require('child_process');
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 执行 A 类静态合规检查
|
|
19
|
+
* @param {Object} options - Gate 入口传递的选项
|
|
20
|
+
* @returns {Object} 检查结果 { status, score, issues[] }
|
|
21
|
+
*/
|
|
22
|
+
async function run(options = {}) {
|
|
23
|
+
const issues = [];
|
|
24
|
+
let totalChecks = 0;
|
|
25
|
+
let passedChecks = 0;
|
|
26
|
+
|
|
27
|
+
// ----------------------------------------------------------
|
|
28
|
+
// A1: ESLint 检查
|
|
29
|
+
// ----------------------------------------------------------
|
|
30
|
+
totalChecks++;
|
|
31
|
+
try {
|
|
32
|
+
const eslintOutput = execSync('npx eslint . --ext .ts,.tsx,.js,.jsx --format json 2>/dev/null || echo "[]"', {
|
|
33
|
+
encoding: 'utf-8',
|
|
34
|
+
timeout: 60000,
|
|
35
|
+
cwd: process.cwd()
|
|
36
|
+
});
|
|
37
|
+
const eslintResults = JSON.parse(eslintOutput.replace(/\[|\]/g, m => m));
|
|
38
|
+
const errors = Array.isArray(eslintResults)
|
|
39
|
+
? eslintResults.flatMap(f => f.messages || []).filter(m => m.severity === 2)
|
|
40
|
+
: [];
|
|
41
|
+
|
|
42
|
+
if (errors.length === 0) {
|
|
43
|
+
passedChecks++;
|
|
44
|
+
} else {
|
|
45
|
+
issues.push({
|
|
46
|
+
code: 'A1',
|
|
47
|
+
severity: 'error',
|
|
48
|
+
message: `ESLint 发现 ${errors.length} 个错误`,
|
|
49
|
+
details: errors.slice(0, 5).map(e => `${e.line}:${e.column} - ${e.message}`)
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
} catch (e) {
|
|
53
|
+
issues.push({ code: 'A1', severity: 'warning', message: `ESLint 执行异常: ${e.message.slice(0, 100)}` });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ----------------------------------------------------------
|
|
57
|
+
// A2: Prettier 格式化检查
|
|
58
|
+
// ----------------------------------------------------------
|
|
59
|
+
totalChecks++;
|
|
60
|
+
try {
|
|
61
|
+
execSync('npx prettier --check "src/**/*.{ts,tsx,js,jsx,css,json,md}" 2>&1', {
|
|
62
|
+
encoding: 'utf-8',
|
|
63
|
+
timeout: 30000,
|
|
64
|
+
stdio: 'pipe'
|
|
65
|
+
});
|
|
66
|
+
passedChecks++;
|
|
67
|
+
} catch (e) {
|
|
68
|
+
const output = e.stdout?.toString() || e.stderr?.toString() || '';
|
|
69
|
+
const fileCount = (output.match(/[\w/.-]+\.\w+/g) || []).length;
|
|
70
|
+
issues.push({
|
|
71
|
+
code: 'A2',
|
|
72
|
+
severity: 'warning',
|
|
73
|
+
message: `${fileCount || '多个'} 文件格式不符合 Prettier 规范`,
|
|
74
|
+
suggestion: '运行 `npx prettier --write "src/**/*.{ts,tsx}"` 自动修复'
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ----------------------------------------------------------
|
|
79
|
+
// A3: console.log / debugger 残留检测
|
|
80
|
+
// ----------------------------------------------------------
|
|
81
|
+
totalChecks++;
|
|
82
|
+
try {
|
|
83
|
+
const consolePattern = /\bconsole\.(log|debug|info|warn)\(/g;
|
|
84
|
+
const debuggerPattern = /\bdebugger\b/g;
|
|
85
|
+
const files = findSourceFiles();
|
|
86
|
+
let violations = [];
|
|
87
|
+
|
|
88
|
+
for (const file of files) {
|
|
89
|
+
const content = require('fs').readFileSync(file, 'utf-8');
|
|
90
|
+
let match;
|
|
91
|
+
while ((match = consolePattern.exec(content)) !== null) {
|
|
92
|
+
const lineNum = content.substring(0, match.index).split('\n').length;
|
|
93
|
+
violations.push(`${file}:${lineNum}: console.${match[1]}(...)`);
|
|
94
|
+
}
|
|
95
|
+
while ((match = debuggerPattern.exec(content)) !== null) {
|
|
96
|
+
const lineNum = content.substring(0, match.index).split('\n').length;
|
|
97
|
+
violations.push(`${file}:line ${lineNum}: debugger statement`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// 排除测试文件和 .harness 目录中的 console
|
|
102
|
+
violations = violations.filter(v =>
|
|
103
|
+
!v.includes('.test.') && !v.includes('.spec.') && !v.includes('.harness')
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
if (violations.length === 0) {
|
|
107
|
+
passedChecks++;
|
|
108
|
+
} else {
|
|
109
|
+
issues.push({
|
|
110
|
+
code: 'A3',
|
|
111
|
+
severity: violations.length <= 3 ? 'warning' : 'error',
|
|
112
|
+
message: `发现 ${violations.length} 处 console/debugger 残留`,
|
|
113
|
+
details: violations.slice(0, 5),
|
|
114
|
+
suggestion: '生产代码中不应包含 console.log 或 debugger'
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
} catch (e) {
|
|
118
|
+
issues.push({ code: 'A3', severity: 'warning', message: `扫描异常: ${e.message.slice(0, 100)}` });
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ----------------------------------------------------------
|
|
122
|
+
// A4: 硬编码中文字符串检测(前端代码)
|
|
123
|
+
// ----------------------------------------------------------
|
|
124
|
+
totalChecks++;
|
|
125
|
+
try {
|
|
126
|
+
const files = findSourceFiles(['.tsx', '.jsx']);
|
|
127
|
+
let hardCodedStrings = [];
|
|
128
|
+
const chinesePattern = /['"`]([\u4e00-\u9fa5]+[^'"`\]]{0,50})['"`]/g; // 中文引号字符串
|
|
129
|
+
|
|
130
|
+
for (const file of files) {
|
|
131
|
+
if (file.includes('.test.') || file.includes('.spec.')) continue;
|
|
132
|
+
const content = require('fs').readFileSync(file, 'utf-8');
|
|
133
|
+
let match;
|
|
134
|
+
while ((match = chinesePattern.exec(content)) !== null) {
|
|
135
|
+
const lineNum = content.substring(0, match.index).split('\n').length;
|
|
136
|
+
hardCodedStrings.push(`${file}:${lineNum}: "${match[1]}"`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (hardCodedStrings.length === 0) {
|
|
141
|
+
passedChecks++;
|
|
142
|
+
} else {
|
|
143
|
+
issues.push({
|
|
144
|
+
code: 'A4',
|
|
145
|
+
severity: 'warning',
|
|
146
|
+
message: `发现 ${hardCodedStrings.length} 处可能硬编码的中文字符串`,
|
|
147
|
+
details: hardCodedStrings.slice(0, 5),
|
|
148
|
+
suggestion: '使用 i18n t() 函数替代硬编码文本'
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
} catch (e) {
|
|
152
|
+
// 可选检查,跳过
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ----------------------------------------------------------
|
|
156
|
+
// A5: TODO/FIXME 残留检测
|
|
157
|
+
// ----------------------------------------------------------
|
|
158
|
+
totalChecks++;
|
|
159
|
+
try {
|
|
160
|
+
const todoPattern = /(TODO|FIXME|HACK|XXX)(?!\(#\d+\))/g; // 未关联 Issue 的 TODO
|
|
161
|
+
const files = findSourceFiles();
|
|
162
|
+
let todos = [];
|
|
163
|
+
|
|
164
|
+
for (const file of files) {
|
|
165
|
+
const content = require('fs').readFileSync(file, 'utf-8');
|
|
166
|
+
let match;
|
|
167
|
+
while ((match = todoPattern.exec(content)) !== null) {
|
|
168
|
+
const lineNum = content.substring(0, match.index).split('\n').length;
|
|
169
|
+
todos.push(`${file}:${lineNum}: ${match[0]}`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (todos.length === 0) {
|
|
174
|
+
passedChecks++;
|
|
175
|
+
} else {
|
|
176
|
+
issues.push({
|
|
177
|
+
code: 'A5',
|
|
178
|
+
severity: 'warning',
|
|
179
|
+
message: `发现 ${todos.length} 处未关联 Issue 的 TODO/FIXME`,
|
|
180
|
+
details: todos.slice(0, 5),
|
|
181
|
+
suggestion: '格式应为 `TODO(#123): 说明` 或在 PR 中说明处理计划'
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
} catch (e) {
|
|
185
|
+
issues.push({ code: 'A5', severity: 'warning', message: `TODO 扫描异常` });
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ----------------------------------------------------------
|
|
189
|
+
// A6: 文件长度限制检查
|
|
190
|
+
// ----------------------------------------------------------
|
|
191
|
+
totalChecks++;
|
|
192
|
+
try {
|
|
193
|
+
const files = findSourceFiles();
|
|
194
|
+
let oversizedFiles = [];
|
|
195
|
+
|
|
196
|
+
for (const file of files) {
|
|
197
|
+
if (file.includes('.test.') || file.includes('.spec.')) continue;
|
|
198
|
+
const content = require('fs').readFileSync(file, 'utf-8');
|
|
199
|
+
const lineCount = content.split('\n').length;
|
|
200
|
+
const limit = file.endsWith('.css') || file.endsWith('.scss') ? 500 : 300;
|
|
201
|
+
|
|
202
|
+
if (lineCount > limit) {
|
|
203
|
+
oversizedFiles.push(`${file}: ${lineCount} 行 (限制: ${limit})`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (oversizedFiles.length === 0) {
|
|
208
|
+
passedChecks++;
|
|
209
|
+
} else {
|
|
210
|
+
issues.push({
|
|
211
|
+
code: 'A6',
|
|
212
|
+
severity: 'warning',
|
|
213
|
+
message: `${oversizedFiles.length} 个文件超过行数限制`,
|
|
214
|
+
details: oversizedFiles
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
} catch (e) {
|
|
218
|
+
issues.push({ code: 'A6', severity: 'warning', message: `文件长度检查异常` });
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ----------------------------------------------------------
|
|
222
|
+
// A7: Secret 泄露检测(基础版)
|
|
223
|
+
// ----------------------------------------------------------
|
|
224
|
+
totalChecks++;
|
|
225
|
+
try {
|
|
226
|
+
const secretPatterns = [
|
|
227
|
+
{ pattern: /(?:api[_-]?key|apikey|secret[_-]?key)["'\s]*[:=]\s*["'][^"']{10,}/gi, name: 'API Key' },
|
|
228
|
+
{ pattern: /password\s*[:=]\s*["'][^"']+/gi, name: 'Password' },
|
|
229
|
+
{ pattern: /(?:aws_access_key_id|aws_secret_access_key)["'\s]*[:=]\s*["'][^"']/gi, name: 'AWS Credential' },
|
|
230
|
+
{ pattern: /(?:private|rsa)[_-]?key\s*[:=]\s*["'].*BEGIN/gi, name: 'Private Key' },
|
|
231
|
+
{ pattern: /token\s*[:=]\s*["'](?:ey|sk_|pk_)[^"']{20,}/gi, name: 'Token' }
|
|
232
|
+
];
|
|
233
|
+
|
|
234
|
+
const files = findSourceFiles();
|
|
235
|
+
let secretsFound = [];
|
|
236
|
+
|
|
237
|
+
for (const { pattern, name } of secretPatterns) {
|
|
238
|
+
for (const file of files) {
|
|
239
|
+
const content = require('fs').readFileSync(file, 'utf-8');
|
|
240
|
+
let match;
|
|
241
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
242
|
+
const lineNum = content.substring(0, match.index).split('\n').length;
|
|
243
|
+
secretsFound.push(`${file}:${lineNum}: 可能的 ${name} 泄露`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// 排除示例/测试文件和 .env.example
|
|
249
|
+
secretsFound = secretsFound.filter(s =>
|
|
250
|
+
!s.includes('.example') && !s.includes('.test.') && !s.includes('.spec.') &&
|
|
251
|
+
!s.includes('.harness')
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
if (secretsFound.length === 0) {
|
|
255
|
+
passedChecks++;
|
|
256
|
+
} else {
|
|
257
|
+
issues.push({
|
|
258
|
+
code: 'A7',
|
|
259
|
+
severity: 'error', // 秘密泄露是严重问题
|
|
260
|
+
message: `🔴 发现 ${secretsFound.length} 处可能的凭证/密钥泄露!`,
|
|
261
|
+
details: secretsFound.slice(0, 5),
|
|
262
|
+
suggestion: '立即移除硬编码凭证,使用环境变量或密钥管理服务!这是安全红线。'
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
} catch (e) {
|
|
266
|
+
issues.push({ code: 'A7', severity: 'error', message: `Secret 扫描异常: ${e.message}` });
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ----------------------------------------------------------
|
|
270
|
+
// 计算得分和状态
|
|
271
|
+
// ----------------------------------------------------------
|
|
272
|
+
const score = Math.round((passedChecks / totalChecks) * 100);
|
|
273
|
+
const hasErrorIssues = issues.some(i => i.severity === 'error');
|
|
274
|
+
const status = hasErrorIssues ? 'fail' : (issues.length > 0 ? 'warning' : 'pass');
|
|
275
|
+
|
|
276
|
+
return {
|
|
277
|
+
status,
|
|
278
|
+
score: `${score}% (${passedChecks}/${totalChecks})`,
|
|
279
|
+
issues,
|
|
280
|
+
summary: { passed: passedChecks, total: totalChecks }
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* 辅助函数:查找源码文件
|
|
286
|
+
*/
|
|
287
|
+
function findSourceFiles(extensions = ['.ts', '.tsx', '.js', '.jsx']) {
|
|
288
|
+
const fs = require('fs');
|
|
289
|
+
const path = require('path');
|
|
290
|
+
const results = [];
|
|
291
|
+
const srcDir = path.join(process.cwd(), 'src');
|
|
292
|
+
|
|
293
|
+
function walk(dir) {
|
|
294
|
+
if (!fs.existsSync(dir)) return;
|
|
295
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
296
|
+
for (const entry of entries) {
|
|
297
|
+
const full = path.join(dir, entry.name);
|
|
298
|
+
if (entry.isDirectory()) {
|
|
299
|
+
// 排除 node_modules 等
|
|
300
|
+
if (!['node_modules', '.next', 'dist', 'coverage', '.git'].includes(entry.name)) {
|
|
301
|
+
walk(full);
|
|
302
|
+
}
|
|
303
|
+
} else if (extensions.some(ext => entry.name.endsWith(ext))) {
|
|
304
|
+
results.push(full);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
walk(srcDir);
|
|
310
|
+
return results;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
module.exports = run;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gate Check Category C: 测试合规 (test-compliance)
|
|
3
|
+
*
|
|
4
|
+
* 检查项:
|
|
5
|
+
* - 单元测试执行与覆盖率收集
|
|
6
|
+
* - E2E 关键路径测试(可选)
|
|
7
|
+
* - API 集成测试(可选)
|
|
8
|
+
* - 测试数量对比(基线模式)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const { execSync } = require('child_process');
|
|
12
|
+
|
|
13
|
+
async function run(options = {}) {
|
|
14
|
+
const issues = [];
|
|
15
|
+
const testResults = {};
|
|
16
|
+
|
|
17
|
+
// C1: 单元测试
|
|
18
|
+
try {
|
|
19
|
+
const output = execSync('npm run test -- --coverage --json --verbose 2>&1 || echo "{}"', {
|
|
20
|
+
encoding: 'utf-8',
|
|
21
|
+
timeout: 180000,
|
|
22
|
+
cwd: process.cwd(),
|
|
23
|
+
env: { ...process.env, CI: 'true' }
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// 解析 Jest/Vitest JSON 输出
|
|
27
|
+
let coverage = {};
|
|
28
|
+
let testStats = { total: 0, passed: 0, failed: 0 };
|
|
29
|
+
|
|
30
|
+
// 尝试从输出中提取覆盖率信息
|
|
31
|
+
const covMatch = output.matchAll(/"(\w+)"\s*:\s*{\s*"pct"\s*:\s*([\d.]+)/g);
|
|
32
|
+
for (const match of covMatch) {
|
|
33
|
+
coverage[match[1]] = parseFloat(match[2]);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// 提取测试统计
|
|
37
|
+
const testMatch = output.match(/Tests:\s*(\d+)\s+passed.*?(\d+)\s+failed/i);
|
|
38
|
+
if (testMatch) {
|
|
39
|
+
testStats.total = parseInt(testMatch[1]) + parseInt(testMatch[2]);
|
|
40
|
+
testStats.passed = parseInt(testMatch[1]);
|
|
41
|
+
testStats.failed = parseInt(testMatch[2]);
|
|
42
|
+
} else {
|
|
43
|
+
// 尝试其他格式
|
|
44
|
+
const altMatch = output.match(/(\d+) tests? .*? (\d+) passed/i);
|
|
45
|
+
if (altMatch) {
|
|
46
|
+
testStats.total = parseInt(altMatch[1]);
|
|
47
|
+
testStats.passed = parseInt(altMatch[2]);
|
|
48
|
+
testStats.failed = testStats.total - testStats.passed;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
testResults.unit = {
|
|
53
|
+
...testStats,
|
|
54
|
+
coverage,
|
|
55
|
+
raw_output: output.length > 2000 ? output.slice(0, 2000) + '...(truncated)' : output
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
if (testStats.failed > 0) {
|
|
59
|
+
issues.push({
|
|
60
|
+
code: 'C1',
|
|
61
|
+
severity: 'error',
|
|
62
|
+
message: `${testStats.failed} 个单元测试失败`,
|
|
63
|
+
suggestion: '查看测试输出定位失败的用例并修复'
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 覆盖率检查
|
|
68
|
+
const avgCoverage = Object.values(coverage).length > 0
|
|
69
|
+
? Object.values(coverage).reduce((a, b) => a + b, 0) / Object.values(coverage).length
|
|
70
|
+
: 0;
|
|
71
|
+
|
|
72
|
+
if (avgCoverage > 0 && avgCoverage < 70) {
|
|
73
|
+
issues.push({
|
|
74
|
+
code: 'C1-cov',
|
|
75
|
+
severity: 'error',
|
|
76
|
+
message: `平均测试覆盖率过低: ${avgCoverage.toFixed(1)}% (要求 ≥ 70%)`,
|
|
77
|
+
details: coverage
|
|
78
|
+
});
|
|
79
|
+
} else if (avgCoverage > 0 && avgCoverage < 80) {
|
|
80
|
+
issues.push({
|
|
81
|
+
code: 'C1-cov',
|
|
82
|
+
severity: 'warning',
|
|
83
|
+
message: `测试覆盖率偏低: ${avgCoverage.toFixed(1)}% (建议 ≥ 80%)`,
|
|
84
|
+
details: coverage
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
} catch (e) {
|
|
89
|
+
issues.push({
|
|
90
|
+
code: 'C1',
|
|
91
|
+
severity: 'error',
|
|
92
|
+
message: `单元测试执行失败或超时`,
|
|
93
|
+
details: e.message.slice(0, 200)
|
|
94
|
+
});
|
|
95
|
+
testResults.unit = { error: e.message.slice(0, 200) };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// C2: 测试数量趋势(如果有基线)
|
|
99
|
+
if (testResults.unit?.total) {
|
|
100
|
+
testResults.test_count = testResults.unit.total;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 计算状态
|
|
104
|
+
const hasError = issues.some(i => i.severity === 'error');
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
status: hasError ? 'fail' : (issues.length > 0 ? 'warning' : 'pass'),
|
|
108
|
+
score: testResults.unit?.total ? `${testResults.unit.passed}/${testResults.unit.total} 通过` : 'N/A',
|
|
109
|
+
issues,
|
|
110
|
+
details: testResults
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
module.exports = run;
|