ai-unit-test-generator 1.3.0

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,43 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * 解析 reports/jest-report.json,提取失败原因并分类
4
+ * 输出一个简要的提示文本,可注入下一轮 Prompt
5
+ */
6
+
7
+ import { readFileSync, existsSync } from 'fs'
8
+
9
+ export function analyze(path = 'reports/jest-report.json') {
10
+ if (!existsSync(path)) return { summary: 'No jest-report.json', total: 0, failed: 0, hints: [] }
11
+ const json = JSON.parse(readFileSync(path, 'utf8'))
12
+ const testResults = json.testResults || []
13
+ const hints = []
14
+ let total = 0, failed = 0
15
+
16
+ for (const f of testResults) {
17
+ for (const a of (f.assertionResults || [])) {
18
+ total++
19
+ if (a.status === 'failed') failed++
20
+ }
21
+ for (const msg of (f.message ? [f.message] : [])) {
22
+ const m = String(msg)
23
+ if (m.includes('Cannot find module')) hints.push('修正导入路径或添加模块别名')
24
+ if (m.includes('TypeError')) hints.push('检查被测函数或 mock 是否返回预期类型')
25
+ if (m.includes('Timed out')) hints.push('为异步/计时器逻辑添加等待与 fakeTimers')
26
+ if (m.includes('not found') && m.includes('element')) hints.push('使用稳定的查询方式,如 getByRole/LabelText/TestId')
27
+ }
28
+ }
29
+
30
+ const uniqueHints = Array.from(new Set(hints))
31
+ const summary = `总断言: ${total}, 失败: ${failed}, 建议: ${uniqueHints.join(';')}`
32
+ return { summary, total, failed, hints: uniqueHints }
33
+ }
34
+
35
+ export function runCLI(argv = process.argv) {
36
+ const path = argv[2] || 'reports/jest-report.json'
37
+ const res = analyze(path)
38
+ console.log(JSON.stringify(res, null, 2))
39
+ }
40
+
41
+ if (import.meta.url === `file://${process.argv[1]}`) runCLI()
42
+
43
+
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Testing 模块:测试运行与失败分析
3
+ *
4
+ * 提供测试执行和失败报告分析功能
5
+ * 支持多种测试框架(目前实现:Jest)
6
+ */
7
+
8
+ export { main as runTests } from './runner.mjs'
9
+ export { main as analyzeFailures } from './analyzer.mjs'
10
+
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * 运行 Jest,产出 JSON 与覆盖率
4
+ */
5
+
6
+ import { spawn } from 'child_process'
7
+
8
+ function runJest(args = []) {
9
+ return new Promise((resolve, reject) => {
10
+ const child = spawn('npx', ['jest', '--json', '--outputFile=reports/jest-report.json', '--coverage', ...args], {
11
+ stdio: 'inherit',
12
+ cwd: process.cwd()
13
+ })
14
+ child.on('close', code => code === 0 ? resolve(0) : reject(new Error(`jest exited ${code}`)))
15
+ child.on('error', reject)
16
+ })
17
+ }
18
+
19
+ async function main(argv = process.argv) {
20
+ const extra = argv.slice(2)
21
+ try {
22
+ await runJest(extra)
23
+ process.exit(0)
24
+ } catch (err) {
25
+ console.error(err.message)
26
+ process.exit(1)
27
+ }
28
+ }
29
+
30
+ if (import.meta.url === `file://${process.argv[1]}`) main()
31
+
32
+
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Utils 模块:通用工具函数
3
+ *
4
+ * 提供跨模块使用的工具函数
5
+ */
6
+
7
+ export { markDone } from './marker.mjs'
8
+
9
+ // 可以在未来添加更多工具函数
10
+ // export { readJson, writeJson } from './file-io.mjs'
11
+
@@ -0,0 +1,182 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * 批量标记 ut_scores.md 中的完成状态
4
+ */
5
+
6
+ import { readFileSync, writeFileSync, existsSync } from 'fs';
7
+
8
+ /**
9
+ * 标记函数为完成状态
10
+ */
11
+ export function markDone(functionNames, options = {}) {
12
+ const {
13
+ reportPath = 'reports/ut_scores.md',
14
+ status = 'DONE',
15
+ dryRun = false
16
+ } = options;
17
+
18
+ if (!existsSync(reportPath)) {
19
+ throw new Error(`报告文件不存在: ${reportPath}`);
20
+ }
21
+
22
+ let content = readFileSync(reportPath, 'utf8');
23
+ const originalContent = content;
24
+
25
+ const results = {
26
+ success: [],
27
+ notFound: []
28
+ };
29
+
30
+ functionNames.forEach(name => {
31
+ // 匹配 | TODO | [score] | [priority] | [name] | 格式
32
+ const escapedName = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
33
+ const regex = new RegExp(
34
+ `\\| TODO (\\| [^|]+ \\| [^|]+ \\| ${escapedName} \\|)`,
35
+ 'g'
36
+ );
37
+
38
+ const newContent = content.replace(regex, `| ${status} $1`);
39
+
40
+ if (newContent !== content) {
41
+ results.success.push(name);
42
+ content = newContent;
43
+ } else {
44
+ results.notFound.push(name);
45
+ }
46
+ });
47
+
48
+ if (!dryRun && results.success.length > 0) {
49
+ writeFileSync(reportPath, content);
50
+ }
51
+
52
+ // 统计进度
53
+ const lines = content.split('\n');
54
+ const todoCount = lines.filter(l => l.includes('| TODO |')).length;
55
+ const doneCount = lines.filter(l => l.includes('| DONE |')).length;
56
+ const skipCount = lines.filter(l => l.includes('| SKIP |')).length;
57
+ const total = todoCount + doneCount + skipCount;
58
+
59
+ // 按优先级统计
60
+ const priorities = ['P0', 'P1', 'P2', 'P3'];
61
+ const priorityStats = {};
62
+ priorities.forEach(p => {
63
+ const pTotal = lines.filter(l => l.includes(`| ${p} |`)).length;
64
+ const pDone = lines.filter(l => l.includes('| DONE |') && l.includes(`| ${p} |`)).length;
65
+ const pTodo = lines.filter(l => l.includes('| TODO |') && l.includes(`| ${p} |`)).length;
66
+ if (pTotal > 0) {
67
+ priorityStats[p] = { total: pTotal, done: pDone, todo: pTodo };
68
+ }
69
+ });
70
+
71
+ return {
72
+ ...results,
73
+ stats: {
74
+ total,
75
+ todo: todoCount,
76
+ done: doneCount,
77
+ skip: skipCount,
78
+ priorities: priorityStats
79
+ }
80
+ };
81
+ }
82
+
83
+ /**
84
+ * CLI 入口
85
+ */
86
+ export function runCLI(argv = process.argv) {
87
+ const args = argv.slice(2);
88
+
89
+ if (args.length === 0) {
90
+ console.error('❌ 缺少参数\n');
91
+ console.error('用法:');
92
+ console.error(' ut-score mark-done <function-names> [options]\n');
93
+ console.error('参数:');
94
+ console.error(' function-names 函数名列表(逗号或空格分隔)\n');
95
+ console.error('选项:');
96
+ console.error(' --report PATH 报告文件路径 (默认: reports/ut_scores.md)');
97
+ console.error(' --status STATUS 标记状态 (默认: DONE, 可选: SKIP)');
98
+ console.error(' --dry-run 仅显示将要修改的内容,不实际写入\n');
99
+ console.error('示例:');
100
+ console.error(' ut-score mark-done disableDragBack getMediumScale');
101
+ console.error(' ut-score mark-done "formatToDate,handleQuery"');
102
+ console.error(' ut-score mark-done func1 --status SKIP');
103
+ process.exit(1);
104
+ }
105
+
106
+ // 解析参数
107
+ const options = { reportPath: 'reports/ut_scores.md', status: 'DONE' };
108
+ const names = [];
109
+
110
+ for (let i = 0; i < args.length; i++) {
111
+ if (args[i] === '--report') {
112
+ options.reportPath = args[++i];
113
+ } else if (args[i] === '--status') {
114
+ options.status = args[++i];
115
+ } else if (args[i] === '--dry-run') {
116
+ options.dryRun = true;
117
+ } else {
118
+ names.push(args[i]);
119
+ }
120
+ }
121
+
122
+ // 解析函数名(支持逗号分隔和空格分隔)
123
+ const functionNames = names.join(' ').split(/[,\s]+/).map(s => s.trim()).filter(Boolean);
124
+
125
+ if (functionNames.length === 0) {
126
+ console.error('❌ 没有指定函数名');
127
+ process.exit(1);
128
+ }
129
+
130
+ try {
131
+ console.log(`🔍 准备标记 ${functionNames.length} 个函数为 ${options.status}...\n`);
132
+
133
+ const result = markDone(functionNames, options);
134
+
135
+ if (result.success.length > 0) {
136
+ console.log(`✅ 成功标记 ${result.success.length} 个函数为 ${options.status}:`);
137
+ result.success.forEach(name => console.log(` - ${name}`));
138
+ console.log('');
139
+ }
140
+
141
+ if (result.notFound.length > 0) {
142
+ console.log(`⚠️ 未找到 ${result.notFound.length} 个函数(可能已标记或不存在):`);
143
+ result.notFound.forEach(name => console.log(` - ${name}`));
144
+ console.log('');
145
+ }
146
+
147
+ // 显示进度
148
+ const { stats } = result;
149
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
150
+ console.log('📊 当前进度:');
151
+ console.log(` 总计: ${stats.total}`);
152
+ console.log(` 已完成: ${stats.done} (${(stats.done / stats.total * 100).toFixed(1)}%)`);
153
+ console.log(` 待完成: ${stats.todo}`);
154
+ if (stats.skip > 0) {
155
+ console.log(` 已跳过: ${stats.skip}`);
156
+ }
157
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
158
+
159
+ // 按优先级统计
160
+ if (Object.keys(stats.priorities).length > 0) {
161
+ console.log('');
162
+ console.log('📈 按优先级统计:');
163
+ Object.entries(stats.priorities).forEach(([p, data]) => {
164
+ console.log(` ${p}: ${data.done}/${data.total} 完成,${data.todo} 待处理`);
165
+ });
166
+ }
167
+
168
+ if (options.dryRun) {
169
+ console.log('');
170
+ console.log('ℹ️ Dry-run 模式,未实际修改文件');
171
+ }
172
+ } catch (err) {
173
+ console.error(`❌ 错误: ${err.message}`);
174
+ process.exit(1);
175
+ }
176
+ }
177
+
178
+ // 作为脚本直接运行时
179
+ if (import.meta.url === `file://${process.argv[1]}`) {
180
+ runCLI();
181
+ }
182
+
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * 循环所有批次:每批 N 个,直到没有 TODO
4
+ */
5
+
6
+ import { readFileSync, existsSync } from 'fs'
7
+ import { spawn } from 'child_process'
8
+
9
+ function sh(cmd, args = []) { return new Promise((resolve, reject) => {
10
+ const child = spawn(cmd, args, { stdio: 'inherit', cwd: process.cwd() })
11
+ child.on('close', code => code === 0 ? resolve(0) : reject(new Error(`${cmd} exited ${code}`)))
12
+ child.on('error', reject)
13
+ })}
14
+
15
+ function countTodos(path = 'reports/ut_scores.md') {
16
+ if (!existsSync(path)) return 0
17
+ const md = readFileSync(path, 'utf8')
18
+ return (md.match(/\|\s*TODO\s*\|/g) || []).length
19
+ }
20
+
21
+ async function main(argv = process.argv) {
22
+ const args = argv.slice(2)
23
+ const priority = args[0] || 'P0'
24
+ const batchSize = Number(args[1] || 10)
25
+ let skip = Number(args[2] || 0)
26
+
27
+ while (true) {
28
+ const remain = countTodos()
29
+ if (remain === 0) {
30
+ console.log('✅ All TODO items are done.')
31
+ break
32
+ }
33
+ console.log(`
34
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
35
+ Batch starting... priority=${priority}, size=${batchSize}, skip=${skip}
36
+ Remaining TODO: ${remain}
37
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`)
38
+
39
+ try {
40
+ await sh('node', ['ai-test-generator/lib/run-batch.mjs', priority, String(batchSize), String(skip)])
41
+ } catch (err) {
42
+ console.error('Batch failed:', err.message)
43
+ }
44
+
45
+ skip += batchSize
46
+ }
47
+ }
48
+
49
+ if (import.meta.url === `file://${process.argv[1]}`) main()
50
+
51
+
@@ -0,0 +1,187 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * 单批次:生成 prompt → 调用 AI → 提取测试 → 运行 Jest → 自动标记状态
4
+ */
5
+
6
+ import { spawn } from 'child_process'
7
+ import { existsSync, readFileSync, writeFileSync } 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
+ /**
29
+ * 从报告中读取 TODO 函数列表
30
+ */
31
+ function readTodoFunctions(reportPath, priority, limit) {
32
+ if (!existsSync(reportPath)) {
33
+ throw new Error(`Report not found: ${reportPath}`)
34
+ }
35
+
36
+ const content = readFileSync(reportPath, 'utf-8')
37
+ const lines = content.split('\n')
38
+
39
+ const todoFunctions = []
40
+ for (const line of lines) {
41
+ if (line.includes('| TODO |') && line.includes(`| ${priority} |`)) {
42
+ // 解析表格行: | Name | Path | ... | Priority | Score | Status |
43
+ const parts = line.split('|').map(p => p.trim()).filter(Boolean)
44
+ if (parts.length >= 6) {
45
+ todoFunctions.push({
46
+ name: parts[0],
47
+ path: parts[1]
48
+ })
49
+ }
50
+ }
51
+ }
52
+
53
+ return todoFunctions.slice(0, limit)
54
+ }
55
+
56
+ /**
57
+ * 标记函数状态为 DONE
58
+ */
59
+ function markFunctionsDone(reportPath, functionNames) {
60
+ if (!existsSync(reportPath)) return
61
+
62
+ let content = readFileSync(reportPath, 'utf-8')
63
+
64
+ for (const name of functionNames) {
65
+ // 查找包含该函数名且状态为 TODO 的行,替换为 DONE
66
+ const lines = content.split('\n')
67
+ for (let i = 0; i < lines.length; i++) {
68
+ if (lines[i].includes(`| ${name} |`) && lines[i].includes('| TODO |')) {
69
+ lines[i] = lines[i].replace('| TODO |', '| DONE |')
70
+ }
71
+ }
72
+ content = lines.join('\n')
73
+ }
74
+
75
+ writeFileSync(reportPath, content, 'utf-8')
76
+ }
77
+
78
+ async function main(argv = process.argv) {
79
+ const args = argv.slice(2)
80
+ const priority = args[0] || 'P0'
81
+ const limit = Number(args[1] || 10)
82
+ const skip = Number(args[2] || 0)
83
+ const reportPath = args[3] || 'reports/ut_scores.md'
84
+ const minCovDelta = priority === 'P0' ? 2 : (priority === 'P1' ? 1 : 0)
85
+
86
+ // 读取 TODO 函数列表(跳过 DONE)
87
+ console.log(`📋 Reading TODO functions from ${reportPath}...`)
88
+ const todoFunctions = readTodoFunctions(reportPath, priority, limit)
89
+
90
+ if (todoFunctions.length === 0) {
91
+ console.log(`✅ No TODO functions found for ${priority}`)
92
+ return
93
+ }
94
+
95
+ console.log(`📝 Found ${todoFunctions.length} TODO functions`)
96
+
97
+ // 记录初始覆盖率
98
+ const beforeCov = getCoveragePercent(readCoverageSummary())
99
+
100
+ // 1) 生成 Prompt(只针对 TODO 函数,加入上一轮失败提示)
101
+ const promptArgs = [
102
+ 'ai-test-generator/lib/ai/prompt-builder.mjs',
103
+ '--report', reportPath,
104
+ '-p', priority,
105
+ '-n', String(limit),
106
+ '--skip', String(skip),
107
+ '--only-todo' // 新增:只处理 TODO 状态
108
+ ]
109
+ try {
110
+ await sh('node', [...promptArgs, '--hints-file', 'reports/hints.txt'])
111
+ } catch {
112
+ await sh('node', promptArgs)
113
+ }
114
+
115
+ // 2) 调用 AI
116
+ console.log('\n🤖 Calling AI...')
117
+ await sh('node', ['ai-test-generator/lib/ai/client.mjs', '--prompt', 'prompt.txt', '--out', 'reports/ai_response.txt'])
118
+
119
+ // 3) 提取测试
120
+ console.log('\n📦 Extracting tests...')
121
+ await sh('node', ['ai-test-generator/lib/ai/extractor.mjs', 'reports/ai_response.txt', '--overwrite'])
122
+
123
+ // 4) 运行 Jest(按优先级自适应重跑)
124
+ console.log('\n🧪 Running tests...')
125
+ const reruns = priority === 'P0' ? 1 : 0
126
+ let testsPassed = false
127
+
128
+ for (let i = 0; i < Math.max(1, reruns + 1); i++) {
129
+ try {
130
+ await sh('node', ['ai-test-generator/lib/testing/runner.mjs'])
131
+ testsPassed = true
132
+ break
133
+ } catch {
134
+ if (i === reruns) {
135
+ console.warn('⚠️ Tests failed after retries')
136
+ }
137
+ }
138
+ }
139
+
140
+ // 5) 校验覆盖率增量
141
+ if (minCovDelta > 0) {
142
+ const afterCov = getCoveragePercent(readCoverageSummary())
143
+ const delta = afterCov - beforeCov
144
+ if (delta < minCovDelta) {
145
+ console.warn(`⚠️ Coverage delta ${delta.toFixed(2)}% < required ${minCovDelta}% (before: ${beforeCov.toFixed(2)}%, after: ${afterCov.toFixed(2)}%)`)
146
+ } else {
147
+ console.log(`✅ Coverage improved: ${beforeCov.toFixed(2)}% → ${afterCov.toFixed(2)}% (+${delta.toFixed(2)}%)`)
148
+ }
149
+ }
150
+
151
+ // 6) 自动标记 DONE(如果测试通过)
152
+ if (testsPassed) {
153
+ console.log('\n✏️ Marking functions as DONE...')
154
+ const functionNames = todoFunctions.map(f => f.name)
155
+ markFunctionsDone(reportPath, functionNames)
156
+ console.log(`✅ Marked ${functionNames.length} functions as DONE`)
157
+ } else {
158
+ console.log('\n⚠️ Tests failed, keeping functions as TODO for retry')
159
+ }
160
+
161
+ // 7) 失败分析并落盘 hints(用于下次重试)
162
+ console.log('\n🔍 Analyzing failures...')
163
+ const { spawn: spawnLocal } = await import('child_process')
164
+ const { writeFileSync: writeFileSyncLocal } = await import('fs')
165
+ await new Promise((resolve) => {
166
+ const child = spawnLocal('node', ['ai-test-generator/lib/testing/analyzer.mjs'], { stdio: ['inherit','pipe','inherit'] })
167
+ const chunks = []
168
+ child.stdout.on('data', d => chunks.push(Buffer.from(d)))
169
+ child.on('close', () => {
170
+ try {
171
+ const obj = JSON.parse(Buffer.concat(chunks).toString('utf8'))
172
+ if (obj.hints?.length) {
173
+ writeFileSyncLocal('reports/hints.txt', `# 上一轮失败修复建议\n- ${obj.hints.join('\n- ')}`)
174
+ console.log(`💡 Saved ${obj.hints.length} hints for next run`)
175
+ }
176
+ } catch {}
177
+ resolve()
178
+ })
179
+ })
180
+
181
+ console.log('\n✅ Batch completed!')
182
+ }
183
+
184
+ main().catch(err => {
185
+ console.error('❌ Batch failed:', err.message)
186
+ process.exit(1)
187
+ })
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Workflows 模块:编排工作流
3
+ *
4
+ * 提供批量测试生成和全自动化流程编排
5
+ * 组合 Core、AI 和 Testing 模块实现端到端工作流
6
+ */
7
+
8
+ export { main as runBatch } from './batch.mjs'
9
+ export { main as runAll } from './all.mjs'
10
+
package/package.json ADDED
@@ -0,0 +1,69 @@
1
+ {
2
+ "name": "ai-unit-test-generator",
3
+ "version": "1.3.0",
4
+ "description": "AI-powered unit test generator with smart priority scoring",
5
+ "keywords": [
6
+ "unit-test",
7
+ "testing",
8
+ "priority",
9
+ "scoring",
10
+ "ai-test-generation",
11
+ "code-analysis",
12
+ "typescript",
13
+ "react"
14
+ ],
15
+ "author": "YuhengZhou",
16
+ "license": "MIT",
17
+ "type": "module",
18
+ "main": "./lib/index.mjs",
19
+ "bin": {
20
+ "ai-test": "./bin/cli.js"
21
+ },
22
+ "exports": {
23
+ ".": "./lib/index.mjs",
24
+ "./core": "./lib/core/index.mjs",
25
+ "./ai": "./lib/ai/index.mjs",
26
+ "./testing": "./lib/testing/index.mjs",
27
+ "./workflows": "./lib/workflows/index.mjs",
28
+ "./utils": "./lib/utils/index.mjs"
29
+ },
30
+ "files": [
31
+ "bin",
32
+ "lib",
33
+ "templates",
34
+ "README.md",
35
+ "CHANGELOG.md",
36
+ "LICENSE"
37
+ ],
38
+ "scripts": {
39
+ "prepublishOnly": "echo 'Ready to publish'",
40
+ "test": "echo \"Error: no test specified\" && exit 1"
41
+ },
42
+ "dependencies": {
43
+ "@eslint/js": "^9.17.0",
44
+ "commander": "^12.1.0",
45
+ "csv-stringify": "^6.5.2",
46
+ "escomplex": "^2.0.0-alpha",
47
+ "esprima": "^4.0.1",
48
+ "eslint": "^9.17.0",
49
+ "eslint-plugin-sonarjs": "^3.0.1",
50
+ "fast-glob": "^3.3.3",
51
+ "markdown-table": "^3.0.4",
52
+ "ts-morph": "^24.0.0",
53
+ "typescript": "^5.7.3",
54
+ "typescript-eslint": "^8.19.1"
55
+ },
56
+ "devDependencies": {},
57
+ "engines": {
58
+ "node": ">=18.0.0"
59
+ },
60
+ "repository": {
61
+ "type": "git",
62
+ "url": "https://github.com/YuhengZhou/ai-unit-test-generator.git"
63
+ },
64
+ "bugs": {
65
+ "url": "https://github.com/YuhengZhou/ai-unit-test-generator/issues"
66
+ },
67
+ "homepage": "https://github.com/YuhengZhou/ai-unit-test-generator#readme"
68
+ }
69
+