ai-unit-test-generator 1.4.5 → 2.0.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/CHANGELOG.md +138 -0
- package/README.md +307 -322
- package/bin/cli.js +100 -202
- package/lib/ai/analyzer-prompt.mjs +120 -0
- package/lib/ai/config-writer.mjs +99 -0
- package/lib/ai/context-builder.mjs +52 -0
- package/lib/ai/index.mjs +11 -0
- package/lib/ai/reviewer.mjs +283 -0
- package/lib/ai/sampler.mjs +97 -0
- package/lib/ai/validator.mjs +101 -0
- package/lib/core/scanner.mjs +112 -2
- package/lib/core/scorer.mjs +184 -4
- package/lib/utils/config-manager.mjs +110 -0
- package/lib/utils/index.mjs +2 -0
- package/lib/utils/scan-manager.mjs +121 -0
- package/lib/workflows/analyze.mjs +157 -0
- package/lib/workflows/generate.mjs +98 -0
- package/lib/workflows/index.mjs +7 -0
- package/lib/workflows/init.mjs +45 -0
- package/lib/workflows/scan.mjs +144 -0
- package/package.json +1 -1
- package/templates/default.config.jsonc +58 -0
package/bin/cli.js
CHANGED
@@ -1,239 +1,137 @@
|
|
1
1
|
#!/usr/bin/env node
|
2
2
|
|
3
|
+
/**
|
4
|
+
* AI-Unit-Test-Generator CLI
|
5
|
+
*
|
6
|
+
* Commands:
|
7
|
+
* init - Initialize configuration file
|
8
|
+
* analyze - AI-powered codebase analysis
|
9
|
+
* scan - Scan code and generate priority scoring
|
10
|
+
* generate - Generate unit tests
|
11
|
+
*/
|
12
|
+
|
3
13
|
import { program } from 'commander'
|
4
14
|
import { fileURLToPath } from 'url'
|
5
|
-
import { dirname, join
|
6
|
-
import {
|
7
|
-
import { spawn } from 'child_process'
|
15
|
+
import { dirname, join } from 'path'
|
16
|
+
import { readFileSync } from 'fs'
|
8
17
|
|
9
18
|
const __filename = fileURLToPath(import.meta.url)
|
10
19
|
const __dirname = dirname(__filename)
|
11
|
-
const PKG_ROOT =
|
20
|
+
const PKG_ROOT = join(__dirname, '..')
|
12
21
|
|
13
|
-
//
|
22
|
+
// 读取版本号
|
14
23
|
const pkgJson = JSON.parse(readFileSync(join(PKG_ROOT, 'package.json'), 'utf-8'))
|
15
24
|
|
16
25
|
program
|
17
26
|
.name('ai-test')
|
18
27
|
.description('AI-powered unit test generator with smart priority scoring')
|
19
28
|
.version(pkgJson.version)
|
29
|
+
.addHelpText('after', `
|
30
|
+
Quick Start:
|
31
|
+
1. $ ai-test init # Create config file
|
32
|
+
2. $ ai-test analyze # Let AI analyze your codebase (optional)
|
33
|
+
3. $ ai-test scan # Scan & score functions
|
34
|
+
4. $ ai-test generate # Generate tests
|
35
|
+
|
36
|
+
Examples:
|
37
|
+
$ ai-test init
|
38
|
+
$ ai-test analyze
|
39
|
+
$ ai-test scan --skip-git
|
40
|
+
$ ai-test generate -n 20 --all
|
41
|
+
|
42
|
+
Documentation: https://github.com/YuhengZhou/ai-unit-test-generator
|
43
|
+
`)
|
20
44
|
|
21
45
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
22
|
-
// 命令 1:
|
46
|
+
// 命令 1: init - 初始化配置
|
47
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
48
|
+
program
|
49
|
+
.command('init')
|
50
|
+
.description('Initialize ai-test configuration file')
|
51
|
+
.option('-c, --config <path>', 'Config file path', 'ai-test.config.jsonc')
|
52
|
+
.action(async (options) => {
|
53
|
+
const { init } = await import('../lib/workflows/init.mjs')
|
54
|
+
await init(options)
|
55
|
+
})
|
56
|
+
|
57
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
58
|
+
// 命令 2: analyze - AI 配置分析
|
59
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
60
|
+
program
|
61
|
+
.command('analyze')
|
62
|
+
.description('AI-powered codebase analysis for scoring optimization')
|
63
|
+
.option('-c, --config <path>', 'Config file path')
|
64
|
+
.option('-o, --output <dir>', 'Output directory', 'reports')
|
65
|
+
.addHelpText('after', `
|
66
|
+
How it works:
|
67
|
+
1. Samples representative code from your codebase
|
68
|
+
2. Calls Cursor Agent to analyze business logic
|
69
|
+
3. Generates scoring suggestions (business critical paths, high risk modules)
|
70
|
+
4. Interactive review - you choose which suggestions to apply
|
71
|
+
5. Updates ai-test.config.jsonc with approved suggestions
|
72
|
+
|
73
|
+
Note: Requires cursor-agent CLI to be installed
|
74
|
+
`)
|
75
|
+
.action(async (options) => {
|
76
|
+
const { analyze } = await import('../lib/workflows/analyze.mjs')
|
77
|
+
await analyze(options)
|
78
|
+
})
|
79
|
+
|
80
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
81
|
+
// 命令 3: scan - 扫描代码并打分
|
23
82
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
24
83
|
program
|
25
84
|
.command('scan')
|
26
85
|
.description('Scan code and generate priority scoring report')
|
27
|
-
.option('-c, --config <path>', 'Config file path'
|
86
|
+
.option('-c, --config <path>', 'Config file path')
|
28
87
|
.option('-o, --output <dir>', 'Output directory', 'reports')
|
29
88
|
.option('--skip-git', 'Skip Git signals generation')
|
89
|
+
.addHelpText('after', `
|
90
|
+
Generates:
|
91
|
+
- reports/targets.json (AST + complexity data)
|
92
|
+
- reports/git_signals.json (Git history signals)
|
93
|
+
- reports/ut_scores.md (Human-readable report)
|
94
|
+
- reports/ut_scores.csv (Machine-readable scores)
|
95
|
+
|
96
|
+
Scoring modes:
|
97
|
+
- Layered: Different weights for each layer (foundation, business, state, UI)
|
98
|
+
- Legacy: Unified weights across all code
|
99
|
+
|
100
|
+
AI enhancement:
|
101
|
+
- If you ran 'ai-test analyze', scores will automatically use AI suggestions
|
102
|
+
- AI-identified business critical paths get higher priority
|
103
|
+
`)
|
30
104
|
.action(async (options) => {
|
31
|
-
|
32
|
-
|
33
|
-
// 自动探测现有配置 & 初始化(不存在则创建)
|
34
|
-
const detectOrder = [config, 'ai-test.config.jsonc', 'ai-test.config.json']
|
35
|
-
const detected = detectOrder.find(p => existsSync(p))
|
36
|
-
if (detected) config = detected
|
37
|
-
if (!existsSync(config)) {
|
38
|
-
console.log('⚙️ Config not found, creating default config...')
|
39
|
-
const templatePath = join(PKG_ROOT, 'templates', 'default.config.jsonc')
|
40
|
-
copyFileSync(templatePath, config)
|
41
|
-
console.log(`✅ Config created: ${config}\n`)
|
42
|
-
}
|
43
|
-
|
44
|
-
// 创建输出目录
|
45
|
-
if (!existsSync(output)) {
|
46
|
-
mkdirSync(output, { recursive: true })
|
47
|
-
}
|
48
|
-
|
49
|
-
// 可选:在扫描前自动运行覆盖率(由配置控制)
|
50
|
-
try {
|
51
|
-
const cfgText = existsSync(config) ? readFileSync(config, 'utf-8') : '{}'
|
52
|
-
const cfg = JSON.parse(cfgText)
|
53
|
-
const covCfg = cfg?.coverage || { runBeforeScan: false }
|
54
|
-
if (covCfg.runBeforeScan) {
|
55
|
-
console.log('🧪 Running coverage before scan...')
|
56
|
-
await new Promise((resolve, reject) => {
|
57
|
-
const cmd = covCfg.command || 'npx jest --coverage --silent'
|
58
|
-
const child = spawn(cmd, { stdio: 'inherit', shell: true, cwd: process.cwd() })
|
59
|
-
child.on('close', code => code === 0 ? resolve(0) : reject(new Error(`coverage exited ${code}`)))
|
60
|
-
child.on('error', reject)
|
61
|
-
})
|
62
|
-
}
|
63
|
-
} catch (err) {
|
64
|
-
console.warn('⚠️ Coverage step failed or Jest not installed. Skipping coverage and continuing scan.')
|
65
|
-
console.warn(' - npm i -D jest@29 ts-jest@29 @types/jest@29 jest-environment-jsdom@29 --legacy-peer-deps')
|
66
|
-
}
|
67
|
-
|
68
|
-
console.log('🚀 Starting code scan...\n')
|
69
|
-
|
70
|
-
try {
|
71
|
-
// Step 1: 生成目标列表
|
72
|
-
console.log('📋 Step 1: Scanning targets...')
|
73
|
-
await runScript('core/scanner.mjs', [
|
74
|
-
'--config', config,
|
75
|
-
'--out', join(output, 'targets.json')
|
76
|
-
])
|
77
|
-
|
78
|
-
// Step 2: 生成 Git 信号 (可选)
|
79
|
-
if (!skipGit) {
|
80
|
-
console.log('\n📊 Step 2: Analyzing Git history...')
|
81
|
-
await runScript('core/git-analyzer.mjs', [
|
82
|
-
'--targets', join(output, 'targets.json'),
|
83
|
-
'--out', join(output, 'git_signals.json')
|
84
|
-
])
|
85
|
-
}
|
86
|
-
|
87
|
-
// Step 3: 运行评分(保留现有状态)
|
88
|
-
console.log('\n🎯 Step 3: Scoring targets...')
|
89
|
-
const scoreArgs = [
|
90
|
-
'--targets', join(output, 'targets.json'),
|
91
|
-
'--config', config,
|
92
|
-
'--out-md', join(output, 'ut_scores.md'),
|
93
|
-
'--out-csv', join(output, 'ut_scores.csv')
|
94
|
-
]
|
95
|
-
if (!skipGit && existsSync(join(output, 'git_signals.json'))) {
|
96
|
-
scoreArgs.push('--git', join(output, 'git_signals.json'))
|
97
|
-
}
|
98
|
-
await runScript('core/scorer.mjs', scoreArgs)
|
99
|
-
|
100
|
-
// 统计 TODO/DONE
|
101
|
-
const reportPath = join(output, 'ut_scores.md')
|
102
|
-
if (existsSync(reportPath)) {
|
103
|
-
const content = readFileSync(reportPath, 'utf-8')
|
104
|
-
const todoCount = (content.match(/\| TODO \|/g) || []).length
|
105
|
-
const doneCount = (content.match(/\| DONE \|/g) || []).length
|
106
|
-
|
107
|
-
console.log('\n✅ Scan completed!')
|
108
|
-
console.log(`\n📊 Status:`)
|
109
|
-
console.log(` TODO: ${todoCount}`)
|
110
|
-
console.log(` DONE: ${doneCount}`)
|
111
|
-
console.log(` Total: ${todoCount + doneCount}`)
|
112
|
-
console.log(`\n📄 Report: ${reportPath}`)
|
113
|
-
console.log(`\n💡 Next: ai-test generate`)
|
114
|
-
}
|
115
|
-
} catch (err) {
|
116
|
-
console.error('❌ Scan failed:', err.message)
|
117
|
-
process.exit(1)
|
118
|
-
}
|
105
|
+
const { scan } = await import('../lib/workflows/scan.mjs')
|
106
|
+
await scan(options)
|
119
107
|
})
|
120
108
|
|
121
109
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
122
|
-
// 命令
|
110
|
+
// 命令 4: generate - 生成测试
|
123
111
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
124
112
|
program
|
125
113
|
.command('generate')
|
126
|
-
.description('Generate tests
|
114
|
+
.description('Generate unit tests for untested functions')
|
127
115
|
.option('-n, --count <number>', 'Number of functions to generate', parseInt, 10)
|
128
116
|
.option('-p, --priority <level>', 'Priority filter (P0, P1, P2, P3)', 'P0')
|
129
117
|
.option('--all', 'Generate all remaining TODO functions')
|
130
118
|
.option('--report <path>', 'Report file path', 'reports/ut_scores.md')
|
119
|
+
.addHelpText('after', `
|
120
|
+
Examples:
|
121
|
+
$ ai-test generate # Generate 10 P0 functions
|
122
|
+
$ ai-test generate -n 20 # Generate 20 P0 functions
|
123
|
+
$ ai-test generate -p P1 -n 15 # Generate 15 P1 functions
|
124
|
+
$ ai-test generate --all # Generate all P0 TODO functions
|
125
|
+
|
126
|
+
Features:
|
127
|
+
- Automatic test generation using Cursor Agent
|
128
|
+
- Jest integration with coverage tracking
|
129
|
+
- Failure retry with hints
|
130
|
+
- Auto-marking DONE on success
|
131
|
+
`)
|
131
132
|
.action(async (options) => {
|
132
|
-
const {
|
133
|
-
|
134
|
-
// 检查报告是否存在
|
135
|
-
if (!existsSync(report)) {
|
136
|
-
console.error(`❌ Report not found: ${report}`)
|
137
|
-
console.log(` Run: ai-test scan`)
|
138
|
-
process.exit(1)
|
139
|
-
}
|
140
|
-
|
141
|
-
if (all) {
|
142
|
-
// 持续生成直到所有 TODO 完成
|
143
|
-
console.log(`🚀 Generating all ${priority} TODO functions...\n`)
|
144
|
-
|
145
|
-
let batchNum = 1
|
146
|
-
let totalGenerated = 0
|
147
|
-
let totalPassed = 0
|
148
|
-
|
149
|
-
while (true) {
|
150
|
-
// 检查还有多少 TODO
|
151
|
-
const content = readFileSync(report, 'utf-8')
|
152
|
-
const lines = content.split('\n')
|
153
|
-
const todoLines = lines.filter(line =>
|
154
|
-
line.includes('| TODO |') && line.includes(`| ${priority} |`)
|
155
|
-
)
|
156
|
-
|
157
|
-
if (todoLines.length === 0) {
|
158
|
-
console.log(`\n✅ All ${priority} functions completed!`)
|
159
|
-
console.log(` Total generated: ${totalGenerated}`)
|
160
|
-
console.log(` Total passed: ${totalPassed}`)
|
161
|
-
break
|
162
|
-
}
|
163
|
-
|
164
|
-
console.log(`\n━━━━ Batch ${batchNum} (${todoLines.length} TODO remaining) ━━━━`)
|
165
|
-
|
166
|
-
try {
|
167
|
-
const result = await generateBatch(priority, Math.min(count, todoLines.length), 0, report)
|
168
|
-
totalGenerated += result.generated
|
169
|
-
totalPassed += result.passed
|
170
|
-
batchNum++
|
171
|
-
} catch (err) {
|
172
|
-
console.error(`❌ Batch ${batchNum} failed:`, err.message)
|
173
|
-
break
|
174
|
-
}
|
175
|
-
}
|
176
|
-
} else {
|
177
|
-
// 生成指定数量
|
178
|
-
console.log(`🚀 Generating ${count} ${priority} functions...\n`)
|
179
|
-
try {
|
180
|
-
await generateBatch(priority, count, 0, report)
|
181
|
-
} catch (err) {
|
182
|
-
console.error('❌ Generation failed:', err.message)
|
183
|
-
process.exit(1)
|
184
|
-
}
|
185
|
-
}
|
186
|
-
})
|
187
|
-
|
188
|
-
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
189
|
-
// 辅助函数
|
190
|
-
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
191
|
-
|
192
|
-
/**
|
193
|
-
* 生成单批测试
|
194
|
-
*/
|
195
|
-
async function generateBatch(priority, count, skip, report) {
|
196
|
-
const batchScript = join(PKG_ROOT, 'lib/workflows/batch.mjs')
|
197
|
-
|
198
|
-
return new Promise((resolve, reject) => {
|
199
|
-
const child = spawn('node', [batchScript, priority, String(count), String(skip), report], {
|
200
|
-
stdio: 'inherit',
|
201
|
-
cwd: process.cwd()
|
202
|
-
})
|
203
|
-
|
204
|
-
child.on('close', (code) => {
|
205
|
-
if (code === 0) {
|
206
|
-
resolve({ generated: count, passed: count }) // TODO: 从 batch 输出获取实际数字
|
207
|
-
} else {
|
208
|
-
reject(new Error(`Batch generation failed with code ${code}`))
|
209
|
-
}
|
210
|
-
})
|
211
|
-
|
212
|
-
child.on('error', reject)
|
213
|
-
})
|
214
|
-
}
|
215
|
-
|
216
|
-
/**
|
217
|
-
* 运行脚本
|
218
|
-
*/
|
219
|
-
function runScript(scriptPath, args) {
|
220
|
-
return new Promise((resolve, reject) => {
|
221
|
-
const fullPath = join(PKG_ROOT, 'lib', scriptPath)
|
222
|
-
const child = spawn('node', [fullPath, ...args], {
|
223
|
-
stdio: 'inherit',
|
224
|
-
cwd: process.cwd()
|
225
|
-
})
|
226
|
-
|
227
|
-
child.on('close', (code) => {
|
228
|
-
if (code === 0) {
|
229
|
-
resolve()
|
230
|
-
} else {
|
231
|
-
reject(new Error(`Script ${scriptPath} exited with code ${code}`))
|
232
|
-
}
|
233
|
-
})
|
234
|
-
|
235
|
-
child.on('error', reject)
|
133
|
+
const { generate } = await import('../lib/workflows/generate.mjs')
|
134
|
+
await generate(options)
|
236
135
|
})
|
237
|
-
}
|
238
136
|
|
239
137
|
program.parse()
|
@@ -0,0 +1,120 @@
|
|
1
|
+
/**
|
2
|
+
* AI 分析 Prompt 构建器
|
3
|
+
*/
|
4
|
+
|
5
|
+
/**
|
6
|
+
* 构建分析 Prompt
|
7
|
+
*/
|
8
|
+
export function buildAnalysisPrompt(samples, stats, projectCtx) {
|
9
|
+
return `You are analyzing a ${projectCtx.framework} codebase to identify business-critical paths and high-risk modules.
|
10
|
+
|
11
|
+
## 📊 Project Overview
|
12
|
+
|
13
|
+
- **Framework**: ${projectCtx.framework}
|
14
|
+
- **Total Files**: ${stats.totalFiles}
|
15
|
+
- **Total Lines**: ${stats.totalLines}
|
16
|
+
- **Key Dependencies**: ${projectCtx.criticalDeps.join(', ') || 'None detected'}
|
17
|
+
|
18
|
+
## 📂 Code Samples (${samples.length} files)
|
19
|
+
|
20
|
+
${samples.map((s, i) => `
|
21
|
+
### Sample ${i + 1}: ${s.path}
|
22
|
+
**Layer**: ${s.layer}
|
23
|
+
**Reason**: ${s.reason}
|
24
|
+
|
25
|
+
\`\`\`typescript
|
26
|
+
${s.preview}
|
27
|
+
\`\`\`
|
28
|
+
`).join('\n')}
|
29
|
+
|
30
|
+
## 🎯 Your Task
|
31
|
+
|
32
|
+
**YOU HAVE ACCESS TO THE FULL CODEBASE** via Cursor's indexing. The samples above are just for quick reference.
|
33
|
+
|
34
|
+
Please analyze the ENTIRE codebase (not just the samples) and suggest:
|
35
|
+
|
36
|
+
1. **businessCriticalPaths**: Which paths handle core business logic?
|
37
|
+
- Look for: payment, booking, pricing, checkout, order processing
|
38
|
+
- Use your codebase index to find all relevant files
|
39
|
+
|
40
|
+
2. **highRiskModules**: Which modules have high error risk?
|
41
|
+
- Look for: date/time handling, external APIs, money calculations, parsing
|
42
|
+
- Check for complex logic, many try-catch blocks
|
43
|
+
|
44
|
+
3. **testabilityAdjustments**: Which paths are easier/harder to test?
|
45
|
+
- Look for: pure functions (easier), heavy dependencies (harder)
|
46
|
+
- Consider side effects, I/O operations
|
47
|
+
|
48
|
+
## 💡 Use Your Codebase Knowledge
|
49
|
+
|
50
|
+
- You can search the codebase using @codebase
|
51
|
+
- You know the full dependency graph
|
52
|
+
- You understand the business logic from code context
|
53
|
+
|
54
|
+
## OUTPUT SCHEMA
|
55
|
+
|
56
|
+
\`\`\`json
|
57
|
+
{
|
58
|
+
"suggestions": {
|
59
|
+
"businessCriticalPaths": [
|
60
|
+
{
|
61
|
+
"pattern": "services/payment/**",
|
62
|
+
"confidence": 0.95,
|
63
|
+
"reason": "Handles Stripe payment processing",
|
64
|
+
"suggestedBC": 10,
|
65
|
+
"evidence": [
|
66
|
+
"Contains processPayment function with Stripe API calls",
|
67
|
+
"Referenced by checkout flow in multiple places",
|
68
|
+
"Handles money transactions"
|
69
|
+
]
|
70
|
+
}
|
71
|
+
],
|
72
|
+
"highRiskModules": [
|
73
|
+
{
|
74
|
+
"pattern": "utils/date/**",
|
75
|
+
"confidence": 0.88,
|
76
|
+
"reason": "Complex timezone and date calculations",
|
77
|
+
"suggestedER": 8,
|
78
|
+
"evidence": [
|
79
|
+
"Multiple timezone conversion functions",
|
80
|
+
"Handles daylight saving time"
|
81
|
+
]
|
82
|
+
}
|
83
|
+
],
|
84
|
+
"testabilityAdjustments": [
|
85
|
+
{
|
86
|
+
"pattern": "utils/**",
|
87
|
+
"confidence": 0.92,
|
88
|
+
"reason": "Pure utility functions with no side effects",
|
89
|
+
"adjustment": "+1",
|
90
|
+
"evidence": [
|
91
|
+
"All exports are pure functions",
|
92
|
+
"No external dependencies observed"
|
93
|
+
]
|
94
|
+
}
|
95
|
+
]
|
96
|
+
}
|
97
|
+
}
|
98
|
+
\`\`\`
|
99
|
+
|
100
|
+
## CRITICAL RULES
|
101
|
+
|
102
|
+
1. **Output ONLY JSON** - No explanations, no markdown wrapper
|
103
|
+
2. **Match Schema Exactly** - Any deviation will be rejected
|
104
|
+
3. **Stay Within Bounds** - All scores must be within specified ranges
|
105
|
+
4. **Require Evidence** - Each suggestion needs 2-3 concrete evidence points
|
106
|
+
5. **No Assumptions** - Only suggest what you can directly observe
|
107
|
+
|
108
|
+
## CONSTRAINTS
|
109
|
+
|
110
|
+
- confidence ≥ 0.70 (businessCriticalPaths ≥ 0.85)
|
111
|
+
- 2-3 evidence items per suggestion
|
112
|
+
- Pattern must match actual paths in codebase
|
113
|
+
- Max 10 suggestions per category
|
114
|
+
- suggestedBC: 8 | 9 | 10
|
115
|
+
- suggestedER: 7 | 8 | 9 | 10
|
116
|
+
- adjustment: "-2" | "-1" | "+1" | "+2"
|
117
|
+
|
118
|
+
Output ONLY the JSON, no explanation.`
|
119
|
+
}
|
120
|
+
|
@@ -0,0 +1,99 @@
|
|
1
|
+
/**
|
2
|
+
* 安全的配置写入器
|
3
|
+
*/
|
4
|
+
|
5
|
+
import { readConfig, writeConfig } from '../utils/config-manager.mjs'
|
6
|
+
|
7
|
+
const LOCKED_PATHS = [
|
8
|
+
'scoringMode',
|
9
|
+
'layers',
|
10
|
+
'weights',
|
11
|
+
'thresholds',
|
12
|
+
'ccMapping',
|
13
|
+
'dependencyCountMapping',
|
14
|
+
'coverageScoring',
|
15
|
+
'fallbacks',
|
16
|
+
'aiEnhancement.enabled'
|
17
|
+
]
|
18
|
+
|
19
|
+
const WRITABLE_PATHS = [
|
20
|
+
'aiEnhancement.analyzed',
|
21
|
+
'aiEnhancement.analyzedAt',
|
22
|
+
'aiEnhancement.suggestions'
|
23
|
+
]
|
24
|
+
|
25
|
+
/**
|
26
|
+
* 应用 AI 建议到配置文件
|
27
|
+
*/
|
28
|
+
export async function applyAISuggestions(configPath, suggestions) {
|
29
|
+
// 1. 读取现有配置
|
30
|
+
const config = readConfig(configPath)
|
31
|
+
|
32
|
+
// 2. 验证权限
|
33
|
+
validateWritePermissions(suggestions)
|
34
|
+
|
35
|
+
// 3. 深拷贝配置
|
36
|
+
const newConfig = JSON.parse(JSON.stringify(config))
|
37
|
+
|
38
|
+
// 4. 初始化 aiEnhancement(如果不存在)
|
39
|
+
if (!newConfig.aiEnhancement) {
|
40
|
+
newConfig.aiEnhancement = {
|
41
|
+
enabled: true,
|
42
|
+
analyzed: false,
|
43
|
+
analyzedAt: null,
|
44
|
+
suggestions: {
|
45
|
+
businessCriticalPaths: [],
|
46
|
+
highRiskModules: [],
|
47
|
+
testabilityAdjustments: []
|
48
|
+
}
|
49
|
+
}
|
50
|
+
}
|
51
|
+
|
52
|
+
// 5. 只写入允许的字段
|
53
|
+
newConfig.aiEnhancement.analyzed = true
|
54
|
+
newConfig.aiEnhancement.analyzedAt = new Date().toISOString()
|
55
|
+
newConfig.aiEnhancement.suggestions = suggestions
|
56
|
+
|
57
|
+
// 6. 验证核心配置未被修改
|
58
|
+
validateCoreConfigIntact(config, newConfig)
|
59
|
+
|
60
|
+
// 7. 写入文件
|
61
|
+
writeConfig(configPath, newConfig)
|
62
|
+
|
63
|
+
return newConfig
|
64
|
+
}
|
65
|
+
|
66
|
+
/**
|
67
|
+
* 验证写入权限
|
68
|
+
*/
|
69
|
+
function validateWritePermissions(suggestions) {
|
70
|
+
const allowedKeys = ['businessCriticalPaths', 'highRiskModules', 'testabilityAdjustments']
|
71
|
+
|
72
|
+
for (const key of Object.keys(suggestions)) {
|
73
|
+
if (!allowedKeys.includes(key)) {
|
74
|
+
throw new Error(`AI attempted to write forbidden field: ${key}`)
|
75
|
+
}
|
76
|
+
}
|
77
|
+
}
|
78
|
+
|
79
|
+
/**
|
80
|
+
* 验证核心配置完整性
|
81
|
+
*/
|
82
|
+
function validateCoreConfigIntact(oldConfig, newConfig) {
|
83
|
+
for (const path of LOCKED_PATHS) {
|
84
|
+
const oldValue = getNestedValue(oldConfig, path)
|
85
|
+
const newValue = getNestedValue(newConfig, path)
|
86
|
+
|
87
|
+
if (JSON.stringify(oldValue) !== JSON.stringify(newValue)) {
|
88
|
+
throw new Error(`Core config was modified (locked path: ${path})`)
|
89
|
+
}
|
90
|
+
}
|
91
|
+
}
|
92
|
+
|
93
|
+
/**
|
94
|
+
* 获取嵌套对象的值
|
95
|
+
*/
|
96
|
+
function getNestedValue(obj, path) {
|
97
|
+
return path.split('.').reduce((curr, key) => curr?.[key], obj)
|
98
|
+
}
|
99
|
+
|
@@ -0,0 +1,52 @@
|
|
1
|
+
/**
|
2
|
+
* 项目上下文构建工具
|
3
|
+
*/
|
4
|
+
|
5
|
+
import { existsSync, readFileSync } from 'node:fs'
|
6
|
+
|
7
|
+
/**
|
8
|
+
* 构建项目上下文信息
|
9
|
+
*/
|
10
|
+
export async function buildProjectContext() {
|
11
|
+
const context = {
|
12
|
+
framework: 'Unknown',
|
13
|
+
criticalDeps: [],
|
14
|
+
devDeps: []
|
15
|
+
}
|
16
|
+
|
17
|
+
// 读取 package.json
|
18
|
+
if (existsSync('package.json')) {
|
19
|
+
try {
|
20
|
+
const pkg = JSON.parse(readFileSync('package.json', 'utf-8'))
|
21
|
+
|
22
|
+
context.name = pkg.name
|
23
|
+
context.framework = detectFramework(pkg.dependencies || {})
|
24
|
+
|
25
|
+
// 识别关键依赖
|
26
|
+
const deps = Object.keys(pkg.dependencies || {})
|
27
|
+
const criticalKeywords = ['stripe', 'payment', 'auth', 'prisma', 'db', 'axios', 'fetch', 'jotai', 'zustand', 'redux']
|
28
|
+
|
29
|
+
context.criticalDeps = deps.filter(dep =>
|
30
|
+
criticalKeywords.some(kw => dep.toLowerCase().includes(kw))
|
31
|
+
)
|
32
|
+
|
33
|
+
context.devDeps = Object.keys(pkg.devDependencies || {})
|
34
|
+
} catch (err) {
|
35
|
+
console.warn('Warning: Could not read package.json')
|
36
|
+
}
|
37
|
+
}
|
38
|
+
|
39
|
+
return context
|
40
|
+
}
|
41
|
+
|
42
|
+
/**
|
43
|
+
* 检测项目框架
|
44
|
+
*/
|
45
|
+
function detectFramework(deps) {
|
46
|
+
if (deps['next']) return 'Next.js'
|
47
|
+
if (deps['react']) return 'React'
|
48
|
+
if (deps['vue']) return 'Vue'
|
49
|
+
if (deps['@angular/core']) return 'Angular'
|
50
|
+
return 'Node.js'
|
51
|
+
}
|
52
|
+
|
package/lib/ai/index.mjs
CHANGED
@@ -3,9 +3,20 @@
|
|
3
3
|
*
|
4
4
|
* 提供 Prompt 构建、AI 调用和测试提取功能
|
5
5
|
* 支持多种 LLM(目前实现:cursor-agent)
|
6
|
+
*
|
7
|
+
* AI 分析功能:代码库分析、配置优化建议
|
6
8
|
*/
|
7
9
|
|
10
|
+
// 测试生成相关
|
8
11
|
export { buildBatchPrompt, runCLI as buildPrompt } from './prompt-builder.mjs'
|
9
12
|
export { main as callAI } from './client.mjs'
|
10
13
|
export { extractTests } from './extractor.mjs'
|
11
14
|
|
15
|
+
// AI 分析相关
|
16
|
+
export * from './sampler.mjs'
|
17
|
+
export * from './context-builder.mjs'
|
18
|
+
export * from './analyzer-prompt.mjs'
|
19
|
+
export * from './validator.mjs'
|
20
|
+
export * from './reviewer.mjs'
|
21
|
+
export * from './config-writer.mjs'
|
22
|
+
|