ai-unit-test-generator 1.4.2 → 1.4.4

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/CHANGELOG.md CHANGED
@@ -5,6 +5,24 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.4.4] - 2025-01-10
9
+
10
+ ### Fixed
11
+ - Fixed CLI template path to use `default.config.jsonc` instead of `.json`
12
+ - Fixed config auto-detection order: `ai-test.config.jsonc` → `ai-test.config.json`
13
+
14
+ ## [1.4.3] - 2025-01-10
15
+
16
+ ### Changed
17
+ - **Config file format**: Changed default config from `.json` to `.jsonc` with comprehensive inline comments
18
+ - **Report simplification**: Removed redundant `Notes` column from reports (info now in config comments)
19
+ - **Better documentation**: Config file now self-documenting with detailed explanations of each scoring parameter
20
+ - Auto-detection order: `ai-test.config.jsonc` → `ai-test.config.json` → `ut_scoring_config.json` (deprecated)
21
+ - **Code cleanup**: Removed redundant legacy files
22
+
23
+ ### Fixed
24
+ - Config file now properly supports JSONC format (JSON with comments)
25
+
8
26
  ## [1.0.0] - 2025-01-10
9
27
 
10
28
  ### Breaking Changes
package/bin/cli.js CHANGED
@@ -31,12 +31,12 @@ program
31
31
  let { config, output, skipGit } = options
32
32
 
33
33
  // 自动探测现有配置 & 初始化(不存在则创建)
34
- const detectOrder = [config, 'ai-test.config.json', 'ai-test.config.jsonc']
34
+ const detectOrder = [config, 'ai-test.config.jsonc', 'ai-test.config.json']
35
35
  const detected = detectOrder.find(p => existsSync(p))
36
36
  if (detected) config = detected
37
37
  if (!existsSync(config)) {
38
38
  console.log('⚙️ Config not found, creating default config...')
39
- const templatePath = join(PKG_ROOT, 'templates', 'default.config.json')
39
+ const templatePath = join(PKG_ROOT, 'templates', 'default.config.jsonc')
40
40
  copyFileSync(templatePath, config)
41
41
  console.log(`✅ Config created: ${config}\n`)
42
42
  }
@@ -294,14 +294,14 @@ function defaultMd(rows, statusMap = new Map()) {
294
294
  let md = '<!-- UT Priority Scoring Report -->\n'
295
295
  md += '<!-- Format: Status can be "TODO" | "DONE" | "SKIP" -->\n'
296
296
  md += '<!-- You can mark status by replacing TODO with DONE or SKIP -->\n\n'
297
- md += '| Status | Score | Priority | Name | Type | Layer | Path | Coverage | CS | BC | CC | ER | Testability | DepCount | Notes |\n'
298
- md += '|--------|-------|----------|------|------|-------|------|----------|----|----|----|----|-----------|---------| ------- |\n'
297
+ md += '| Status | Score | Priority | Name | Type | Layer | Path | Coverage | CS | BC | CC | ER | Testability | DepCount |\n'
298
+ md += '|--------|-------|----------|------|------|-------|------|----------|----|----|----|----|-----------|----------|\n'
299
299
 
300
300
  sorted.forEach(r => {
301
301
  // 从状态映射中查找现有状态,否则默认为 TODO
302
302
  const key = `${r.path}#${r.name}`
303
303
  const status = statusMap.get(key) || 'TODO'
304
- 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`
304
+ 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'} |\n`
305
305
  })
306
306
 
307
307
  // 添加统计信息
@@ -333,7 +333,7 @@ function defaultCsv(rows) {
333
333
  // 按总分降序排序(高分在前)
334
334
  const sorted = [...rows].sort((a, b) => b.score - a.score)
335
335
 
336
- const head = ['status','score','priority','name','path','type','layer','layerName','coveragePct','coverageScore','BC','CC','ER','testability','dependencyCount','notes'].join(',')
336
+ const head = ['status','score','priority','name','path','type','layer','layerName','coveragePct','coverageScore','BC','CC','ER','testability','dependencyCount'].join(',')
337
337
  const body = sorted.map(r => [
338
338
  'TODO',
339
339
  r.score,
@@ -349,8 +349,7 @@ function defaultCsv(rows) {
349
349
  r.CC,
350
350
  r.ER,
351
351
  r.testability || r.ROI,
352
- r.dependencyCount || 'N/A',
353
- `"${r.notes}"`
352
+ r.dependencyCount || 'N/A'
354
353
  ].join(',')).join('\n')
355
354
  return head + '\n' + body + '\n'
356
355
  }
@@ -601,28 +600,6 @@ async function main() {
601
600
  }
602
601
  }
603
602
 
604
- // 构建 notes
605
- const notesParts = []
606
- const cognitive = eslintCognitive?.[`${path}#${name}`]
607
- if (cognitive !== undefined) notesParts.push('CC:fused')
608
- else notesParts.push('CC:cyclo-only')
609
- if (internal && loc >= (cfg?.ccAdjust?.locBonusThreshold ?? 50)) notesParts.push(`CC+${cfg?.ccAdjust?.locBonus ?? 1}loc`)
610
- if (CCFinal > CC) notesParts.push('CC+platform')
611
- if (depGraphData && depGraphData.crossModuleScore > 0) notesParts.push(`ER+depGraph(${depGraphData.crossModuleScore})`)
612
- if (!git) notesParts.push('ER:fallback')
613
- else notesParts.push('ER:git')
614
- if (overrides) {
615
- const key = `${path}#${name}`
616
- if (overrides.BC?.[key]) notesParts.push('BC:override')
617
- if (overrides.CC?.[key]) notesParts.push('CC:override')
618
- if (overrides.ER?.[key]) notesParts.push('ER:override')
619
- if (overrides.ROI?.[key]) notesParts.push('ROI:override')
620
- }
621
- if (localImpact) notesParts.push('Impact:local')
622
- if (localROI) notesParts.push('ROI:local')
623
- notesParts.push(internal ? 'Internal:Y' : 'Internal:N')
624
- if (!isMainChain(path, cfg)) notesParts.push(`BC:cap≤${cfg?.bcCapForNonMainChain ?? 8}`)
625
-
626
603
  rows.push({
627
604
  name,
628
605
  path,
@@ -638,8 +615,7 @@ async function main() {
638
615
  coveragePct: coveragePct ?? 'N/A',
639
616
  coverageScore: coverageScore ?? 'N/A',
640
617
  score,
641
- priority,
642
- notes: notesParts.join('; ')
618
+ priority
643
619
  })
644
620
  }
645
621
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-unit-test-generator",
3
- "version": "1.4.2",
3
+ "version": "1.4.4",
4
4
  "description": "AI-powered unit test generator with smart priority scoring",
5
5
  "keywords": [
6
6
  "unit-test",
@@ -0,0 +1,314 @@
1
+ {
2
+ // ========== 核心评分配置 ==========
3
+ "scoringMode": "layered", // 评分模式: "layered"(分层) 或 "legacy"(传统)
4
+ "round": {
5
+ "mode": "floor", // 取整模式: floor(向下) / ceil(向上) / round(四舍五入)
6
+ "digits": 2 // 保留小数位数
7
+ },
8
+
9
+ // ========== 扫描目标配置 ==========
10
+ "providers": ["func"], // 扫描目标类型: func(函数) / class(类) / method(方法)
11
+ "targetGeneration": {
12
+ "excludeDirs": [] // 排除的目录,例如: ["__tests__", "node_modules"]
13
+ },
14
+ "internalInclude": true, // 是否包含内部函数(非导出函数)
15
+ "internalThresholds": {
16
+ "minLoc": 15, // 内部函数最小行数阈值
17
+ "bonusLoc": 50, // 超过此行数给予额外关注
18
+ "excludePatterns": [ // 排除的文件模式
19
+ "**/__tests__/**",
20
+ "**/*.spec.ts",
21
+ "**/*.spec.tsx"
22
+ ]
23
+ },
24
+
25
+ // ========== 分层架构配置 ==========
26
+ // 测试金字塔:Foundation(底层) -> Business Logic -> State Management -> UI Components(顶层)
27
+ "layers": {
28
+ // 基础工具层:纯函数、工具函数、无依赖的基础代码
29
+ "foundation": {
30
+ "name": "Foundation (基础工具层)",
31
+ "description": "纯函数、工具函数、无依赖的基础代码",
32
+ "patterns": ["utils/**", "constants/**", "config/**", "types/**"],
33
+ "characteristics": {
34
+ "isPure": true, // 是否为纯函数
35
+ "noDependencies": true, // 是否无依赖
36
+ "multipleReferences": true // 是否被多处引用
37
+ },
38
+ "weights": {
39
+ "testability": 0.45, // 可测试性权重(最高)
40
+ "dependencyCount": 0.25, // 依赖数量权重
41
+ "complexity": 0.20, // 复杂度权重
42
+ "coverage": 0.10 // 覆盖率权重
43
+ },
44
+ "thresholds": { "P0": 7.5, "P1": 6.0, "P2": 4.0 },
45
+ "coverageTarget": 100, // 目标覆盖率 100%
46
+ "autoP0": false
47
+ },
48
+
49
+ // 业务逻辑层:包含业务规则的函数,不包含UI
50
+ "business": {
51
+ "name": "Business Logic (业务逻辑层)",
52
+ "description": "包含业务规则的函数,不包含UI",
53
+ "patterns": ["services/**", "stores/**", "hooks/**"],
54
+ "characteristics": {
55
+ "hasBusinessRules": true,
56
+ "noUI": true,
57
+ "mayDependOnUtils": true
58
+ },
59
+ "weights": {
60
+ "businessCriticality": 0.35, // 业务关键度权重(最高)
61
+ "complexity": 0.25,
62
+ "errorRisk": 0.25,
63
+ "coverage": 0.15
64
+ },
65
+ "thresholds": { "P0": 8.0, "P1": 6.5, "P2": 4.5 },
66
+ "coverageTarget": 80
67
+ },
68
+
69
+ // 状态管理层:Jotai atoms、Zustand stores等
70
+ "state": {
71
+ "name": "State Management (状态管理层)",
72
+ "description": "Jotai atoms、Zustand stores等状态管理",
73
+ "patterns": ["atoms/**", "stores/**"],
74
+ "characteristics": {
75
+ "isStateManagement": true,
76
+ "connectsLogicAndUI": true
77
+ },
78
+ "weights": {
79
+ "businessCriticality": 0.45,
80
+ "complexity": 0.25,
81
+ "errorRisk": 0.20,
82
+ "coverage": 0.10
83
+ },
84
+ "thresholds": { "P0": 8.0, "P1": 6.5, "P2": 4.5 },
85
+ "coverageTarget": 70
86
+ },
87
+
88
+ // UI组件层:React组件、包含交互逻辑
89
+ "ui": {
90
+ "name": "UI Components (UI组件层)",
91
+ "description": "React组件、包含交互逻辑",
92
+ "patterns": ["components/**", "pages/**", "context/**"],
93
+ "characteristics": {
94
+ "isReactComponent": true,
95
+ "hasInteraction": true
96
+ },
97
+ "weights": {
98
+ "businessCriticality": 0.35,
99
+ "complexity": 0.25,
100
+ "testability": 0.25,
101
+ "coverage": 0.15
102
+ },
103
+ "thresholds": { "P0": 8.5, "P1": 7.0, "P2": 5.0 },
104
+ "coverageTarget": 50 // UI层目标覆盖率适当降低
105
+ }
106
+ },
107
+
108
+ // ========== 覆盖率评分配置 ==========
109
+ // 参考: Meta TestGen-LLM 覆盖率增量验证
110
+ "coverageScoring": {
111
+ "naScore": 5, // 无覆盖率数据时的默认分数
112
+ "mapping": [ // 覆盖率百分比到分数的映射(覆盖率越低,分数越高,优先级越高)
113
+ { "lte": 0, "score": 10 }, // 0% 覆盖率 -> 最高优先级
114
+ { "lte": 40, "score": 8 }, // ≤40% -> 高优先级
115
+ { "lte": 70, "score": 6 }, // ≤70% -> 中等优先级
116
+ { "lte": 90, "score": 3 }, // ≤90% -> 低优先级
117
+ { "lte": 100, "score": 1 } // 100% 覆盖率 -> 最低优先级
118
+ ]
119
+ },
120
+
121
+ // ========== 覆盖率自动扫描配置 ==========
122
+ "coverage": {
123
+ "runBeforeScan": true, // 是否在扫描前自动运行覆盖率分析
124
+ "command": "npx jest --coverage --silent" // 覆盖率分析命令
125
+ },
126
+
127
+ // ========== 传统评分模式的权重配置 ==========
128
+ // 仅在 scoringMode: "legacy" 时使用
129
+ "weights": {
130
+ "BC": 0.25, // Business Criticality (业务关键度)
131
+ "CC": 0.15, // Cyclomatic Complexity (圈复杂度)
132
+ "ER": 0.15, // Error Risk (错误风险)
133
+ "ROI": 0.15, // Return on Investment (投资回报率,即可测试性)
134
+ "dependencyCount": 0.10, // 依赖数量
135
+ "coverage": 0.20 // 覆盖率权重(新增,参考 CodeScene 的热点分析)
136
+ },
137
+ "thresholds": { "P0": 8.0, "P1": 6.5, "P2": 4.5 },
138
+
139
+ // ========== 覆盖率加成配置 ==========
140
+ // 参考: ISTQB 风险驱动测试
141
+ "coverageBoost": {
142
+ "enable": true,
143
+ "threshold": 60, // 覆盖率低于此值时启用加成
144
+ "scale": 0.5, // 加成系数
145
+ "maxBoost": 0.5 // 最大加成值
146
+ },
147
+
148
+ // ========== 复杂度融合配置 ==========
149
+ // 圈复杂度 + 认知复杂度融合
150
+ "ccFusion": {
151
+ "useCognitive": true, // 是否启用认知复杂度
152
+ "cyclomaticT": 15, // 圈复杂度阈值
153
+ "cognitiveT": 25, // 认知复杂度阈值
154
+ "wC": 0.7, // 圈复杂度权重
155
+ "wK": 0.3, // 认知复杂度权重
156
+ "cap": 10 // 最大分数上限
157
+ },
158
+
159
+ // ========== 复杂度调整配置 ==========
160
+ "ccAdjust": {
161
+ "depthW": 0.2, // 深度权重
162
+ "branchesW": 0.2, // 分支数权重
163
+ "paramsW": 0.1, // 参数数量权重
164
+ "locBonusThreshold": 50, // 行数加成阈值
165
+ "locBonus": 1 // 行数加成分数
166
+ },
167
+
168
+ // ========== 业务关键度关键词配置 ==========
169
+ // 参考: ISTQB 风险驱动测试 - API表面暴露度
170
+ "bcKeywords": {
171
+ "10": ["price", "booking", "payment", "checkout"], // 支付核心
172
+ "9": ["recommend", "search"], // 搜索推荐
173
+ "8": ["filter", "list"], // 列表筛选
174
+ "7": ["login", "order", "config"], // 登录配置
175
+ "5": ["navigation", "seo", "header"], // 导航SEO
176
+ "3": ["log", "trace", "decor"] // 日志装饰
177
+ },
178
+
179
+ // ========== 主链路配置 ==========
180
+ "mainChainPaths": [], // 主链路路径,例如: ["src/services/booking/**"]
181
+ "bcCapForNonMainChain": 8, // 非主链路业务关键度上限
182
+
183
+ // ========== 圈复杂度映射配置 ==========
184
+ "ccMapping": {
185
+ "cyclomatic": [
186
+ { "gt": 15, "score": 10 }, // >15 -> 最复杂
187
+ { "gte": 11, "lte": 15, "score": 10 }, // 11-15 -> 高复杂
188
+ { "gte": 6, "lte": 10, "score": 9 }, // 6-10 -> 中等复杂
189
+ { "gte": 3, "lte": 5, "score": 7 }, // 3-5 -> 低复杂
190
+ { "lte": 2, "score": 6 } // ≤2 -> 简单
191
+ ],
192
+ "adjustments": [ // 额外复杂度调整规则
193
+ { "field": "maxDepth", "op": ">=", "value": 4, "delta": 1 }, // 深度≥4 +1分
194
+ { "field": "branches", "op": ">=", "value": 12, "delta": 1 }, // 分支≥12 +1分
195
+ { "field": "params", "op": ">=", "value": 6, "delta": 1 }, // 参数≥6 +1分
196
+ { "field": "statements", "op": ">=", "value": 80, "delta": 1 }, // 语句≥80 +1分
197
+ { "field": "cognitive", "op": ">=", "value": 25, "delta": 1 } // 认知≥25 +1分
198
+ ],
199
+ "cap": 10,
200
+ "platformAdjust": {
201
+ "delta": 1, // 平台相关代码额外+1分
202
+ "cap": 10,
203
+ "skipIfLikelihoodGte": 4 // 如果错误可能性≥4则跳过此调整
204
+ }
205
+ },
206
+
207
+ // ========== 降级复杂度映射配置 ==========
208
+ "fallbackMapping": {
209
+ "conditions": [
210
+ { "gt": 12, "score": 10 },
211
+ { "gte": 8, "lte": 12, "score": 8 },
212
+ { "gte": 5, "lte": 7, "score": 6 },
213
+ { "gte": 3, "lte": 4, "score": 4 },
214
+ { "lte": 2, "score": 2 }
215
+ ],
216
+ "nesting": [
217
+ { "gte": 4, "delta": 2 },
218
+ { "eq": 3, "delta": 1 }
219
+ ],
220
+ "earlyReturns": [
221
+ { "gte": 4, "delta": 1 }
222
+ ],
223
+ "paramsOrSources": [
224
+ { "paramsGte": 6, "delta": 1 },
225
+ { "sourcesGte": 3, "delta": 1 }
226
+ ],
227
+ "cap": 10
228
+ },
229
+
230
+ // ========== 依赖图配置 ==========
231
+ "depGraph": {
232
+ "enable": true,
233
+ "neighborCategoryBoost": 2, // 邻近类别加成
234
+ "degreeBoost": 8 // 依赖度加成
235
+ },
236
+
237
+ // ========== 错误风险矩阵配置 ==========
238
+ // 参考: Meta TestGen-LLM 稳定性重跑验证
239
+ // ER = 错误可能性 (Likelihood) × 影响度 (Impact)
240
+ "erMatrix": {
241
+ "5": { "5": 10, "4": 9, "3": 8, "2": 7, "1": 6 }, // 可能性=5
242
+ "4": { "5": 9, "4": 8, "3": 7, "2": 6, "1": 5 }, // 可能性=4
243
+ "3": { "5": 8, "4": 7, "3": 6, "2": 5, "1": 4 }, // 可能性=3
244
+ "2": { "5": 7, "4": 6, "3": 5, "2": 4, "1": 3 }, // 可能性=2
245
+ "1": { "5": 6, "4": 5, "3": 4, "2": 3, "1": 2 } // 可能性=1
246
+ },
247
+
248
+ // ========== 错误可能性规则配置 ==========
249
+ // 参考: CodeScene 的变更频率(Churn)分析
250
+ "likelihoodRules": [
251
+ { "field": "commits30d", "op": ">=", "value": 6, "score": 5 }, // 30天内≥6次提交 -> 高频变更
252
+ { "field": "commits30d", "op": "between", "min": 3, "max": 5, "score": 4 },
253
+ { "field": "commits30d", "op": "between", "min": 1, "max": 2, "score": 3 },
254
+ { "field": "fallback90d", "op": "gt", "value": 0, "score": 2 }, // 90天内有变更
255
+ { "field": "fallback180dZero", "op": "eq", "value": true, "score": 1 } // 180天内无变更
256
+ ],
257
+
258
+ // ========== 加成规则配置 ==========
259
+ "boostRules": {
260
+ "authors30dGte": 3, // 30天内贡献者≥3人
261
+ "crossModule": true, // 跨模块引用
262
+ "multiPlatform": true, // 多平台代码
263
+ "cap": 5 // 加成上限
264
+ },
265
+
266
+ // ========== 跨模块类别配置 ==========
267
+ "crossModuleCategories": ["components", "hooks", "utils", "services", "pages"],
268
+
269
+ // ========== 本地提示映射配置 ==========
270
+ "hintMaps": {
271
+ "impactLocal": "configs/impact.local.json",
272
+ "roiLocal": "configs/roi.local.json"
273
+ },
274
+
275
+ // ========== 覆盖配置 ==========
276
+ "overrides": "reports/overrides.json",
277
+
278
+ // ========== 影响度关键词配置 ==========
279
+ "impactKeywords": {
280
+ "5": ["payment", "booking", "price"],
281
+ "4": ["filter", "list", "display"],
282
+ "3": ["interaction"],
283
+ "2": ["minor", "ui"],
284
+ "1": ["decor", "cosmetic"]
285
+ },
286
+
287
+ // ========== 可测试性规则配置 ==========
288
+ // 纯函数和可注入函数最适合AI生成测试
289
+ "testabilityRules": {
290
+ "pure": 10, // 纯函数 -> 最易测试
291
+ "injectable": 9, // 依赖注入 -> 很易测试
292
+ "multiContext": 7, // 多上下文 -> 中等难度
293
+ "nativeOrNetwork": 5, // 原生API/网络 -> 需要Mock
294
+ "needsUI": 3 // 需要UI环境 -> 较难测试
295
+ },
296
+
297
+ // ========== 依赖数量映射配置 ==========
298
+ // 依赖越多,集成风险越高,测试价值越高
299
+ "dependencyCountMapping": [
300
+ { "gte": 10, "score": 10 }, // ≥10个依赖 -> 高集成风险
301
+ { "gte": 5, "lt": 10, "score": 10 },
302
+ { "gte": 3, "lt": 5, "score": 9 },
303
+ { "gte": 1, "lt": 3, "score": 7 },
304
+ { "eq": 0, "score": 5 } // 0个依赖 -> 纯函数(低风险但高可测试性)
305
+ ],
306
+
307
+ // ========== 降级默认值配置 ==========
308
+ "fallbacks": {
309
+ "BC": 6, // 业务关键度默认值
310
+ "CC": 6, // 复杂度默认值
311
+ "ERLikelihood": 3, // 错误可能性默认值
312
+ "Testability": 6 // 可测试性默认值
313
+ }
314
+ }
@@ -1,199 +0,0 @@
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
-
package/lib/index.js DELETED
@@ -1,18 +0,0 @@
1
- /**
2
- * @ctrip/ut-priority-scorer
3
- *
4
- * AI-friendly Unit Test priority scoring system
5
- * Based on layered architecture and testability metrics
6
- */
7
-
8
- export { default as genTargets } from './gen-targets.mjs'
9
- export { default as genGitSignals } from './gen-git-signals.mjs'
10
- export { default as score } from './score-ut.mjs'
11
-
12
- // 版本信息
13
- export const version = '1.0.0'
14
-
15
- // 默认配置路径
16
- export const DEFAULT_CONFIG_PATH = 'ut_scoring_config.json'
17
- export const DEFAULT_OUTPUT_DIR = 'reports'
18
-
package/lib/run-batch.mjs DELETED
@@ -1,81 +0,0 @@
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
-
@@ -1,227 +0,0 @@
1
- {
2
- "scoringMode": "layered",
3
- "round": { "mode": "floor", "digits": 2 },
4
- "providers": ["func"],
5
- "targetGeneration": {
6
- "excludeDirs": []
7
- },
8
- "internalInclude": true,
9
- "internalThresholds": {
10
- "minLoc": 15,
11
- "bonusLoc": 50,
12
- "excludePatterns": ["**/__tests__/**", "**/*.spec.ts", "**/*.spec.tsx"]
13
- },
14
- "layers": {
15
- "foundation": {
16
- "name": "Foundation (基础工具层)",
17
- "description": "纯函数、工具函数、无依赖的基础代码",
18
- "patterns": ["utils/**", "constants/**", "config/**", "types/**"],
19
- "characteristics": {
20
- "isPure": true,
21
- "noDependencies": true,
22
- "multipleReferences": true
23
- },
24
- "weights": {
25
- "testability": 0.45,
26
- "dependencyCount": 0.25,
27
- "complexity": 0.20,
28
- "coverage": 0.10
29
- },
30
- "thresholds": { "P0": 7.5, "P1": 6.0, "P2": 4.0 },
31
- "coverageTarget": 100,
32
- "autoP0": false,
33
- "notes": "测试金字塔底层,优先级最高;AI生成友好层"
34
- },
35
- "business": {
36
- "name": "Business Logic (业务逻辑层)",
37
- "description": "包含业务规则的函数,不包含UI",
38
- "patterns": ["services/**", "stores/**", "hooks/**"],
39
- "characteristics": {
40
- "hasBusinessRules": true,
41
- "noUI": true,
42
- "mayDependOnUtils": true
43
- },
44
- "weights": {
45
- "businessCriticality": 0.35,
46
- "complexity": 0.25,
47
- "errorRisk": 0.25,
48
- "coverage": 0.15
49
- },
50
- "thresholds": { "P0": 8.0, "P1": 6.5, "P2": 4.5 },
51
- "coverageTarget": 80,
52
- "notes": "核心业务逻辑,高优先级"
53
- },
54
- "state": {
55
- "name": "State Management (状态管理层)",
56
- "description": "Jotai atoms、Zustand stores等状态管理",
57
- "patterns": ["atoms/**", "stores/**"],
58
- "characteristics": {
59
- "isStateManagement": true,
60
- "connectsLogicAndUI": true
61
- },
62
- "weights": {
63
- "businessCriticality": 0.45,
64
- "complexity": 0.25,
65
- "errorRisk": 0.20,
66
- "coverage": 0.10
67
- },
68
- "thresholds": { "P0": 8.0, "P1": 6.5, "P2": 4.5 },
69
- "coverageTarget": 70,
70
- "notes": "状态管理,连接逻辑和UI"
71
- },
72
- "ui": {
73
- "name": "UI Components (UI组件层)",
74
- "description": "React组件、包含交互逻辑",
75
- "patterns": ["components/**", "pages/**", "context/**"],
76
- "characteristics": {
77
- "isReactComponent": true,
78
- "hasInteraction": true
79
- },
80
- "weights": {
81
- "businessCriticality": 0.35,
82
- "complexity": 0.25,
83
- "testability": 0.25,
84
- "coverage": 0.15
85
- },
86
- "thresholds": { "P0": 8.5, "P1": 7.0, "P2": 5.0 },
87
- "coverageTarget": 50,
88
- "notes": "UI层,更适合集成测试"
89
- }
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 },
115
- "ccFusion": {
116
- "useCognitive": true,
117
- "cyclomaticT": 15,
118
- "cognitiveT": 25,
119
- "wC": 0.7,
120
- "wK": 0.3,
121
- "cap": 10
122
- },
123
- "ccAdjust": {
124
- "depthW": 0.2,
125
- "branchesW": 0.2,
126
- "paramsW": 0.1,
127
- "locBonusThreshold": 50,
128
- "locBonus": 1
129
- },
130
- "bcKeywords": {
131
- "10": ["price", "booking", "payment", "checkout"],
132
- "9": ["recommend", "search"],
133
- "8": ["filter", "list"],
134
- "7": ["login", "order", "config"],
135
- "5": ["navigation", "seo", "header"],
136
- "3": ["log", "trace", "decor"]
137
- },
138
- "mainChainPaths": [],
139
- "bcCapForNonMainChain": 8,
140
- "ccMapping": {
141
- "cyclomatic": [
142
- { "gt": 15, "score": 10 },
143
- { "gte": 11, "lte": 15, "score": 10 },
144
- { "gte": 6, "lte": 10, "score": 9 },
145
- { "gte": 3, "lte": 5, "score": 7 },
146
- { "lte": 2, "score": 6 }
147
- ],
148
- "adjustments": [
149
- { "field": "maxDepth", "op": ">=", "value": 4, "delta": 1 },
150
- { "field": "branches", "op": ">=", "value": 12, "delta": 1 },
151
- { "field": "params", "op": ">=", "value": 6, "delta": 1 },
152
- { "field": "statements", "op": ">=", "value": 80, "delta": 1 },
153
- { "field": "cognitive", "op": ">=", "value": 25, "delta": 1 }
154
- ],
155
- "cap": 10,
156
- "platformAdjust": { "delta": 1, "cap": 10, "skipIfLikelihoodGte": 4 }
157
- },
158
- "fallbackMapping": {
159
- "conditions": [
160
- { "gt": 12, "score": 10 },
161
- { "gte": 8, "lte": 12, "score": 8 },
162
- { "gte": 5, "lte": 7, "score": 6 },
163
- { "gte": 3, "lte": 4, "score": 4 },
164
- { "lte": 2, "score": 2 }
165
- ],
166
- "nesting": [{ "gte": 4, "delta": 2 }, { "eq": 3, "delta": 1 }],
167
- "earlyReturns": [{ "gte": 4, "delta": 1 }],
168
- "paramsOrSources": [
169
- { "paramsGte": 6, "delta": 1 },
170
- { "sourcesGte": 3, "delta": 1 }
171
- ],
172
- "cap": 10
173
- },
174
- "depGraph": {
175
- "enable": true,
176
- "neighborCategoryBoost": 2,
177
- "degreeBoost": 8
178
- },
179
- "erMatrix": {
180
- "5": { "5": 10, "4": 9, "3": 8, "2": 7, "1": 6 },
181
- "4": { "5": 9, "4": 8, "3": 7, "2": 6, "1": 5 },
182
- "3": { "5": 8, "4": 7, "3": 6, "2": 5, "1": 4 },
183
- "2": { "5": 7, "4": 6, "3": 5, "2": 4, "1": 3 },
184
- "1": { "5": 6, "4": 5, "3": 4, "2": 3, "1": 2 }
185
- },
186
- "likelihoodRules": [
187
- { "field": "commits30d", "op": ">=", "value": 6, "score": 5 },
188
- { "field": "commits30d", "op": "between", "min": 3, "max": 5, "score": 4 },
189
- { "field": "commits30d", "op": "between", "min": 1, "max": 2, "score": 3 },
190
- { "field": "fallback90d", "op": "gt", "value": 0, "score": 2 },
191
- { "field": "fallback180dZero", "op": "eq", "value": true, "score": 1 }
192
- ],
193
- "boostRules": {
194
- "authors30dGte": 3,
195
- "crossModule": true,
196
- "multiPlatform": true,
197
- "cap": 5
198
- },
199
- "crossModuleCategories": ["components", "hooks", "utils", "services", "pages"],
200
- "hintMaps": {
201
- "impactLocal": "configs/impact.local.json",
202
- "roiLocal": "configs/roi.local.json"
203
- },
204
- "overrides": "reports/overrides.json",
205
- "impactKeywords": {
206
- "5": ["payment", "booking", "price"],
207
- "4": ["filter", "list", "display"],
208
- "3": ["interaction"],
209
- "2": ["minor", "ui"],
210
- "1": ["decor", "cosmetic"]
211
- },
212
- "testabilityRules": {
213
- "pure": 10,
214
- "injectable": 9,
215
- "multiContext": 7,
216
- "nativeOrNetwork": 5,
217
- "needsUI": 3
218
- },
219
- "dependencyCountMapping": [
220
- { "gte": 10, "score": 10 },
221
- { "gte": 5, "lt": 10, "score": 10 },
222
- { "gte": 3, "lt": 5, "score": 9 },
223
- { "gte": 1, "lt": 3, "score": 7 },
224
- { "eq": 0, "score": 5 }
225
- ],
226
- "fallbacks": { "BC": 6, "CC": 6, "ERLikelihood": 3, "Testability": 6 }
227
- }