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,79 @@
|
|
1
|
+
#!/usr/bin/env node
|
2
|
+
/**
|
3
|
+
* 使用 cursor-agent 生成 AI 回复
|
4
|
+
* - 从文件或 stdin 读取 prompt
|
5
|
+
* - 通过 cursor-agent chat 调用模型
|
6
|
+
* - 将回复写入到输出文件
|
7
|
+
*/
|
8
|
+
|
9
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs'
|
10
|
+
import { dirname } from 'path'
|
11
|
+
import { spawn } from 'child_process'
|
12
|
+
|
13
|
+
function parseArgs(argv) {
|
14
|
+
const args = {}
|
15
|
+
for (let i = 2; i < argv.length; i++) {
|
16
|
+
const a = argv[i]
|
17
|
+
if (a.startsWith('--')) {
|
18
|
+
const [k, v] = a.includes('=') ? a.split('=') : [a, argv[i + 1]]
|
19
|
+
args[k.replace(/^--/, '')] = v === undefined || v.startsWith('--') ? true : v
|
20
|
+
if (v !== undefined && !v.startsWith('--') && !a.includes('=')) i++
|
21
|
+
}
|
22
|
+
}
|
23
|
+
return args
|
24
|
+
}
|
25
|
+
|
26
|
+
export async function runOnce({ prompt, promptFile, out = 'reports/ai_response.txt', model, timeoutSec = 600 }) {
|
27
|
+
if (!prompt) {
|
28
|
+
if (!promptFile || !existsSync(promptFile)) throw new Error(`Prompt file not found: ${promptFile}`)
|
29
|
+
prompt = readFileSync(promptFile, 'utf8')
|
30
|
+
}
|
31
|
+
|
32
|
+
return new Promise((resolve, reject) => {
|
33
|
+
const args = ['chat']
|
34
|
+
if (model) args.push('-m', model)
|
35
|
+
|
36
|
+
const child = spawn('cursor-agent', args, { stdio: ['pipe', 'pipe', 'inherit'] })
|
37
|
+
|
38
|
+
const chunks = []
|
39
|
+
child.stdout.on('data', d => chunks.push(Buffer.from(d)))
|
40
|
+
|
41
|
+
// 写入 prompt 到 stdin
|
42
|
+
child.stdin.write(prompt)
|
43
|
+
child.stdin.end()
|
44
|
+
|
45
|
+
const to = setTimeout(() => {
|
46
|
+
child.kill('SIGKILL')
|
47
|
+
reject(new Error(`cursor-agent timeout after ${timeoutSec}s`))
|
48
|
+
}, Number(timeoutSec) * 1000)
|
49
|
+
|
50
|
+
child.on('close', code => {
|
51
|
+
clearTimeout(to)
|
52
|
+
const outText = Buffer.concat(chunks).toString('utf8')
|
53
|
+
try { mkdirSync(dirname(out), { recursive: true }); writeFileSync(out, outText) } catch {}
|
54
|
+
if (code !== 0) return reject(new Error(`cursor-agent exited with code ${code}`))
|
55
|
+
resolve({ out, bytes: outText.length })
|
56
|
+
})
|
57
|
+
child.on('error', reject)
|
58
|
+
})
|
59
|
+
}
|
60
|
+
|
61
|
+
export async function runCLI(argv = process.argv) {
|
62
|
+
const args = parseArgs(argv)
|
63
|
+
const promptFile = args['prompt'] || args['prompt-file'] || null
|
64
|
+
const out = args['out'] || 'reports/ai_response.txt'
|
65
|
+
const model = args['model'] || null
|
66
|
+
const timeoutSec = args['timeout'] ? Number(args['timeout']) : 600
|
67
|
+
|
68
|
+
try {
|
69
|
+
await runOnce({ promptFile, out, model, timeoutSec })
|
70
|
+
console.log(`✅ AI response saved: ${out}`)
|
71
|
+
} catch (err) {
|
72
|
+
console.error(`❌ AI generate failed: ${err.message}`)
|
73
|
+
process.exit(1)
|
74
|
+
}
|
75
|
+
}
|
76
|
+
|
77
|
+
if (import.meta.url === `file://${process.argv[1]}`) runCLI()
|
78
|
+
|
79
|
+
|
@@ -0,0 +1,199 @@
|
|
1
|
+
#!/usr/bin/env node
|
2
|
+
/**
|
3
|
+
* 从 AI 回复中提取测试文件并自动创建
|
4
|
+
*/
|
5
|
+
|
6
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
|
7
|
+
import { dirname } from 'path';
|
8
|
+
|
9
|
+
/**
|
10
|
+
* 从 AI 响应中提取测试文件
|
11
|
+
*/
|
12
|
+
export function extractTests(content, options = {}) {
|
13
|
+
const { overwrite = false, dryRun = false } = options;
|
14
|
+
|
15
|
+
// 先尝试解析 JSON Manifest(优先)
|
16
|
+
// 形如:```json { version: 1, files: [{ path, source, ... }] }
|
17
|
+
let manifest = null;
|
18
|
+
const manifestRegex = /```json\s*\n([\s\S]*?)\n```/gi;
|
19
|
+
let m;
|
20
|
+
while ((m = manifestRegex.exec(content)) !== null) {
|
21
|
+
const jsonStr = m[1].trim();
|
22
|
+
try {
|
23
|
+
const obj = JSON.parse(jsonStr);
|
24
|
+
if (obj && Array.isArray(obj.files)) {
|
25
|
+
manifest = obj;
|
26
|
+
break;
|
27
|
+
}
|
28
|
+
} catch {}
|
29
|
+
}
|
30
|
+
const manifestPaths = manifest?.files?.map(f => String(f.path).trim()) ?? [];
|
31
|
+
|
32
|
+
// 再匹配多种文件代码块格式(回退/兼容,增强鲁棒性)
|
33
|
+
const patterns = [
|
34
|
+
// 格式1: ### 测试文件: path (有/无反引号)
|
35
|
+
/###\s*测试文件\s*[::]\s*`?([^\n`]+?)`?\s*\n```(?:typescript|ts|tsx|javascript|js|jsx|json|text)?\s*\n([\s\S]*?)\n```/gi,
|
36
|
+
// 格式2: **测试文件**: path
|
37
|
+
/\*\*测试文件\*\*\s*[::]\s*`?([^\n`]+)`?\s*\n```(?:typescript|ts|tsx|javascript|js|jsx|json|text)?\s*\n([\s\S]*?)\n```/gi,
|
38
|
+
// 格式3: 文件路径: path
|
39
|
+
/文件路径\s*[::]\s*`?([^\n`]+)`?\s*\n```(?:typescript|ts|tsx|javascript|js|jsx|json|text)?\s*\n([\s\S]*?)\n```/gi,
|
40
|
+
// 格式4: # path.test.ts (仅文件名标题)
|
41
|
+
/^#+\s+([^\n]+\.test\.[jt]sx?)\s*\n```(?:typescript|ts|tsx|javascript|js|jsx)?\s*\n([\s\S]*?)\n```/gim,
|
42
|
+
// 格式5: 更宽松的匹配(任意"path"后接代码块,路径必须含 .test.)
|
43
|
+
/(?:path|文件|file)\s*[::]?\s*`?([^\n`]*\.test\.[jt]sx?[^\n`]*?)`?\s*\n```(?:typescript|ts|tsx|javascript|js|jsx)?\s*\n([\s\S]*?)\n```/gi,
|
44
|
+
];
|
45
|
+
|
46
|
+
const created = [];
|
47
|
+
const skipped = [];
|
48
|
+
const errors = [];
|
49
|
+
|
50
|
+
patterns.forEach(fileRegex => {
|
51
|
+
let match;
|
52
|
+
while ((match = fileRegex.exec(content)) !== null) {
|
53
|
+
const [, filePath, testCode] = match;
|
54
|
+
const cleanPath = filePath.trim();
|
55
|
+
|
56
|
+
// 若存在 Manifest,则只允许清单内的路径
|
57
|
+
if (manifestPaths.length > 0 && !manifestPaths.includes(cleanPath)) {
|
58
|
+
continue;
|
59
|
+
}
|
60
|
+
|
61
|
+
// 跳过已处理的文件
|
62
|
+
if (created.some(f => f.path === cleanPath) ||
|
63
|
+
skipped.some(f => f.path === cleanPath)) {
|
64
|
+
continue;
|
65
|
+
}
|
66
|
+
|
67
|
+
// 检查文件是否已存在
|
68
|
+
if (existsSync(cleanPath) && !overwrite) {
|
69
|
+
skipped.push({ path: cleanPath, reason: 'exists' });
|
70
|
+
continue;
|
71
|
+
}
|
72
|
+
|
73
|
+
if (dryRun) {
|
74
|
+
created.push({ path: cleanPath, code: testCode.trim(), dryRun: true });
|
75
|
+
continue;
|
76
|
+
}
|
77
|
+
|
78
|
+
try {
|
79
|
+
// 确保目录存在
|
80
|
+
mkdirSync(dirname(cleanPath), { recursive: true });
|
81
|
+
|
82
|
+
// 写入测试文件
|
83
|
+
const cleanCode = testCode.trim();
|
84
|
+
writeFileSync(cleanPath, cleanCode + '\n');
|
85
|
+
created.push({ path: cleanPath, code: cleanCode });
|
86
|
+
} catch (err) {
|
87
|
+
errors.push({ path: cleanPath, error: err.message });
|
88
|
+
}
|
89
|
+
}
|
90
|
+
});
|
91
|
+
|
92
|
+
// 若有 Manifest 但未生成任何文件,则报告可能缺失代码块
|
93
|
+
if (manifestPaths.length > 0) {
|
94
|
+
const createdPaths = new Set(created.map(f => f.path));
|
95
|
+
const missing = manifestPaths.filter(p => !createdPaths.has(p));
|
96
|
+
missing.forEach(p => errors.push({ path: p, error: 'missing code block for manifest entry' }));
|
97
|
+
}
|
98
|
+
|
99
|
+
return { created, skipped, errors };
|
100
|
+
}
|
101
|
+
|
102
|
+
/**
|
103
|
+
* CLI 入口
|
104
|
+
*/
|
105
|
+
export function runCLI(argv = process.argv) {
|
106
|
+
const args = argv.slice(2);
|
107
|
+
|
108
|
+
if (args.length === 0) {
|
109
|
+
console.error('❌ 缺少参数\n');
|
110
|
+
console.error('用法: ai-test extract-tests <response-file> [options]\n');
|
111
|
+
console.error('选项:');
|
112
|
+
console.error(' --overwrite 覆盖已存在的文件');
|
113
|
+
console.error(' --dry-run 仅显示将要创建的文件,不实际写入\n');
|
114
|
+
console.error('示例:');
|
115
|
+
console.error(' ai-test extract-tests response.txt');
|
116
|
+
console.error(' ai-test extract-tests response.txt --overwrite');
|
117
|
+
process.exit(1);
|
118
|
+
}
|
119
|
+
|
120
|
+
const responseFile = args[0];
|
121
|
+
const options = {
|
122
|
+
overwrite: args.includes('--overwrite'),
|
123
|
+
dryRun: args.includes('--dry-run')
|
124
|
+
};
|
125
|
+
|
126
|
+
if (!existsSync(responseFile)) {
|
127
|
+
console.error(`❌ 文件不存在: ${responseFile}`);
|
128
|
+
process.exit(1);
|
129
|
+
}
|
130
|
+
|
131
|
+
try {
|
132
|
+
const content = readFileSync(responseFile, 'utf8');
|
133
|
+
const { created, skipped, errors } = extractTests(content, options);
|
134
|
+
|
135
|
+
// 输出结果
|
136
|
+
created.forEach(f => {
|
137
|
+
if (f.dryRun) {
|
138
|
+
console.log(`[DRY-RUN] 将创建: ${f.path}`);
|
139
|
+
} else {
|
140
|
+
console.log(`✅ 创建测试文件: ${f.path}`);
|
141
|
+
}
|
142
|
+
});
|
143
|
+
|
144
|
+
skipped.forEach(f => {
|
145
|
+
if (f.reason === 'exists') {
|
146
|
+
console.log(`⚠️ 跳过(已存在): ${f.path}`);
|
147
|
+
}
|
148
|
+
});
|
149
|
+
|
150
|
+
errors.forEach(f => {
|
151
|
+
console.error(`❌ 创建失败: ${f.path} - ${f.error}`);
|
152
|
+
});
|
153
|
+
|
154
|
+
console.log('');
|
155
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
156
|
+
console.log(`✨ 总共创建 ${created.length} 个测试文件`);
|
157
|
+
if (skipped.length > 0) {
|
158
|
+
console.log(`⚠️ 跳过 ${skipped.length} 个已存在的文件`);
|
159
|
+
}
|
160
|
+
if (errors.length > 0) {
|
161
|
+
console.log(`❌ ${errors.length} 个文件创建失败`);
|
162
|
+
}
|
163
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
164
|
+
console.log('');
|
165
|
+
|
166
|
+
if (created.length > 0 && !options.dryRun) {
|
167
|
+
console.log('🧪 运行测试:');
|
168
|
+
console.log(' npm test');
|
169
|
+
console.log('');
|
170
|
+
console.log('📝 标记完成(如需):');
|
171
|
+
const functionNames = created
|
172
|
+
.map(f => f.path.match(/\/([^/]+)\.test\./)?.[1])
|
173
|
+
.filter(Boolean)
|
174
|
+
.join(',');
|
175
|
+
if (functionNames) {
|
176
|
+
console.log(` ai-test mark-done "${functionNames}"`);
|
177
|
+
}
|
178
|
+
} else if (created.length === 0) {
|
179
|
+
console.log('❌ 未找到任何测试文件');
|
180
|
+
console.log('');
|
181
|
+
console.log('请检查 AI 回复格式是否正确。预期格式:');
|
182
|
+
console.log(' ### 测试文件: src/utils/xxx.test.ts');
|
183
|
+
console.log(' ```typescript');
|
184
|
+
console.log(' // 测试代码');
|
185
|
+
console.log(' ```');
|
186
|
+
}
|
187
|
+
|
188
|
+
process.exit(errors.length > 0 ? 1 : 0);
|
189
|
+
} catch (err) {
|
190
|
+
console.error(`❌ 错误: ${err.message}`);
|
191
|
+
process.exit(1);
|
192
|
+
}
|
193
|
+
}
|
194
|
+
|
195
|
+
// 作为脚本直接运行时
|
196
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
197
|
+
runCLI();
|
198
|
+
}
|
199
|
+
|
package/lib/ai/index.mjs
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
/**
|
2
|
+
* AI 模块:AI 交互与测试生成
|
3
|
+
*
|
4
|
+
* 提供 Prompt 构建、AI 调用和测试提取功能
|
5
|
+
* 支持多种 LLM(目前实现:cursor-agent)
|
6
|
+
*/
|
7
|
+
|
8
|
+
export { buildBatchPrompt, runCLI as buildPrompt } from './prompt-builder.mjs'
|
9
|
+
export { main as callAI } from './client.mjs'
|
10
|
+
export { extractTests } from './extractor.mjs'
|
11
|
+
|
@@ -0,0 +1,298 @@
|
|
1
|
+
#!/usr/bin/env node
|
2
|
+
/**
|
3
|
+
* 生成批量测试的 AI Prompt
|
4
|
+
* 从评分报告中提取目标,构建包含源码上下文的 Prompt
|
5
|
+
*/
|
6
|
+
|
7
|
+
import { readFileSync, existsSync } from 'fs';
|
8
|
+
import { resolve } from 'path';
|
9
|
+
|
10
|
+
/**
|
11
|
+
* 从 ut_scores.md 中解析测试目标
|
12
|
+
*/
|
13
|
+
export function parseTargets(mdPath, filter = {}) {
|
14
|
+
if (!existsSync(mdPath)) {
|
15
|
+
throw new Error(`报告文件不存在: ${mdPath}`);
|
16
|
+
}
|
17
|
+
|
18
|
+
const content = readFileSync(mdPath, 'utf8');
|
19
|
+
const lines = content.split('\n');
|
20
|
+
const targets = [];
|
21
|
+
|
22
|
+
lines.forEach(line => {
|
23
|
+
if (!line.includes('| TODO |')) return;
|
24
|
+
|
25
|
+
const parts = line.split('|').map(s => s.trim());
|
26
|
+
if (parts.length < 8) return;
|
27
|
+
|
28
|
+
const [, status, score, priority, name, type, layer, path] = parts;
|
29
|
+
|
30
|
+
// 应用过滤器
|
31
|
+
if (filter.priority && priority !== filter.priority) return;
|
32
|
+
if (filter.layer && !layer.includes(filter.layer)) return;
|
33
|
+
if (filter.minScore && parseFloat(score) < filter.minScore) return;
|
34
|
+
if (filter.onlyPaths && Array.isArray(filter.onlyPaths) && filter.onlyPaths.length > 0) {
|
35
|
+
const allow = filter.onlyPaths.some(p => path === p || path.endsWith(p));
|
36
|
+
if (!allow) return;
|
37
|
+
}
|
38
|
+
|
39
|
+
targets.push({
|
40
|
+
name,
|
41
|
+
type,
|
42
|
+
layer,
|
43
|
+
path,
|
44
|
+
priority,
|
45
|
+
score: parseFloat(score)
|
46
|
+
});
|
47
|
+
});
|
48
|
+
|
49
|
+
return targets;
|
50
|
+
}
|
51
|
+
|
52
|
+
/**
|
53
|
+
* 提取函数源码
|
54
|
+
*/
|
55
|
+
export function extractFunctionCode(filePath, functionName) {
|
56
|
+
if (!existsSync(filePath)) {
|
57
|
+
return `// 文件不存在: ${filePath}`;
|
58
|
+
}
|
59
|
+
|
60
|
+
try {
|
61
|
+
const content = readFileSync(filePath, 'utf8');
|
62
|
+
|
63
|
+
// 多种函数定义模式
|
64
|
+
const patterns = [
|
65
|
+
new RegExp(`export\\s+(async\\s+)?function\\s+${functionName}\\s*\\([^)]*\\)`, 'm'),
|
66
|
+
new RegExp(`export\\s+const\\s+${functionName}\\s*=`, 'm'),
|
67
|
+
new RegExp(`function\\s+${functionName}\\s*\\([^)]*\\)`, 'm'),
|
68
|
+
new RegExp(`const\\s+${functionName}\\s*=`, 'm'),
|
69
|
+
];
|
70
|
+
|
71
|
+
let matchIndex = -1;
|
72
|
+
|
73
|
+
for (const pattern of patterns) {
|
74
|
+
const match = content.match(pattern);
|
75
|
+
if (match) {
|
76
|
+
matchIndex = content.indexOf(match[0]);
|
77
|
+
break;
|
78
|
+
}
|
79
|
+
}
|
80
|
+
|
81
|
+
if (matchIndex === -1) {
|
82
|
+
return `// 未找到函数: ${functionName}`;
|
83
|
+
}
|
84
|
+
|
85
|
+
// 提取函数体
|
86
|
+
let braceCount = 0;
|
87
|
+
let inFunction = false;
|
88
|
+
let funcCode = '';
|
89
|
+
|
90
|
+
for (let i = matchIndex; i < content.length; i++) {
|
91
|
+
const char = content[i];
|
92
|
+
funcCode += char;
|
93
|
+
|
94
|
+
if (char === '{') {
|
95
|
+
braceCount++;
|
96
|
+
inFunction = true;
|
97
|
+
} else if (char === '}') {
|
98
|
+
braceCount--;
|
99
|
+
if (inFunction && braceCount === 0) break;
|
100
|
+
}
|
101
|
+
|
102
|
+
// 简单处理箭头函数
|
103
|
+
if (funcCode.includes('=>') && !inFunction && char === ';') break;
|
104
|
+
}
|
105
|
+
|
106
|
+
return funcCode || content.slice(matchIndex, Math.min(matchIndex + 1000, content.length));
|
107
|
+
} catch (err) {
|
108
|
+
return `// 读取失败: ${err.message}`;
|
109
|
+
}
|
110
|
+
}
|
111
|
+
|
112
|
+
/**
|
113
|
+
* 构建测试生成 Prompt
|
114
|
+
*/
|
115
|
+
export function buildBatchPrompt(targets, options = {}) {
|
116
|
+
const {
|
117
|
+
framework = 'React + TypeScript',
|
118
|
+
testFramework = 'Jest',
|
119
|
+
coverageTarget = 80,
|
120
|
+
customInstructions = ''
|
121
|
+
} = options;
|
122
|
+
|
123
|
+
// 预先计算测试文件清单(用于 JSON manifest 与展示)
|
124
|
+
const files = targets.map(t => {
|
125
|
+
const testPath = t.path.replace(/\.(ts|tsx|js|jsx)$/i, m => `.test${m}`)
|
126
|
+
return {
|
127
|
+
path: testPath,
|
128
|
+
source: t.path,
|
129
|
+
name: t.name,
|
130
|
+
type: t.type,
|
131
|
+
layer: t.layer,
|
132
|
+
priority: t.priority,
|
133
|
+
score: t.score
|
134
|
+
}
|
135
|
+
})
|
136
|
+
|
137
|
+
let prompt = `# 批量生成单元测试
|
138
|
+
|
139
|
+
你是一个单元测试专家。我需要为以下 ${targets.length} 个函数生成单元测试。
|
140
|
+
|
141
|
+
## 项目信息
|
142
|
+
- 框架:${framework}
|
143
|
+
- 测试框架:${testFramework} + Testing Library
|
144
|
+
- 要求:每个函数覆盖率 >= ${coverageTarget}%
|
145
|
+
|
146
|
+
## 测试要求
|
147
|
+
1. 使用 ${testFramework} 的标准语法 (describe、test/it、expect)
|
148
|
+
2. 覆盖正常情况、边界条件、异常情况
|
149
|
+
3. 对于 React Hooks,使用 @testing-library/react-hooks
|
150
|
+
4. 对于 React 组件,使用 @testing-library/react
|
151
|
+
5. 对于工具函数,直接测试输入输出
|
152
|
+
6. 必要时使用 mock 模拟依赖
|
153
|
+
7. 测试文件命名:与原文件同名,加 .test 后缀
|
154
|
+
8. 严禁修改被测源码与新增依赖;避免使用真实时间/网络/随机数(请使用 fake timers、模块化 mock)
|
155
|
+
${customInstructions ? `\n${customInstructions}\n` : ''}
|
156
|
+
---
|
157
|
+
|
158
|
+
`;
|
159
|
+
|
160
|
+
// 严格输出协议:先输出 JSON manifest,再输出逐文件代码块
|
161
|
+
prompt += `## 测试文件清单(JSON Manifest)
|
162
|
+
请首先输出一个 JSON,包含将要生成的所有测试文件路径与源信息:
|
163
|
+
|
164
|
+
\`\`\`json
|
165
|
+
${JSON.stringify({ version: 1, files }, null, 2)}
|
166
|
+
\`\`\`
|
167
|
+
|
168
|
+
---
|
169
|
+
`;
|
170
|
+
|
171
|
+
targets.forEach((target, index) => {
|
172
|
+
const code = extractFunctionCode(target.path, target.name);
|
173
|
+
const testPath = target.path.replace(/\.(ts|tsx|js|jsx)$/i, m => `.test${m}`)
|
174
|
+
|
175
|
+
prompt += `
|
176
|
+
## 测试 ${index + 1}/${targets.length}: ${target.name}
|
177
|
+
|
178
|
+
**文件路径**: \`${target.path}\`
|
179
|
+
**函数类型**: ${target.type}
|
180
|
+
**所属层级**: ${target.layer}
|
181
|
+
**优先级**: ${target.priority} (分数: ${target.score})
|
182
|
+
|
183
|
+
**函数源码**:
|
184
|
+
\`\`\`typescript
|
185
|
+
${code}
|
186
|
+
\`\`\`
|
187
|
+
|
188
|
+
**测试文件路径**: \`${testPath}\`
|
189
|
+
|
190
|
+
---
|
191
|
+
`;
|
192
|
+
});
|
193
|
+
|
194
|
+
prompt += `
|
195
|
+
## 输出格式
|
196
|
+
|
197
|
+
请严格按照以下顺序与格式输出:
|
198
|
+
|
199
|
+
1) 先输出“测试文件清单(JSON Manifest)”——与上文格式一致,包含所有 \"path\"。
|
200
|
+
2) 然后依次输出每个测试文件的代码块:
|
201
|
+
|
202
|
+
### 测试文件: [文件路径]
|
203
|
+
\`\`\`typescript
|
204
|
+
[完整的测试代码]
|
205
|
+
\`\`\`
|
206
|
+
|
207
|
+
### 测试文件: [下一个文件路径]
|
208
|
+
...
|
209
|
+
|
210
|
+
要求:
|
211
|
+
- 代码块语言可为 ts/tsx/typescript/js/javascript/jsx 之一
|
212
|
+
- 文件路径必须与 JSON Manifest 中的 path 一致
|
213
|
+
- 不要省略任何测试文件
|
214
|
+
|
215
|
+
---
|
216
|
+
|
217
|
+
现在开始生成 ${targets.length} 个测试文件:
|
218
|
+
`;
|
219
|
+
|
220
|
+
return prompt;
|
221
|
+
}
|
222
|
+
|
223
|
+
/**
|
224
|
+
* CLI 入口
|
225
|
+
*/
|
226
|
+
export function runCLI(argv = process.argv) {
|
227
|
+
const args = argv.slice(2);
|
228
|
+
const filter = {};
|
229
|
+
const options = {};
|
230
|
+
|
231
|
+
// 解析参数
|
232
|
+
for (let i = 0; i < args.length; i++) {
|
233
|
+
const arg = args[i];
|
234
|
+
if (arg === '--priority' || arg === '-p') {
|
235
|
+
filter.priority = args[++i];
|
236
|
+
} else if (arg === '--layer' || arg === '-l') {
|
237
|
+
filter.layer = args[++i];
|
238
|
+
} else if (arg === '--limit' || arg === '-n') {
|
239
|
+
filter.limit = parseInt(args[++i]);
|
240
|
+
} else if (arg === '--skip') {
|
241
|
+
filter.skip = parseInt(args[++i]);
|
242
|
+
} else if (arg === '--min-score') {
|
243
|
+
filter.minScore = parseFloat(args[++i]);
|
244
|
+
} else if (arg === '--report') {
|
245
|
+
options.reportPath = args[++i];
|
246
|
+
} else if (arg === '--framework') {
|
247
|
+
options.framework = args[++i];
|
248
|
+
} else if (arg === '--hints') {
|
249
|
+
options.customInstructions = (options.customInstructions || '') + `\n${args[++i]}`;
|
250
|
+
} else if (arg === '--hints-file') {
|
251
|
+
const p = args[++i];
|
252
|
+
try { options.customInstructions = (options.customInstructions || '') + `\n${readFileSync(p, 'utf8')}` } catch {}
|
253
|
+
} else if (arg === '--only-paths') {
|
254
|
+
const csv = args[++i] || '';
|
255
|
+
filter.onlyPaths = csv.split(',').map(s => s.trim()).filter(Boolean);
|
256
|
+
}
|
257
|
+
}
|
258
|
+
|
259
|
+
const mdPath = options.reportPath || 'reports/ut_scores.md';
|
260
|
+
|
261
|
+
try {
|
262
|
+
let targets = parseTargets(mdPath, filter);
|
263
|
+
|
264
|
+
// 支持跳过前 N 个(用于分页)
|
265
|
+
const skip = Number.isInteger(filter.skip) && filter.skip > 0 ? filter.skip : 0;
|
266
|
+
if (skip) targets = targets.slice(skip);
|
267
|
+
|
268
|
+
if (filter.limit) targets = targets.slice(0, filter.limit);
|
269
|
+
|
270
|
+
if (targets.length === 0) {
|
271
|
+
console.error('❌ 没有找到匹配的目标\n');
|
272
|
+
console.error('用法示例:');
|
273
|
+
console.error(' ai-test gen-prompt -p P0 -l Foundation -n 5');
|
274
|
+
console.error(' ai-test gen-prompt -p P0 --min-score 7.5 -n 10\n');
|
275
|
+
console.error('参数:');
|
276
|
+
console.error(' -p, --priority 优先级过滤 (P0, P1, P2, P3)');
|
277
|
+
console.error(' -l, --layer 层级过滤 (Foundation, Business, State, UI)');
|
278
|
+
console.error(' -n, --limit 限制数量');
|
279
|
+
console.error(' --skip 跳过前 N 个');
|
280
|
+
console.error(' --min-score 最低分数');
|
281
|
+
console.error(' --report 报告文件路径 (默认: reports/ut_scores.md)');
|
282
|
+
console.error(' --framework 项目框架 (默认: React + TypeScript)');
|
283
|
+
process.exit(1);
|
284
|
+
}
|
285
|
+
|
286
|
+
console.error(`✅ 找到 ${targets.length} 个目标\n`);
|
287
|
+
console.log(buildBatchPrompt(targets, options));
|
288
|
+
} catch (err) {
|
289
|
+
console.error(`❌ 错误: ${err.message}`);
|
290
|
+
process.exit(1);
|
291
|
+
}
|
292
|
+
}
|
293
|
+
|
294
|
+
// 作为脚本直接运行时
|
295
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
296
|
+
runCLI();
|
297
|
+
}
|
298
|
+
|