ai-unit-test-generator 1.3.0 → 1.4.1

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/README.md CHANGED
@@ -319,6 +319,41 @@ Notes:
319
319
 
320
320
  ## 📈 Metrics Explained
321
321
 
322
+ ### Coverage-aware Scoring (New)
323
+
324
+ We incorporate code coverage into prioritization, balancing both incremental (new code) and stock (existing code) scenarios.
325
+
326
+ - Coverage Score (CS) mapping:
327
+ - 0% → 10 (highest)
328
+ - 1-40% → 8
329
+ - 41-70% → 6
330
+ - 71-90% → 3
331
+ - 91-100% → 1 (lowest)
332
+ - N/A → 5 (no data)
333
+ - Default weights (legacy mode): `coverage: 0.20`
334
+ - Layered weights: each layer includes a `coverage` weight (see default config)
335
+ - Optional `coverageBoost` provides a small additive boost for low-coverage files to break ties.
336
+
337
+ Coverage is parsed from `coverage/coverage-summary.json` if present. You can run coverage before `scan` to leverage coverage-aware prioritization.
338
+
339
+ Configuration snippet:
340
+
341
+ ```json
342
+ {
343
+ "coverageScoring": {
344
+ "naScore": 5,
345
+ "mapping": [
346
+ { "lte": 0, "score": 10 },
347
+ { "lte": 40, "score": 8 },
348
+ { "lte": 70, "score": 6 },
349
+ { "lte": 90, "score": 3 },
350
+ { "lte": 100, "score": 1 }
351
+ ]
352
+ },
353
+ "coverageBoost": { "enable": true, "threshold": 60, "scale": 0.5, "maxBoost": 0.5 }
354
+ }
355
+ ```
356
+
322
357
  ### Testability (50% weight in Foundation layer)
323
358
  - Pure functions: 10/10
324
359
  - Simple mocks: 8-9/10
package/bin/cli.js CHANGED
@@ -43,6 +43,25 @@ program
43
43
  mkdirSync(output, { recursive: true })
44
44
  }
45
45
 
46
+ // 可选:在扫描前自动运行覆盖率(由配置控制)
47
+ try {
48
+ const cfgText = existsSync(config) ? readFileSync(config, 'utf-8') : '{}'
49
+ const cfg = JSON.parse(cfgText)
50
+ const covCfg = cfg?.coverage || { runBeforeScan: false }
51
+ if (covCfg.runBeforeScan) {
52
+ console.log('🧪 Running coverage before scan...')
53
+ await new Promise((resolve, reject) => {
54
+ const cmd = covCfg.command || 'npx jest --coverage --silent'
55
+ const child = spawn(cmd, { stdio: 'inherit', shell: true, cwd: process.cwd() })
56
+ child.on('close', code => code === 0 ? resolve(0) : reject(new Error(`coverage exited ${code}`)))
57
+ child.on('error', reject)
58
+ })
59
+ }
60
+ } catch (err) {
61
+ console.warn('⚠️ Coverage step failed or Jest not installed. Skipping coverage and continuing scan.')
62
+ console.warn(' - npm i -D jest@29 ts-jest@29 @types/jest@29 jest-environment-jsdom@29 --legacy-peer-deps')
63
+ }
64
+
46
65
  console.log('🚀 Starting code scan...\n')
47
66
 
48
67
  try {
@@ -191,8 +191,12 @@ function mapDependencyCount(depGraphData, cfg) {
191
191
  return 2
192
192
  }
193
193
  // 传统评分(兼容旧配置)
194
- function computeScoreLegacy({ BC, CC, ER, ROI }, cfg) {
195
- const s = BC * pickWeight(cfg,'BC',0.4) + CC * pickWeight(cfg,'CC',0.3) + ER * pickWeight(cfg,'ER',0.2) + ROI * pickWeight(cfg,'ROI',0.1)
194
+ function computeScoreLegacy({ BC, CC, ER, ROI, coverageScore }, cfg) {
195
+ const s = BC * pickWeight(cfg,'BC',0.4)
196
+ + CC * pickWeight(cfg,'CC',0.3)
197
+ + ER * pickWeight(cfg,'ER',0.2)
198
+ + ROI * pickWeight(cfg,'ROI',0.1)
199
+ + (typeof coverageScore === 'number' ? coverageScore * pickWeight(cfg,'coverage',0) : 0)
196
200
  const score = toFixedDown(s, (cfg?.round?.digits ?? 2))
197
201
  const P0 = pickThreshold(cfg,'P0',8.5), P1 = pickThreshold(cfg,'P1',6.5), P2 = pickThreshold(cfg,'P2',4.5)
198
202
  let priority = 'P3'
@@ -203,7 +207,7 @@ function computeScoreLegacy({ BC, CC, ER, ROI }, cfg) {
203
207
  }
204
208
 
205
209
  // 分层评分(新方法)
206
- function computeScoreLayered({ BC, CC, ER, testability, dependencyCount }, target, cfg) {
210
+ function computeScoreLayered({ BC, CC, ER, testability, dependencyCount, coverageScore }, target, cfg) {
207
211
  const layer = target.layer || 'unknown'
208
212
  const layerDef = cfg?.layers?.[layer]
209
213
 
@@ -221,6 +225,7 @@ function computeScoreLayered({ BC, CC, ER, testability, dependencyCount }, targe
221
225
  if (weights.errorRisk !== undefined) score += ER * weights.errorRisk
222
226
  if (weights.testability !== undefined) score += testability * weights.testability
223
227
  if (weights.dependencyCount !== undefined) score += dependencyCount * weights.dependencyCount
228
+ if (weights.coverage !== undefined && typeof coverageScore === 'number') score += coverageScore * weights.coverage
224
229
 
225
230
  score = toFixedDown(score, cfg?.round?.digits ?? 2)
226
231
 
@@ -245,6 +250,25 @@ function computeScore(metrics, target, cfg) {
245
250
  }
246
251
  }
247
252
 
253
+ // 覆盖率百分比到覆盖率分数(Coverage Score)映射
254
+ function mapCoverageScore(pct, cfg) {
255
+ if (pct === null || pct === undefined || Number.isNaN(pct)) {
256
+ return cfg?.coverageScoring?.naScore ?? 5
257
+ }
258
+ const mapping = cfg?.coverageScoring?.mapping
259
+ if (Array.isArray(mapping) && mapping.length) {
260
+ const ordered = mapping.slice().sort((a,b)=> (a.lte ?? 1e9) - (b.lte ?? 1e9))
261
+ for (const rule of ordered) {
262
+ if (typeof rule.lte === 'number' && pct <= rule.lte) return rule.score
263
+ }
264
+ }
265
+ if (pct <= 0) return 10
266
+ if (pct <= 40) return 8
267
+ if (pct <= 70) return 6
268
+ if (pct <= 90) return 3
269
+ return 1
270
+ }
271
+
248
272
  function defaultMd(rows, statusMap = new Map()) {
249
273
  // 按总分降序排序(高分在前)
250
274
  const sorted = [...rows].sort((a, b) => b.score - a.score)
@@ -252,14 +276,14 @@ function defaultMd(rows, statusMap = new Map()) {
252
276
  let md = '<!-- UT Priority Scoring Report -->\n'
253
277
  md += '<!-- Format: Status can be "TODO" | "DONE" | "SKIP" -->\n'
254
278
  md += '<!-- You can mark status by replacing TODO with DONE or SKIP -->\n\n'
255
- md += '| Status | Score | Priority | Name | Type | Layer | Path | BC | CC | ER | Testability | DepCount | Notes |\n'
256
- md += '|--------|-------|----------|------|------|-------|------|----|----|----|-----------|---------| ------- |\n'
279
+ md += '| Status | Score | Priority | Name | Type | Layer | Path | Coverage | CS | BC | CC | ER | Testability | DepCount | Notes |\n'
280
+ md += '|--------|-------|----------|------|------|-------|------|----------|----|----|----|----|-----------|---------| ------- |\n'
257
281
 
258
282
  sorted.forEach(r => {
259
283
  // 从状态映射中查找现有状态,否则默认为 TODO
260
284
  const key = `${r.path}#${r.name}`
261
285
  const status = statusMap.get(key) || 'TODO'
262
- md += `| ${status} | ${r.score} | ${r.priority} | ${r.name} | ${r.type} | ${r.layerName || r.layer} | ${r.path} | ${r.BC} | ${r.CC} | ${r.ER} | ${r.testability || r.ROI} | ${r.dependencyCount || 'N/A'} | ${r.notes} |\n`
286
+ md += `| ${status} | ${r.score} | ${r.priority} | ${r.name} | ${r.type} | ${r.layerName || r.layer} | ${r.path} | ${typeof r.coveragePct === 'number' ? r.coveragePct.toFixed(1) + '%':'N/A'} | ${r.coverageScore ?? 'N/A'} | ${r.BC} | ${r.CC} | ${r.ER} | ${r.testability || r.ROI} | ${r.dependencyCount || 'N/A'} | ${r.notes} |\n`
263
287
  })
264
288
 
265
289
  // 添加统计信息
@@ -291,7 +315,7 @@ function defaultCsv(rows) {
291
315
  // 按总分降序排序(高分在前)
292
316
  const sorted = [...rows].sort((a, b) => b.score - a.score)
293
317
 
294
- const head = ['status','score','priority','name','path','type','layer','layerName','BC','CC','ER','testability','dependencyCount','notes'].join(',')
318
+ const head = ['status','score','priority','name','path','type','layer','layerName','coveragePct','coverageScore','BC','CC','ER','testability','dependencyCount','notes'].join(',')
295
319
  const body = sorted.map(r => [
296
320
  'TODO',
297
321
  r.score,
@@ -301,6 +325,8 @@ function defaultCsv(rows) {
301
325
  r.type,
302
326
  r.layer || 'N/A',
303
327
  r.layerName || 'N/A',
328
+ (typeof r.coveragePct === 'number' ? r.coveragePct.toFixed(1) + '%' : 'N/A'),
329
+ (r.coverageScore ?? 'N/A'),
304
330
  r.BC,
305
331
  r.CC,
306
332
  r.ER,
@@ -483,7 +509,7 @@ async function main() {
483
509
  // P0-2: 构建依赖图
484
510
  const depGraph = cfg?.depGraph?.enable ? buildDepGraph(funcProvider.project, cfg) : null
485
511
 
486
- // 可选覆盖率引导:读取 coverage-summary.json
512
+ // 读取覆盖率汇总(如果存在)
487
513
  let coverageSummary = null
488
514
  if (existsSync(coverageJsonPath)) {
489
515
  try { coverageSummary = JSON.parse(readFileSync(coverageJsonPath, 'utf8')) } catch {}
@@ -524,19 +550,27 @@ async function main() {
524
550
  const testability = mapTestabilityByConfig(roiHint, cfg, localROI, overrides, t)
525
551
  const dependencyCount = mapDependencyCount(depGraphData, cfg)
526
552
 
527
- let { score, priority, layer, layerName } = computeScore({ BC, CC: CCFinal, ER, ROI, testability, dependencyCount }, t, cfg)
528
-
529
- // 覆盖率加权(可选):对低覆盖模块提升优先级或分数
553
+ // 计算覆盖率分数(用于打分)
554
+ let coveragePct = null
555
+ let coverageScore = null
530
556
  if (coverageSummary) {
531
557
  const fileKey = Object.keys(coverageSummary).find(k => k.endsWith(path))
532
558
  const fileCov = fileKey ? coverageSummary[fileKey] : null
533
559
  const linesPct = fileCov?.lines?.pct
560
+ if (typeof linesPct === 'number') coveragePct = linesPct
561
+ coverageScore = mapCoverageScore(linesPct, cfg)
562
+ }
563
+
564
+ let { score, priority, layer, layerName } = computeScore({ BC, CC: CCFinal, ER, ROI, testability, dependencyCount, coverageScore }, t, cfg)
565
+
566
+ // 覆盖率加权(可选):对低覆盖文件小幅加分
567
+ if (coverageSummary) {
534
568
  const cfgBoost = cfg?.coverageBoost || { enable: false }
535
- if (cfgBoost.enable && typeof linesPct === 'number') {
569
+ if (cfgBoost.enable && typeof coveragePct === 'number') {
536
570
  const threshold = cfgBoost.threshold ?? 60
537
571
  const maxBoost = cfgBoost.maxBoost ?? 0.5
538
- if (linesPct < threshold) {
539
- const ratio = (threshold - linesPct) / Math.max(threshold, 1)
572
+ if (coveragePct < threshold) {
573
+ const ratio = (threshold - coveragePct) / Math.max(threshold, 1)
540
574
  const delta = toFixedDown(Math.min(maxBoost, ratio * (cfgBoost.scale ?? 0.5)), 2)
541
575
  score = toFixedDown(score + delta, cfg?.round?.digits ?? 2)
542
576
  // 重新判定优先级
@@ -583,6 +617,8 @@ async function main() {
583
617
  ROI,
584
618
  testability,
585
619
  dependencyCount,
620
+ coveragePct: coveragePct ?? 'N/A',
621
+ coverageScore: coverageScore ?? 'N/A',
586
622
  score,
587
623
  priority,
588
624
  notes: notesParts.join('; ')
@@ -0,0 +1,199 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * 从 AI 回复中提取测试文件并自动创建
4
+ */
5
+
6
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
7
+ import { dirname } from 'path';
8
+
9
+ /**
10
+ * 从 AI 响应中提取测试文件
11
+ */
12
+ export function extractTests(content, options = {}) {
13
+ const { overwrite = false, dryRun = false } = options;
14
+
15
+ // 先尝试解析 JSON Manifest(优先)
16
+ // 形如:```json { version: 1, files: [{ path, source, ... }] }
17
+ let manifest = null;
18
+ const manifestRegex = /```json\s*\n([\s\S]*?)\n```/gi;
19
+ let m;
20
+ while ((m = manifestRegex.exec(content)) !== null) {
21
+ const jsonStr = m[1].trim();
22
+ try {
23
+ const obj = JSON.parse(jsonStr);
24
+ if (obj && Array.isArray(obj.files)) {
25
+ manifest = obj;
26
+ break;
27
+ }
28
+ } catch {}
29
+ }
30
+ const manifestPaths = manifest?.files?.map(f => String(f.path).trim()) ?? [];
31
+
32
+ // 再匹配多种文件代码块格式(回退/兼容,增强鲁棒性)
33
+ const patterns = [
34
+ // 格式1: ### 测试文件: path (有/无反引号)
35
+ /###\s*测试文件\s*[::]\s*`?([^\n`]+?)`?\s*\n```(?:typescript|ts|tsx|javascript|js|jsx|json|text)?\s*\n([\s\S]*?)\n```/gi,
36
+ // 格式2: **测试文件**: path
37
+ /\*\*测试文件\*\*\s*[::]\s*`?([^\n`]+)`?\s*\n```(?:typescript|ts|tsx|javascript|js|jsx|json|text)?\s*\n([\s\S]*?)\n```/gi,
38
+ // 格式3: 文件路径: path
39
+ /文件路径\s*[::]\s*`?([^\n`]+)`?\s*\n```(?:typescript|ts|tsx|javascript|js|jsx|json|text)?\s*\n([\s\S]*?)\n```/gi,
40
+ // 格式4: # path.test.ts (仅文件名标题)
41
+ /^#+\s+([^\n]+\.test\.[jt]sx?)\s*\n```(?:typescript|ts|tsx|javascript|js|jsx)?\s*\n([\s\S]*?)\n```/gim,
42
+ // 格式5: 更宽松的匹配(任意"path"后接代码块,路径必须含 .test.)
43
+ /(?:path|文件|file)\s*[::]?\s*`?([^\n`]*\.test\.[jt]sx?[^\n`]*?)`?\s*\n```(?:typescript|ts|tsx|javascript|js|jsx)?\s*\n([\s\S]*?)\n```/gi,
44
+ ];
45
+
46
+ const created = [];
47
+ const skipped = [];
48
+ const errors = [];
49
+
50
+ patterns.forEach(fileRegex => {
51
+ let match;
52
+ while ((match = fileRegex.exec(content)) !== null) {
53
+ const [, filePath, testCode] = match;
54
+ const cleanPath = filePath.trim();
55
+
56
+ // 若存在 Manifest,则只允许清单内的路径
57
+ if (manifestPaths.length > 0 && !manifestPaths.includes(cleanPath)) {
58
+ continue;
59
+ }
60
+
61
+ // 跳过已处理的文件
62
+ if (created.some(f => f.path === cleanPath) ||
63
+ skipped.some(f => f.path === cleanPath)) {
64
+ continue;
65
+ }
66
+
67
+ // 检查文件是否已存在
68
+ if (existsSync(cleanPath) && !overwrite) {
69
+ skipped.push({ path: cleanPath, reason: 'exists' });
70
+ continue;
71
+ }
72
+
73
+ if (dryRun) {
74
+ created.push({ path: cleanPath, code: testCode.trim(), dryRun: true });
75
+ continue;
76
+ }
77
+
78
+ try {
79
+ // 确保目录存在
80
+ mkdirSync(dirname(cleanPath), { recursive: true });
81
+
82
+ // 写入测试文件
83
+ const cleanCode = testCode.trim();
84
+ writeFileSync(cleanPath, cleanCode + '\n');
85
+ created.push({ path: cleanPath, code: cleanCode });
86
+ } catch (err) {
87
+ errors.push({ path: cleanPath, error: err.message });
88
+ }
89
+ }
90
+ });
91
+
92
+ // 若有 Manifest 但未生成任何文件,则报告可能缺失代码块
93
+ if (manifestPaths.length > 0) {
94
+ const createdPaths = new Set(created.map(f => f.path));
95
+ const missing = manifestPaths.filter(p => !createdPaths.has(p));
96
+ missing.forEach(p => errors.push({ path: p, error: 'missing code block for manifest entry' }));
97
+ }
98
+
99
+ return { created, skipped, errors };
100
+ }
101
+
102
+ /**
103
+ * CLI 入口
104
+ */
105
+ export function runCLI(argv = process.argv) {
106
+ const args = argv.slice(2);
107
+
108
+ if (args.length === 0) {
109
+ console.error('❌ 缺少参数\n');
110
+ console.error('用法: ai-test extract-tests <response-file> [options]\n');
111
+ console.error('选项:');
112
+ console.error(' --overwrite 覆盖已存在的文件');
113
+ console.error(' --dry-run 仅显示将要创建的文件,不实际写入\n');
114
+ console.error('示例:');
115
+ console.error(' ai-test extract-tests response.txt');
116
+ console.error(' ai-test extract-tests response.txt --overwrite');
117
+ process.exit(1);
118
+ }
119
+
120
+ const responseFile = args[0];
121
+ const options = {
122
+ overwrite: args.includes('--overwrite'),
123
+ dryRun: args.includes('--dry-run')
124
+ };
125
+
126
+ if (!existsSync(responseFile)) {
127
+ console.error(`❌ 文件不存在: ${responseFile}`);
128
+ process.exit(1);
129
+ }
130
+
131
+ try {
132
+ const content = readFileSync(responseFile, 'utf8');
133
+ const { created, skipped, errors } = extractTests(content, options);
134
+
135
+ // 输出结果
136
+ created.forEach(f => {
137
+ if (f.dryRun) {
138
+ console.log(`[DRY-RUN] 将创建: ${f.path}`);
139
+ } else {
140
+ console.log(`✅ 创建测试文件: ${f.path}`);
141
+ }
142
+ });
143
+
144
+ skipped.forEach(f => {
145
+ if (f.reason === 'exists') {
146
+ console.log(`⚠️ 跳过(已存在): ${f.path}`);
147
+ }
148
+ });
149
+
150
+ errors.forEach(f => {
151
+ console.error(`❌ 创建失败: ${f.path} - ${f.error}`);
152
+ });
153
+
154
+ console.log('');
155
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
156
+ console.log(`✨ 总共创建 ${created.length} 个测试文件`);
157
+ if (skipped.length > 0) {
158
+ console.log(`⚠️ 跳过 ${skipped.length} 个已存在的文件`);
159
+ }
160
+ if (errors.length > 0) {
161
+ console.log(`❌ ${errors.length} 个文件创建失败`);
162
+ }
163
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
164
+ console.log('');
165
+
166
+ if (created.length > 0 && !options.dryRun) {
167
+ console.log('🧪 运行测试:');
168
+ console.log(' npm test');
169
+ console.log('');
170
+ console.log('📝 标记完成(如需):');
171
+ const functionNames = created
172
+ .map(f => f.path.match(/\/([^/]+)\.test\./)?.[1])
173
+ .filter(Boolean)
174
+ .join(',');
175
+ if (functionNames) {
176
+ console.log(` ai-test mark-done "${functionNames}"`);
177
+ }
178
+ } else if (created.length === 0) {
179
+ console.log('❌ 未找到任何测试文件');
180
+ console.log('');
181
+ console.log('请检查 AI 回复格式是否正确。预期格式:');
182
+ console.log(' ### 测试文件: src/utils/xxx.test.ts');
183
+ console.log(' ```typescript');
184
+ console.log(' // 测试代码');
185
+ console.log(' ```');
186
+ }
187
+
188
+ process.exit(errors.length > 0 ? 1 : 0);
189
+ } catch (err) {
190
+ console.error(`❌ 错误: ${err.message}`);
191
+ process.exit(1);
192
+ }
193
+ }
194
+
195
+ // 作为脚本直接运行时
196
+ if (import.meta.url === `file://${process.argv[1]}`) {
197
+ runCLI();
198
+ }
199
+
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * 单批次:生成 prompt → 调用 AI → 提取测试 → 运行 Jest → 输出失败摘要
4
+ */
5
+
6
+ import { spawn } from 'child_process'
7
+ import { existsSync, readFileSync } from 'fs'
8
+
9
+ function sh(cmd, args = []) {
10
+ return new Promise((resolve, reject) => {
11
+ const child = spawn(cmd, args, { stdio: 'inherit', cwd: process.cwd() })
12
+ child.on('close', code => code === 0 ? resolve(0) : reject(new Error(`${cmd} exited ${code}`)))
13
+ child.on('error', reject)
14
+ })
15
+ }
16
+
17
+ function readCoverageSummary() {
18
+ const path = 'coverage/coverage-summary.json'
19
+ if (!existsSync(path)) return null
20
+ try { return JSON.parse(readFileSync(path, 'utf8')) } catch { return null }
21
+ }
22
+
23
+ function getCoveragePercent(summary) {
24
+ if (!summary || !summary.total) return 0
25
+ return summary.total.lines?.pct ?? 0
26
+ }
27
+
28
+ async function main(argv = process.argv) {
29
+ const args = argv.slice(2)
30
+ const priority = args[0] || 'P0'
31
+ const limit = Number(args[1] || 10)
32
+ const skip = Number(args[2] || 0)
33
+ const minCovDelta = priority === 'P0' ? 2 : (priority === 'P1' ? 1 : 0)
34
+
35
+ // 记录初始覆盖率
36
+ const beforeCov = getCoveragePercent(readCoverageSummary())
37
+
38
+ // 1) 生成 Prompt(加入上一轮失败提示 hints.txt 如存在)
39
+ const promptArgs = ['ai-test-generator/lib/gen-test-prompt.mjs', '--report', 'reports/ut_scores.md', '-p', priority, '-n', String(limit), '--skip', String(skip)]
40
+ try { await sh('node', [...promptArgs, '--hints-file', 'reports/hints.txt']) } catch { await sh('node', promptArgs) }
41
+
42
+ // 2) 调用 AI
43
+ await sh('node', ['ai-test-generator/lib/ai-generate.mjs', '--prompt', 'prompt.txt', '--out', 'reports/ai_response.txt'])
44
+
45
+ // 3) 提取测试
46
+ await sh('node', ['ai-test-generator/lib/extract-tests.mjs', 'reports/ai_response.txt', '--overwrite'])
47
+
48
+ // 4) 运行 Jest(按优先级自适应重跑与覆盖率增量)
49
+ const reruns = priority === 'P0' ? 1 : (priority === 'P1' ? 0 : 0)
50
+ for (let i = 0; i < Math.max(1, reruns + 1); i++) {
51
+ try { await sh('node', ['ai-test-generator/lib/jest-runner.mjs']) } catch { /* 继续做失败分析 */ }
52
+ }
53
+
54
+ // 校验覆盖率增量(P0/P1)
55
+ if (minCovDelta > 0) {
56
+ const afterCov = getCoveragePercent(readCoverageSummary())
57
+ const delta = afterCov - beforeCov
58
+ if (delta < minCovDelta) {
59
+ console.warn(`⚠️ Coverage delta ${delta.toFixed(2)}% < required ${minCovDelta}% (before: ${beforeCov.toFixed(2)}%, after: ${afterCov.toFixed(2)}%)`)
60
+ } else {
61
+ console.log(`✅ Coverage improved: ${beforeCov.toFixed(2)}% → ${afterCov.toFixed(2)}% (+${delta.toFixed(2)}%)`)
62
+ }
63
+ }
64
+
65
+ // 5) 失败分析并落盘 hints
66
+ const { spawn } = await import('child_process')
67
+ const { writeFileSync } = await import('fs')
68
+ await new Promise((resolve) => {
69
+ const child = spawn('node', ['ai-test-generator/lib/jest-failure-analyzer.mjs'], { stdio: ['inherit','pipe','inherit'] })
70
+ const chunks = []
71
+ child.stdout.on('data', d => chunks.push(Buffer.from(d)))
72
+ child.on('close', () => {
73
+ try { const obj = JSON.parse(Buffer.concat(chunks).toString('utf8')); writeFileSync('reports/hints.txt', obj.hints?.length ? `# 上一轮失败修复建议\n- ${obj.hints.join('\n- ')}` : '') } catch {}
74
+ resolve()
75
+ })
76
+ })
77
+ }
78
+
79
+ if (import.meta.url === `file://${process.argv[1]}`) main()
80
+
81
+
@@ -5,6 +5,12 @@
5
5
 
6
6
  import { readFileSync, existsSync } from 'fs'
7
7
  import { spawn } from 'child_process'
8
+ import { fileURLToPath } from 'url'
9
+ import { dirname, join } from 'path'
10
+
11
+ const __filename = fileURLToPath(import.meta.url)
12
+ const __dirname = dirname(__filename)
13
+ const pkgRoot = join(__dirname, '../..')
8
14
 
9
15
  function sh(cmd, args = []) { return new Promise((resolve, reject) => {
10
16
  const child = spawn(cmd, args, { stdio: 'inherit', cwd: process.cwd() })
@@ -37,7 +43,7 @@ Remaining TODO: ${remain}
37
43
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`)
38
44
 
39
45
  try {
40
- await sh('node', ['ai-test-generator/lib/run-batch.mjs', priority, String(batchSize), String(skip)])
46
+ await sh('node', [join(pkgRoot, 'lib/workflows/batch.mjs'), priority, String(batchSize), String(skip)])
41
47
  } catch (err) {
42
48
  console.error('Batch failed:', err.message)
43
49
  }
@@ -5,6 +5,12 @@
5
5
 
6
6
  import { spawn } from 'child_process'
7
7
  import { existsSync, readFileSync, writeFileSync } from 'fs'
8
+ import { fileURLToPath } from 'url'
9
+ import { dirname, join } from 'path'
10
+
11
+ const __filename = fileURLToPath(import.meta.url)
12
+ const __dirname = dirname(__filename)
13
+ const pkgRoot = join(__dirname, '../..')
8
14
 
9
15
  function sh(cmd, args = []) {
10
16
  return new Promise((resolve, reject) => {
@@ -99,7 +105,7 @@ async function main(argv = process.argv) {
99
105
 
100
106
  // 1) 生成 Prompt(只针对 TODO 函数,加入上一轮失败提示)
101
107
  const promptArgs = [
102
- 'ai-test-generator/lib/ai/prompt-builder.mjs',
108
+ join(pkgRoot, 'lib/ai/prompt-builder.mjs'),
103
109
  '--report', reportPath,
104
110
  '-p', priority,
105
111
  '-n', String(limit),
@@ -114,11 +120,11 @@ async function main(argv = process.argv) {
114
120
 
115
121
  // 2) 调用 AI
116
122
  console.log('\n🤖 Calling AI...')
117
- await sh('node', ['ai-test-generator/lib/ai/client.mjs', '--prompt', 'prompt.txt', '--out', 'reports/ai_response.txt'])
123
+ await sh('node', [join(pkgRoot, 'lib/ai/client.mjs'), '--prompt', 'prompt.txt', '--out', 'reports/ai_response.txt'])
118
124
 
119
125
  // 3) 提取测试
120
126
  console.log('\n📦 Extracting tests...')
121
- await sh('node', ['ai-test-generator/lib/ai/extractor.mjs', 'reports/ai_response.txt', '--overwrite'])
127
+ await sh('node', [join(pkgRoot, 'lib/ai/extractor.mjs'), 'reports/ai_response.txt', '--overwrite'])
122
128
 
123
129
  // 4) 运行 Jest(按优先级自适应重跑)
124
130
  console.log('\n🧪 Running tests...')
@@ -127,7 +133,7 @@ async function main(argv = process.argv) {
127
133
 
128
134
  for (let i = 0; i < Math.max(1, reruns + 1); i++) {
129
135
  try {
130
- await sh('node', ['ai-test-generator/lib/testing/runner.mjs'])
136
+ await sh('node', [join(pkgRoot, 'lib/testing/runner.mjs')])
131
137
  testsPassed = true
132
138
  break
133
139
  } catch {
@@ -163,7 +169,7 @@ async function main(argv = process.argv) {
163
169
  const { spawn: spawnLocal } = await import('child_process')
164
170
  const { writeFileSync: writeFileSyncLocal } = await import('fs')
165
171
  await new Promise((resolve) => {
166
- const child = spawnLocal('node', ['ai-test-generator/lib/testing/analyzer.mjs'], { stdio: ['inherit','pipe','inherit'] })
172
+ const child = spawnLocal('node', [join(pkgRoot, 'lib/testing/analyzer.mjs')], { stdio: ['inherit','pipe','inherit'] })
167
173
  const chunks = []
168
174
  child.stdout.on('data', d => chunks.push(Buffer.from(d)))
169
175
  child.on('close', () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-unit-test-generator",
3
- "version": "1.3.0",
3
+ "version": "1.4.1",
4
4
  "description": "AI-powered unit test generator with smart priority scoring",
5
5
  "keywords": [
6
6
  "unit-test",
@@ -66,4 +66,3 @@
66
66
  },
67
67
  "homepage": "https://github.com/YuhengZhou/ai-unit-test-generator#readme"
68
68
  }
69
-
@@ -22,9 +22,10 @@
22
22
  "multipleReferences": true
23
23
  },
24
24
  "weights": {
25
- "testability": 0.50,
26
- "dependencyCount": 0.30,
27
- "complexity": 0.20
25
+ "testability": 0.45,
26
+ "dependencyCount": 0.25,
27
+ "complexity": 0.20,
28
+ "coverage": 0.10
28
29
  },
29
30
  "thresholds": { "P0": 7.5, "P1": 6.0, "P2": 4.0 },
30
31
  "coverageTarget": 100,
@@ -41,9 +42,10 @@
41
42
  "mayDependOnUtils": true
42
43
  },
43
44
  "weights": {
44
- "businessCriticality": 0.40,
45
- "complexity": 0.30,
46
- "errorRisk": 0.30
45
+ "businessCriticality": 0.35,
46
+ "complexity": 0.25,
47
+ "errorRisk": 0.25,
48
+ "coverage": 0.15
47
49
  },
48
50
  "thresholds": { "P0": 8.0, "P1": 6.5, "P2": 4.5 },
49
51
  "coverageTarget": 80,
@@ -58,9 +60,10 @@
58
60
  "connectsLogicAndUI": true
59
61
  },
60
62
  "weights": {
61
- "businessCriticality": 0.50,
62
- "complexity": 0.30,
63
- "errorRisk": 0.20
63
+ "businessCriticality": 0.45,
64
+ "complexity": 0.25,
65
+ "errorRisk": 0.20,
66
+ "coverage": 0.10
64
67
  },
65
68
  "thresholds": { "P0": 8.0, "P1": 6.5, "P2": 4.5 },
66
69
  "coverageTarget": 70,
@@ -75,15 +78,40 @@
75
78
  "hasInteraction": true
76
79
  },
77
80
  "weights": {
78
- "businessCriticality": 0.40,
79
- "complexity": 0.30,
80
- "testability": 0.30
81
+ "businessCriticality": 0.35,
82
+ "complexity": 0.25,
83
+ "testability": 0.25,
84
+ "coverage": 0.15
81
85
  },
82
86
  "thresholds": { "P0": 8.5, "P1": 7.0, "P2": 5.0 },
83
87
  "coverageTarget": 50,
84
88
  "notes": "UI层,更适合集成测试"
85
89
  }
86
90
  },
91
+ "coverageScoring": {
92
+ "naScore": 5,
93
+ "mapping": [
94
+ { "lte": 0, "score": 10 },
95
+ { "lte": 40, "score": 8 },
96
+ { "lte": 70, "score": 6 },
97
+ { "lte": 90, "score": 3 },
98
+ { "lte": 100, "score": 1 }
99
+ ]
100
+ },
101
+ "coverage": {
102
+ "runBeforeScan": true,
103
+ "command": "npx jest --coverage --silent"
104
+ },
105
+ "weights": {
106
+ "BC": 0.25,
107
+ "CC": 0.15,
108
+ "ER": 0.15,
109
+ "ROI": 0.15,
110
+ "dependencyCount": 0.10,
111
+ "coverage": 0.20
112
+ },
113
+ "thresholds": { "P0": 8.0, "P1": 6.5, "P2": 4.5 },
114
+ "coverageBoost": { "enable": true, "threshold": 60, "scale": 0.5, "maxBoost": 0.5 },
87
115
  "ccFusion": {
88
116
  "useCognitive": true,
89
117
  "cyclomaticT": 15,