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.
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Generate 工作流:生成单元测试
3
+ */
4
+
5
+ import { spawn } from 'node:child_process'
6
+ import { existsSync, readFileSync } from 'node:fs'
7
+ import { join, dirname } from 'node:path'
8
+ import { fileURLToPath } from 'node:url'
9
+
10
+ const __filename = fileURLToPath(import.meta.url)
11
+ const __dirname = dirname(__filename)
12
+ const PKG_ROOT = join(__dirname, '../..')
13
+
14
+ /**
15
+ * 生成单批测试
16
+ */
17
+ async function generateBatch(priority, count, skip, report) {
18
+ const batchScript = join(PKG_ROOT, 'lib/workflows/batch.mjs')
19
+
20
+ return new Promise((resolve, reject) => {
21
+ const child = spawn('node', [batchScript, priority, String(count), String(skip), report], {
22
+ stdio: 'inherit',
23
+ cwd: process.cwd()
24
+ })
25
+
26
+ child.on('close', (code) => {
27
+ if (code === 0) {
28
+ resolve({ generated: count, passed: count })
29
+ } else {
30
+ reject(new Error(`Batch generation failed with code ${code}`))
31
+ }
32
+ })
33
+
34
+ child.on('error', reject)
35
+ })
36
+ }
37
+
38
+ /**
39
+ * Generate 工作流
40
+ */
41
+ export async function generate(options) {
42
+ const { count, priority, all, report } = options
43
+
44
+ // 1. 检查报告是否存在
45
+ if (!existsSync(report)) {
46
+ console.error(`❌ Report not found: ${report}`)
47
+ console.log(` Run: ai-test scan`)
48
+ process.exit(1)
49
+ }
50
+
51
+ if (all) {
52
+ // 2. 持续生成直到所有 TODO 完成
53
+ console.log(`🚀 Generating all ${priority} TODO functions...\n`)
54
+
55
+ let batchNum = 1
56
+ let totalGenerated = 0
57
+ let totalPassed = 0
58
+
59
+ while (true) {
60
+ // 检查还有多少 TODO
61
+ const content = readFileSync(report, 'utf-8')
62
+ const lines = content.split('\n')
63
+ const todoLines = lines.filter(line =>
64
+ line.includes('| TODO |') && line.includes(`| ${priority} |`)
65
+ )
66
+
67
+ if (todoLines.length === 0) {
68
+ console.log(`\n✅ All ${priority} functions completed!`)
69
+ console.log(` Total generated: ${totalGenerated}`)
70
+ console.log(` Total passed: ${totalPassed}`)
71
+ break
72
+ }
73
+
74
+ console.log(`\n━━━━ Batch ${batchNum} (${todoLines.length} TODO remaining) ━━━━`)
75
+
76
+ try {
77
+ const result = await generateBatch(priority, Math.min(count, todoLines.length), 0, report)
78
+ totalGenerated += result.generated
79
+ totalPassed += result.passed
80
+ batchNum++
81
+ } catch (err) {
82
+ console.error(`❌ Batch ${batchNum} failed:`, err.message)
83
+ break
84
+ }
85
+ }
86
+ } else {
87
+ // 3. 生成指定数量
88
+ console.log(`🚀 Generating ${count} ${priority} functions...\n`)
89
+
90
+ try {
91
+ await generateBatch(priority, count, 0, report)
92
+ } catch (err) {
93
+ console.error('❌ Generation failed:', err.message)
94
+ process.exit(1)
95
+ }
96
+ }
97
+ }
98
+
@@ -5,6 +5,13 @@
5
5
  * 组合 Core、AI 和 Testing 模块实现端到端工作流
6
6
  */
7
7
 
8
+ // 主要工作流
9
+ export * from './init.mjs'
10
+ export * from './analyze.mjs'
11
+ export * from './scan.mjs'
12
+ export * from './generate.mjs'
13
+
14
+ // 批处理工作流(内部使用)
8
15
  export { main as runBatch } from './batch.mjs'
9
16
  export { main as runAll } from './all.mjs'
10
17
 
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Init 工作流:初始化配置文件
3
+ */
4
+
5
+ import { existsSync, copyFileSync } from 'node:fs'
6
+ import { join, dirname } from 'node:path'
7
+ import { fileURLToPath } from 'node:url'
8
+
9
+ const __filename = fileURLToPath(import.meta.url)
10
+ const __dirname = dirname(__filename)
11
+ const PKG_ROOT = join(__dirname, '../..')
12
+
13
+ /**
14
+ * 初始化配置文件
15
+ */
16
+ export async function init(options) {
17
+ const { config } = options
18
+ const configPath = config || 'ai-test.config.jsonc'
19
+
20
+ // 检查配置文件是否已存在
21
+ if (existsSync(configPath)) {
22
+ console.log(`⚠️ Config already exists: ${configPath}`)
23
+ console.log(' Delete it first or use a different path with -c option')
24
+ process.exit(1)
25
+ }
26
+
27
+ // 复制模板
28
+ console.log('⚙️ Creating default config...')
29
+ const templatePath = join(PKG_ROOT, 'templates', 'default.config.jsonc')
30
+
31
+ try {
32
+ copyFileSync(templatePath, configPath)
33
+ console.log(`✅ Config created: ${configPath}`)
34
+
35
+ // 显示下一步建议
36
+ console.log('\n💡 Next steps:')
37
+ console.log(' 1. Review and customize the config (optional)')
38
+ console.log(' 2. Run `ai-test analyze` to let AI analyze your codebase')
39
+ console.log(' 3. Or run `ai-test scan` to start scoring immediately')
40
+ } catch (err) {
41
+ console.error(`❌ Failed to create config: ${err.message}`)
42
+ process.exit(1)
43
+ }
44
+ }
45
+
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Scan 工作流:扫描代码 + 打分 + 生成报告
3
+ */
4
+
5
+ import { spawn } from 'node:child_process'
6
+ import { existsSync, mkdirSync, readFileSync } from 'node:fs'
7
+ import { join, dirname } from 'node:path'
8
+ import { fileURLToPath } from 'node:url'
9
+ import { detectConfig, readConfig } from '../utils/config-manager.mjs'
10
+
11
+ const __filename = fileURLToPath(import.meta.url)
12
+ const __dirname = dirname(__filename)
13
+ const PKG_ROOT = join(__dirname, '../..')
14
+
15
+ /**
16
+ * 移除 JSON 注释
17
+ */
18
+ function stripJsonComments(str) {
19
+ return String(str)
20
+ .replace(/\/\*[\s\S]*?\*\//g, '')
21
+ .replace(/(^|\s)\/\/.*$/gm, '')
22
+ }
23
+
24
+ /**
25
+ * 运行脚本
26
+ */
27
+ function runScript(scriptPath, args) {
28
+ return new Promise((resolve, reject) => {
29
+ const fullPath = join(PKG_ROOT, 'lib', scriptPath)
30
+ const child = spawn('node', [fullPath, ...args], {
31
+ stdio: 'inherit',
32
+ cwd: process.cwd()
33
+ })
34
+
35
+ child.on('close', (code) => {
36
+ if (code === 0) {
37
+ resolve()
38
+ } else {
39
+ reject(new Error(`Script ${scriptPath} exited with code ${code}`))
40
+ }
41
+ })
42
+
43
+ child.on('error', reject)
44
+ })
45
+ }
46
+
47
+ /**
48
+ * Scan 工作流
49
+ */
50
+ export async function scan(options) {
51
+ let { config, output, skipGit } = options
52
+
53
+ // 1. 检查配置
54
+ console.log('🔍 Step 1: Checking configuration...')
55
+ const configPath = detectConfig(config)
56
+
57
+ if (!configPath) {
58
+ console.error('❌ Config not found. Run `ai-test init` first.')
59
+ process.exit(1)
60
+ }
61
+
62
+ config = configPath
63
+ console.log(` Using config: ${config}\n`)
64
+
65
+ // 2. 创建输出目录
66
+ if (!existsSync(output)) {
67
+ mkdirSync(output, { recursive: true })
68
+ }
69
+
70
+ // 3. 可选:运行覆盖率
71
+ try {
72
+ const cfgText = existsSync(config) ? readFileSync(config, 'utf-8') : '{}'
73
+ const cfg = JSON.parse(stripJsonComments(cfgText))
74
+ const covCfg = cfg?.coverage || { runBeforeScan: false }
75
+
76
+ if (covCfg.runBeforeScan) {
77
+ console.log('🧪 Running coverage before scan...')
78
+ await new Promise((resolve) => {
79
+ const cmd = covCfg.command || 'npx jest --coverage --silent'
80
+ const child = spawn(cmd, { stdio: 'inherit', shell: true, cwd: process.cwd() })
81
+ child.on('close', () => resolve())
82
+ child.on('error', () => resolve())
83
+ })
84
+ console.log('✅ Coverage completed.\n')
85
+ }
86
+ } catch (err) {
87
+ console.warn('⚠️ Coverage step failed. Continuing scan.')
88
+ }
89
+
90
+ console.log('🚀 Starting code scan...\n')
91
+
92
+ try {
93
+ // 4. 扫描 AST + 复杂度
94
+ console.log('📋 Step 2: Scanning targets...')
95
+ await runScript('core/scanner.mjs', [
96
+ '--config', config,
97
+ '--out', join(output, 'targets.json')
98
+ ])
99
+
100
+ // 5. Git 信号(可选)
101
+ if (!skipGit) {
102
+ console.log('\n📊 Step 3: Analyzing Git history...')
103
+ await runScript('core/git-analyzer.mjs', [
104
+ '--targets', join(output, 'targets.json'),
105
+ '--out', join(output, 'git_signals.json')
106
+ ])
107
+ }
108
+
109
+ // 6. 打分
110
+ console.log('\n🎯 Step 4: Scoring targets...')
111
+ const scoreArgs = [
112
+ '--targets', join(output, 'targets.json'),
113
+ '--config', config,
114
+ '--out-md', join(output, 'ut_scores.md'),
115
+ '--out-csv', join(output, 'ut_scores.csv')
116
+ ]
117
+
118
+ if (!skipGit && existsSync(join(output, 'git_signals.json'))) {
119
+ scoreArgs.push('--git', join(output, 'git_signals.json'))
120
+ }
121
+
122
+ await runScript('core/scorer.mjs', scoreArgs)
123
+
124
+ // 7. 统计结果
125
+ const reportPath = join(output, 'ut_scores.md')
126
+ if (existsSync(reportPath)) {
127
+ const content = readFileSync(reportPath, 'utf-8')
128
+ const todoCount = (content.match(/\| TODO \|/g) || []).length
129
+ const doneCount = (content.match(/\| DONE \|/g) || []).length
130
+
131
+ console.log('\n✅ Scan completed!')
132
+ console.log(`\n📊 Status:`)
133
+ console.log(` TODO: ${todoCount}`)
134
+ console.log(` DONE: ${doneCount}`)
135
+ console.log(` Total: ${todoCount + doneCount}`)
136
+ console.log(`\n📄 Report: ${reportPath}`)
137
+ console.log(`\n💡 Next: ai-test generate`)
138
+ }
139
+ } catch (err) {
140
+ console.error('❌ Scan failed:', err.message)
141
+ process.exit(1)
142
+ }
143
+ }
144
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-unit-test-generator",
3
- "version": "1.4.5",
3
+ "version": "2.0.1",
4
4
  "description": "AI-powered unit test generator with smart priority scoring",
5
5
  "keywords": [
6
6
  "unit-test",
@@ -310,5 +310,63 @@
310
310
  "CC": 6, // 复杂度默认值
311
311
  "ERLikelihood": 3, // 错误可能性默认值
312
312
  "Testability": 6 // 可测试性默认值
313
+ },
314
+
315
+ // ========== AI 增强配置(AI 唯一可写区域)==========
316
+
317
+ "aiEnhancement": {
318
+ "enabled": true, // 是否启用 AI 增强
319
+ "analyzed": false, // 是否已完成 AI 分析
320
+ "analyzedAt": null, // 分析时间戳
321
+
322
+ // ✅ 业务实体关键词(用于 AST 扫描识别业务对象)
323
+ "entityKeywords": [
324
+ "Payment", "Order", "Booking", "User", "Hotel", "Room",
325
+ "Cart", "Price", "Guest", "Request", "Response"
326
+ ],
327
+
328
+ // AI 建议(由 ai-test analyze 填充)
329
+ "suggestions": {
330
+ "businessCriticalPaths": [
331
+ // 示例:
332
+ // {
333
+ // "pattern": "services/payment/**",
334
+ // "confidence": 0.95,
335
+ // "reason": "Handles Stripe payment processing",
336
+ // "suggestedBC": 10,
337
+ // "evidence": [
338
+ // "Contains processPayment with Stripe API",
339
+ // "Referenced by checkout flow",
340
+ // "Handles money transactions"
341
+ // ]
342
+ // }
343
+ ],
344
+ "highRiskModules": [
345
+ // 示例:
346
+ // {
347
+ // "pattern": "utils/date/**",
348
+ // "confidence": 0.88,
349
+ // "reason": "Complex timezone calculations",
350
+ // "suggestedER": 8,
351
+ // "evidence": [
352
+ // "Multiple timezone conversions",
353
+ // "Handles DST edge cases"
354
+ // ]
355
+ // }
356
+ ],
357
+ "testabilityAdjustments": [
358
+ // 示例:
359
+ // {
360
+ // "pattern": "utils/**",
361
+ // "confidence": 0.92,
362
+ // "reason": "Pure functions, easy to test",
363
+ // "adjustment": "+1",
364
+ // "evidence": [
365
+ // "No side effects",
366
+ // "No external dependencies"
367
+ // ]
368
+ // }
369
+ ]
370
+ }
313
371
  }
314
372
  }