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 +60 -0
- package/lib/ai/reviewer.mjs +135 -169
- package/lib/workflows/analyze.mjs +1 -3
- package/package.json +1 -1
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
|
package/lib/ai/reviewer.mjs
CHANGED
@@ -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
|
53
|
+
let scoreInfo = ''
|
66
54
|
|
67
55
|
if (category === 'businessCriticalPaths') {
|
68
|
-
|
56
|
+
scoreInfo = `BC=${item.suggestedBC}`
|
69
57
|
} else if (category === 'highRiskModules') {
|
70
|
-
|
58
|
+
scoreInfo = `ER=${item.suggestedER}`
|
71
59
|
} else if (category === 'testabilityAdjustments') {
|
72
|
-
|
60
|
+
scoreInfo = `Adj=${item.adjustment}`
|
73
61
|
}
|
74
62
|
|
75
|
-
|
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
|
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
|
-
|
118
|
-
|
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
|
-
|
121
|
-
|
122
|
-
const maxScore = 10
|
75
|
+
console.log(`\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`)
|
76
|
+
console.log(`\n🤖 AI Analysis Results: ${totalSuggestions} suggestions\n`)
|
123
77
|
|
124
|
-
|
78
|
+
let globalIndex = 1
|
79
|
+
const indexMapping = [] // [{ globalIndex, category, localIndex }, ...]
|
125
80
|
|
126
|
-
for (
|
127
|
-
const
|
128
|
-
|
81
|
+
for (const category of categories) {
|
82
|
+
const items = validated[category] || []
|
83
|
+
if (items.length === 0) continue
|
129
84
|
|
130
|
-
const
|
85
|
+
const icon = getCategoryIcon(category)
|
86
|
+
const name = getCategoryName(category)
|
131
87
|
|
132
|
-
|
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
|
-
|
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
|
-
|
97
|
+
console.log(`\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`)
|
98
|
+
|
99
|
+
return { totalSuggestions, indexMapping }
|
146
100
|
}
|
147
101
|
|
148
102
|
/**
|
149
|
-
*
|
103
|
+
* 解析用户输入
|
150
104
|
*/
|
151
|
-
|
152
|
-
|
153
|
-
return []
|
154
|
-
}
|
105
|
+
function parseUserInput(input, totalCount) {
|
106
|
+
const trimmed = input.trim().toLowerCase()
|
155
107
|
|
156
|
-
//
|
157
|
-
|
108
|
+
// 全接受
|
109
|
+
if (trimmed === 'a' || trimmed === 'all') {
|
110
|
+
return { type: 'accept_all' }
|
111
|
+
}
|
158
112
|
|
159
|
-
//
|
160
|
-
|
161
|
-
|
162
|
-
|
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
|
-
|
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 (
|
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
|
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
|
226
|
-
console.log(
|
227
|
-
console.log(` Business Critical Paths: ${result.businessCriticalPaths.length}/${validated.businessCriticalPaths?.length || 0}
|
228
|
-
console.log(` High Risk Modules: ${result.highRiskModules.length}/${validated.highRiskModules?.length || 0}
|
229
|
-
console.log(` Testability Adjustments: ${result.testabilityAdjustments.length}/${validated.testabilityAdjustments?.length || 0}
|
230
|
-
console.log(` Total: ${totalAccepted}/${totalSuggested}
|
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
|
233
|
-
console.log(
|
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
|
161
|
+
// 1. 显示所有建议
|
162
|
+
const { totalSuggestions, indexMapping } = displayAllSuggestions(validated)
|
254
163
|
|
255
|
-
|
256
|
-
|
257
|
-
|
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
|
-
|
264
|
-
|
265
|
-
|
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 (
|
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
|
-
|
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
|
-
|
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 {
|