ai-unit-test-generator 2.0.3 → 2.0.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/CHANGELOG.md CHANGED
@@ -5,6 +5,66 @@ 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
+ ## [2.0.5] - 2025-01-11
9
+
10
+ ### ✨ Feature: Simplified AI Suggestion Review
11
+
12
+ **Problem**: Previous review UX was too complex, requiring category-by-category selection.
13
+
14
+ **Solution**: Completely redesigned `lib/ai/reviewer.mjs` with:
15
+ - ✅ **One-click Accept All** (`a`) - instantly accept all AI suggestions
16
+ - ✅ **One-click Reject All** (`r`) - instantly reject all suggestions
17
+ - ✅ **Partial Accept** (input numbers like `1,3,5`) - granular control
18
+ - ✅ **Compact Display** - all suggestions shown at once with global indexing
19
+ - ✅ **Clear Summary** - final acceptance count before applying changes
20
+
21
+ **User Experience**:
22
+ ```bash
23
+ ai-test analyze
24
+
25
+ # AI 分析完成后立即展示所有建议:
26
+ 🤖 AI Analysis Results: 12 suggestions
27
+
28
+ 🔴 Business Critical Paths (5):
29
+ [1] services/payment/** | BC=10 | Conf: 95%
30
+ → Handles Stripe payment processing
31
+ [2] ...
32
+
33
+ ⚠️ High Risk Modules (4):
34
+ [6] utils/date/** | ER=8 | Conf: 88%
35
+ → Complex timezone calculations
36
+ [7] ...
37
+
38
+ ✅ Testability Adjustments (3):
39
+ [10] utils/** | Adj=+1 | Conf: 92%
40
+ → Pure functions, easy to test
41
+
42
+ ❓ Choose action:
43
+ [a] Accept all 12 suggestions
44
+ [r] Reject all
45
+ Or input numbers (comma-separated, e.g. 1,3,5-8)
46
+
47
+ > a # 或 r,或 1,3,5
48
+ ```
49
+
50
+ **Changes**:
51
+ - Removed multi-stage category review loop
52
+ - Added global indexing across all categories
53
+ - Simplified user input parsing (a/r/numbers only)
54
+ - Removed per-category score adjustment (can be done manually after if needed)
55
+ - Single confirmation step at the end
56
+
57
+ ---
58
+
59
+ ## [2.0.4] - 2025-01-11
60
+
61
+ ### 🐛 Hotfix
62
+ - **Fixed**: ESM import error in `lib/workflows/analyze.mjs` - replaced `require` with proper `import`
63
+ - Added `readFileSync` to imports at module top
64
+ - Resolves `ReferenceError: require is not defined` in ESM context
65
+
66
+ ---
67
+
8
68
  ## [2.0.3] - 2025-01-11
9
69
 
10
70
  ### 🐛 Hotfix
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * 交互式 AI 建议审核器
3
- * 支持分类审核、逐条选择、批量调整分数
3
+ * 支持:一键全接受、一键全拒绝、部分接受(输数字)
4
4
  */
5
5
 
6
6
  import readline from 'readline'
@@ -47,234 +47,200 @@ function getCategoryName(category) {
47
47
  }
48
48
 
49
49
  /**
50
- * 获取分类名称(小写)
51
- */
52
- function getCategoryNameLower(category) {
53
- const names = {
54
- businessCriticalPaths: 'business critical paths',
55
- highRiskModules: 'high risk modules',
56
- testabilityAdjustments: 'testability adjustments'
57
- }
58
- return names[category] || category
59
- }
60
-
61
- /**
62
- * 格式化单个建议
50
+ * 格式化单个建议(紧凑格式)
63
51
  */
64
52
  function formatSuggestion(item, index, category) {
65
- let output = `\n [${index}] ${item.pattern}\n`
53
+ let scoreInfo = ''
66
54
 
67
55
  if (category === 'businessCriticalPaths') {
68
- output += ` BC: ${item.suggestedBC} | Confidence: ${(item.confidence * 100).toFixed(0)}%\n`
56
+ scoreInfo = `BC=${item.suggestedBC}`
69
57
  } else if (category === 'highRiskModules') {
70
- output += ` ER: ${item.suggestedER} | Confidence: ${(item.confidence * 100).toFixed(0)}%\n`
58
+ scoreInfo = `ER=${item.suggestedER}`
71
59
  } else if (category === 'testabilityAdjustments') {
72
- output += ` Adjustment: ${item.adjustment} | Confidence: ${(item.confidence * 100).toFixed(0)}%\n`
60
+ scoreInfo = `Adj=${item.adjustment}`
73
61
  }
74
62
 
75
- output += ` Reason: ${item.reason}\n`
76
- output += ` Evidence:\n`
77
- item.evidence.forEach(e => {
78
- output += ` - ${e}\n`
79
- })
80
-
81
- return output
82
- }
83
-
84
- /**
85
- * 显示建议列表
86
- */
87
- function displaySuggestions(category, items) {
88
- const icon = getCategoryIcon(category)
89
- const name = getCategoryName(category)
90
-
91
- console.log(`\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`)
92
- console.log(`${icon} ${name} (${items.length} suggestions):`)
93
-
94
- items.forEach((item, i) => {
95
- console.log(formatSuggestion(item, i + 1, category))
96
- })
97
-
98
- console.log(`\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`)
99
- }
100
-
101
- /**
102
- * 解析用户选择
103
- */
104
- function parseSelection(input, maxCount) {
105
- if (!input || input.trim() === '') {
106
- return []
107
- }
63
+ const confidence = `${(item.confidence * 100).toFixed(0)}%`
108
64
 
109
- return input.split(',')
110
- .map(s => parseInt(s.trim()))
111
- .filter(n => !isNaN(n) && n >= 1 && n <= maxCount)
65
+ return ` [${index}] ${item.pattern} | ${scoreInfo} | Conf: ${confidence}\n → ${item.reason}`
112
66
  }
113
67
 
114
68
  /**
115
- * 批量调整分数
69
+ * 显示所有建议(紧凑视图)
116
70
  */
117
- async function adjustScores(rl, items, category) {
118
- console.log(`\nAdjust scores (press Enter to keep original):`)
71
+ function displayAllSuggestions(validated) {
72
+ const categories = ['businessCriticalPaths', 'highRiskModules', 'testabilityAdjustments']
73
+ const totalSuggestions = Object.values(validated).reduce((sum, arr) => sum + arr.length, 0)
119
74
 
120
- const scoreField = category === 'businessCriticalPaths' ? 'suggestedBC' : 'suggestedER'
121
- const minScore = category === 'businessCriticalPaths' ? 8 : 7
122
- const maxScore = 10
75
+ console.log(`\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`)
76
+ console.log(`\n🤖 AI Analysis Results: ${totalSuggestions} suggestions\n`)
123
77
 
124
- const adjusted = []
78
+ let globalIndex = 1
79
+ const indexMapping = [] // [{ globalIndex, category, localIndex }, ...]
125
80
 
126
- for (let i = 0; i < items.length; i++) {
127
- const item = { ...items[i] }
128
- const currentScore = item[scoreField]
81
+ for (const category of categories) {
82
+ const items = validated[category] || []
83
+ if (items.length === 0) continue
129
84
 
130
- const input = await ask(rl, ` [${i + 1}] ${item.pattern} (${scoreField}: ${currentScore}): `)
85
+ const icon = getCategoryIcon(category)
86
+ const name = getCategoryName(category)
131
87
 
132
- if (input && input.trim() !== '') {
133
- const newScore = parseInt(input.trim())
134
- if (!isNaN(newScore) && newScore >= minScore && newScore <= maxScore) {
135
- item[scoreField] = newScore
136
- console.log(` ✅ Updated: ${currentScore} → ${newScore}`)
137
- } else {
138
- console.log(` ⚠️ Invalid score (must be ${minScore}-${maxScore}), keeping original`)
139
- }
140
- }
88
+ console.log(`\n${icon} ${name} (${items.length}):`)
141
89
 
142
- adjusted.push(item)
90
+ items.forEach((item, localIndex) => {
91
+ console.log(formatSuggestion(item, globalIndex, category))
92
+ indexMapping.push({ globalIndex, category, localIndex })
93
+ globalIndex++
94
+ })
143
95
  }
144
96
 
145
- return adjusted
97
+ console.log(`\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`)
98
+
99
+ return { totalSuggestions, indexMapping }
146
100
  }
147
101
 
148
102
  /**
149
- * 审核单个分类
103
+ * 解析用户输入
150
104
  */
151
- async function reviewCategory(rl, category, items) {
152
- if (items.length === 0) {
153
- return []
154
- }
105
+ function parseUserInput(input, totalCount) {
106
+ const trimmed = input.trim().toLowerCase()
155
107
 
156
- // 显示建议
157
- displaySuggestions(category, items)
108
+ // 全接受
109
+ if (trimmed === 'a' || trimmed === 'all') {
110
+ return { type: 'accept_all' }
111
+ }
158
112
 
159
- // 询问操作
160
- const categoryName = getCategoryNameLower(category)
161
- const action = await ask(rl,
162
- `❓ Review ${categoryName}:\n` +
163
- ` [a] Accept all (${items.length})\n` +
164
- ` [r] Reject all\n` +
165
- ` [s] Select individually\n` +
166
- ` [n] Skip (keep empty)\n` +
167
- `> `
168
- )
113
+ // 全拒绝
114
+ if (trimmed === 'r' || trimmed === 'reject') {
115
+ return { type: 'reject_all' }
116
+ }
169
117
 
170
- const actionLower = action.trim().toLowerCase()
118
+ // 部分接受(数字列表)
119
+ const numbers = input.split(',')
120
+ .map(s => parseInt(s.trim()))
121
+ .filter(n => !isNaN(n) && n >= 1 && n <= totalCount)
171
122
 
172
- if (actionLower === 'a') {
173
- // 接受全部
174
- console.log(`✅ Accepted: ${items.length}/${items.length} ${categoryName}`)
175
-
176
- // 询问是否调整分数
177
- if (category === 'businessCriticalPaths' || category === 'highRiskModules') {
178
- const adjustInput = await ask(rl, `\nAdjust scores? (y/n) `)
179
- if (adjustInput.trim().toLowerCase() === 'y') {
180
- return await adjustScores(rl, items, category)
181
- }
182
- }
183
-
184
- return items
185
- } else if (actionLower === 's') {
186
- // 逐条选择
187
- const selection = await ask(rl, `\nSelect which suggestions to accept (comma-separated, e.g. 1,3):\n> `)
188
- const indices = parseSelection(selection, items.length)
189
-
190
- if (indices.length === 0) {
191
- console.log(`❌ No suggestions selected`)
192
- return []
193
- }
194
-
195
- const selected = indices.map(i => items[i - 1])
196
- console.log(`✅ Selected: ${selected.length}/${items.length} ${categoryName}`)
197
-
198
- // 询问是否调整分数
199
- if (category === 'businessCriticalPaths' || category === 'highRiskModules') {
200
- const adjustInput = await ask(rl, `\nAdjust scores? (y/n) `)
201
- if (adjustInput.trim().toLowerCase() === 'y') {
202
- return await adjustScores(rl, selected, category)
203
- }
204
- }
205
-
206
- return selected
207
- } else if (actionLower === 'r') {
208
- // 拒绝全部
209
- console.log(`❌ Rejected: all ${categoryName}`)
210
- return []
211
- } else {
212
- // 跳过
213
- console.log(`⏭️ Skipped ${categoryName}`)
214
- return []
123
+ if (numbers.length > 0) {
124
+ return { type: 'partial', indices: numbers }
215
125
  }
126
+
127
+ return { type: 'invalid' }
216
128
  }
217
129
 
218
130
  /**
219
- * 显示总结
131
+ * 显示最终总结
220
132
  */
221
- function displaySummary(result, validated) {
133
+ function displayFinalSummary(result, validated) {
222
134
  const totalSuggested = Object.values(validated).reduce((sum, arr) => sum + arr.length, 0)
223
135
  const totalAccepted = Object.values(result).reduce((sum, arr) => sum + arr.length, 0)
224
136
 
225
- console.log(`\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`)
226
- console.log(`📊 Summary:`)
227
- console.log(` Business Critical Paths: ${result.businessCriticalPaths.length}/${validated.businessCriticalPaths?.length || 0} accepted`)
228
- console.log(` High Risk Modules: ${result.highRiskModules.length}/${validated.highRiskModules?.length || 0} accepted`)
229
- console.log(` Testability Adjustments: ${result.testabilityAdjustments.length}/${validated.testabilityAdjustments?.length || 0} accepted`)
230
- console.log(` Total: ${totalAccepted}/${totalSuggested} suggestions accepted`)
137
+ console.log(`\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`)
138
+ console.log(`\n📊 Final Summary:`)
139
+ console.log(` 🔴 Business Critical Paths: ${result.businessCriticalPaths.length}/${validated.businessCriticalPaths?.length || 0}`)
140
+ console.log(` ⚠️ High Risk Modules: ${result.highRiskModules.length}/${validated.highRiskModules?.length || 0}`)
141
+ console.log(` Testability Adjustments: ${result.testabilityAdjustments.length}/${validated.testabilityAdjustments?.length || 0}`)
142
+ console.log(` Total: ${totalAccepted}/${totalSuggested} accepted\n`)
231
143
 
232
- if (totalAccepted === 0) {
233
- console.log(`\n⚠️ No suggestions accepted. Config will not be modified.`)
144
+ if (totalAccepted > 0) {
145
+ console.log(`💡 These suggestions will be added to ai-test.config.jsonc`)
146
+ console.log(` and will take effect on next: ai-test scan`)
234
147
  }
148
+
149
+ console.log(`\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`)
235
150
  }
236
151
 
237
152
  /**
238
- * 交互式审核
153
+ * 交互式审核(简化版)
239
154
  * @param {Object} validated - 已验证的建议
240
155
  * @returns {Object|null} - 用户批准的建议,或 null(取消)
241
156
  */
242
157
  export async function interactiveReview(validated) {
243
158
  const rl = createInterface()
244
159
 
245
- const result = {
246
- businessCriticalPaths: [],
247
- highRiskModules: [],
248
- testabilityAdjustments: []
249
- }
250
-
251
160
  try {
252
- // 审核每个分类
253
- const categories = ['businessCriticalPaths', 'highRiskModules', 'testabilityAdjustments']
161
+ // 1. 显示所有建议
162
+ const { totalSuggestions, indexMapping } = displayAllSuggestions(validated)
254
163
 
255
- for (const category of categories) {
256
- const items = validated[category] || []
257
- if (items.length === 0) continue
258
-
259
- result[category] = await reviewCategory(rl, category, items)
164
+ if (totalSuggestions === 0) {
165
+ console.log('⚠️ No suggestions to review.')
166
+ rl.close()
167
+ return null
260
168
  }
261
169
 
262
- // 显示总结
263
- displaySummary(result, validated)
264
-
265
- const totalAccepted = Object.values(result).reduce((sum, arr) => sum + arr.length, 0)
170
+ // 2. 询问用户操作
171
+ const userInput = await ask(rl,
172
+ `❓ Choose action:\n` +
173
+ ` [a] Accept all ${totalSuggestions} suggestions\n` +
174
+ ` [r] Reject all\n` +
175
+ ` Or input numbers (comma-separated, e.g. 1,3,5-8)\n` +
176
+ `\n> `
177
+ )
178
+
179
+ const parsed = parseUserInput(userInput, totalSuggestions)
180
+
181
+ // 3. 处理用户选择
182
+ const result = {
183
+ businessCriticalPaths: [],
184
+ highRiskModules: [],
185
+ testabilityAdjustments: []
186
+ }
266
187
 
267
- if (totalAccepted === 0) {
188
+ if (parsed.type === 'accept_all') {
189
+ // 全接受
190
+ result.businessCriticalPaths = validated.businessCriticalPaths || []
191
+ result.highRiskModules = validated.highRiskModules || []
192
+ result.testabilityAdjustments = validated.testabilityAdjustments || []
193
+
194
+ console.log(`\n✅ Accepted all ${totalSuggestions} suggestions`)
195
+
196
+ } else if (parsed.type === 'reject_all') {
197
+ // 全拒绝
198
+ console.log(`\n❌ Rejected all suggestions`)
199
+ rl.close()
200
+ return null
201
+
202
+ } else if (parsed.type === 'partial') {
203
+ // 部分接受
204
+ const selectedIndices = new Set(parsed.indices)
205
+
206
+ indexMapping.forEach(({ globalIndex, category, localIndex }) => {
207
+ if (selectedIndices.has(globalIndex)) {
208
+ const item = validated[category][localIndex]
209
+ result[category].push(item)
210
+ }
211
+ })
212
+
213
+ const totalAccepted = Object.values(result).reduce((sum, arr) => sum + arr.length, 0)
214
+ console.log(`\n✅ Accepted ${totalAccepted}/${totalSuggestions} suggestions`)
215
+
216
+ if (totalAccepted === 0) {
217
+ console.log(`⚠️ No valid suggestions selected`)
218
+ rl.close()
219
+ return null
220
+ }
221
+
222
+ } else {
223
+ // 无效输入
224
+ console.log(`\n❌ Invalid input. No changes made.`)
268
225
  rl.close()
269
226
  return null
270
227
  }
271
228
 
272
- // 最终确认
273
- const save = await ask(rl, `\n💾 Save these changes to ai-test.config.jsonc? (y/n)\n> `)
229
+ // 4. 显示最终总结
230
+ displayFinalSummary(result, validated)
231
+
232
+ // 5. 最终确认
233
+ const confirm = await ask(rl, `💾 Apply these changes? (y/n)\n> `)
274
234
 
275
235
  rl.close()
276
236
 
277
- return save.trim().toLowerCase() === 'y' ? result : null
237
+ if (confirm.trim().toLowerCase() === 'y') {
238
+ return result
239
+ } else {
240
+ console.log(`\n❌ Changes discarded.`)
241
+ return null
242
+ }
243
+
278
244
  } catch (err) {
279
245
  rl.close()
280
246
  throw err
@@ -3,7 +3,7 @@
3
3
  */
4
4
 
5
5
  import { spawn } from 'node:child_process'
6
- import { writeFileSync } from 'node:fs'
6
+ import { writeFileSync, readFileSync } from 'node:fs'
7
7
  import { detectConfig } from '../utils/config-manager.mjs'
8
8
  import { sampleCodeFiles, analyzeProjectStructure } from '../ai/sampler.mjs'
9
9
  import { buildProjectContext } from '../ai/context-builder.mjs'
@@ -122,8 +122,6 @@ export async function analyze(options) {
122
122
  */
123
123
  async function callCursorAgent(promptPath) {
124
124
  return new Promise((resolve, reject) => {
125
- const { readFileSync } = require('node:fs')
126
-
127
125
  // 读取 prompt
128
126
  let prompt
129
127
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-unit-test-generator",
3
- "version": "2.0.3",
3
+ "version": "2.0.5",
4
4
  "description": "AI-powered unit test generator with smart priority scoring",
5
5
  "keywords": [
6
6
  "unit-test",