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.
- package/CHANGELOG.md +264 -0
- package/LICENSE +22 -0
- package/README.md +432 -0
- package/bin/cli.js +217 -0
- package/lib/ai/client.mjs +79 -0
- package/lib/ai/extractor.mjs +199 -0
- package/lib/ai/index.mjs +11 -0
- package/lib/ai/prompt-builder.mjs +298 -0
- package/lib/core/git-analyzer.mjs +151 -0
- package/lib/core/index.mjs +11 -0
- package/lib/core/scanner.mjs +233 -0
- package/lib/core/scorer.mjs +633 -0
- package/lib/index.js +18 -0
- package/lib/index.mjs +25 -0
- package/lib/testing/analyzer.mjs +43 -0
- package/lib/testing/index.mjs +10 -0
- package/lib/testing/runner.mjs +32 -0
- package/lib/utils/index.mjs +11 -0
- package/lib/utils/marker.mjs +182 -0
- package/lib/workflows/all.mjs +51 -0
- package/lib/workflows/batch.mjs +187 -0
- package/lib/workflows/index.mjs +10 -0
- package/package.json +69 -0
- package/templates/default.config.json +199 -0
@@ -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,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,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
|
+
})
|
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
|
+
|