ai-git-tools 2.0.61 → 2.0.63
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/bin/cli.js +4 -3
- package/package.json +1 -1
- package/src/commands/auto-dev.js +11 -95
- package/src/commands/generate-code.js +3 -3
- package/src/commands/write-and-test.js +93 -54
- package/src/core/test-runner.js +59 -9
package/bin/cli.js
CHANGED
|
@@ -142,7 +142,7 @@ program
|
|
|
142
142
|
.command('write-and-test')
|
|
143
143
|
.description('AI 為指定檔案生成 Jest 測試,並自動執行與修復(最多 2 次)')
|
|
144
144
|
.requiredOption('--file <path>', '原始碼路徑')
|
|
145
|
-
.option('--test-type <type>', '測試類型:auto、unit 或
|
|
145
|
+
.option('--test-type <type>', '測試類型:auto、unit、component 或 both(可用逗號指定多個)', 'auto')
|
|
146
146
|
.option('--max-fixes <number>', '最大自動修復次數(預設 2)', '2')
|
|
147
147
|
.option('--no-confirm', '跳過確認直接生成並執行測試')
|
|
148
148
|
.option('--model <model>', '指定 AI 模型')
|
|
@@ -158,11 +158,11 @@ program
|
|
|
158
158
|
// Auto Dev 整合命令
|
|
159
159
|
program
|
|
160
160
|
.command('auto-dev')
|
|
161
|
-
.description('一鍵自動化:從 GitHub Issue
|
|
161
|
+
.description('一鍵自動化:從 GitHub Issue 到代碼生成與測試修正')
|
|
162
162
|
.requiredOption('--issue <number>', 'GitHub Issue 編號')
|
|
163
163
|
.option('--file <path>', '目標檔案路徑(若無則自動推斷)')
|
|
164
164
|
.option('--context <description>', '額外說明或補充需求')
|
|
165
|
-
.option('--test-type <type>', '測試類型:auto、unit 或
|
|
165
|
+
.option('--test-type <type>', '測試類型:auto、unit、component 或 both(預設 auto)', 'auto')
|
|
166
166
|
.option('--max-lines <number>', '最大行數限制(預設 500)', '500')
|
|
167
167
|
.option('--max-fixes <number>', '最大自動修復次數(預設 2)', '2')
|
|
168
168
|
.option('--no-confirm', '全自動模式,跳過所有確認')
|
|
@@ -172,6 +172,7 @@ program
|
|
|
172
172
|
await autoDevCommand(options);
|
|
173
173
|
process.exit(0);
|
|
174
174
|
} catch (error) {
|
|
175
|
+
console.error(`\n[錯誤] ${error.message}`);
|
|
175
176
|
process.exit(1);
|
|
176
177
|
}
|
|
177
178
|
});
|
package/package.json
CHANGED
package/src/commands/auto-dev.js
CHANGED
|
@@ -5,15 +5,11 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { resolve } from 'path';
|
|
8
|
-
import { existsSync, readFileSync } from 'fs';
|
|
9
|
-
import { execSync } from 'child_process';
|
|
10
8
|
import inquirer from 'inquirer';
|
|
11
9
|
import { IssueReader } from '../core/issue-reader.js';
|
|
12
10
|
import { CodeGenerator } from '../core/code-generator.js';
|
|
13
11
|
import { AIClient } from '../core/ai-client.js';
|
|
14
|
-
import {
|
|
15
|
-
import { TestGenerator } from '../core/test-generator.js';
|
|
16
|
-
import { writeAndTestCommand, TEST_TYPE_CHOICES, normalizeTestType } from './write-and-test.js';
|
|
12
|
+
import { writeAndTestCommand, TEST_TYPE_CHOICES, normalizeTestTypeSelection } from './write-and-test.js';
|
|
17
13
|
import { Logger } from '../utils/logger.js';
|
|
18
14
|
|
|
19
15
|
export async function autoDevCommand(options = {}) {
|
|
@@ -125,11 +121,6 @@ export async function autoDevCommand(options = {}) {
|
|
|
125
121
|
requestedTestType: options.testType,
|
|
126
122
|
});
|
|
127
123
|
|
|
128
|
-
const resolvedRuntimeTestType = resolveRuntimeTestType(absoluteFilePath, selectedTestType);
|
|
129
|
-
|
|
130
|
-
logger.step('正在驗證專案可用的測試腳本...');
|
|
131
|
-
ensureJestAvailable(resolvedRuntimeTestType);
|
|
132
|
-
logger.success('已確認可執行 Jest 測試。');
|
|
133
124
|
console.log('');
|
|
134
125
|
|
|
135
126
|
// ============================================================
|
|
@@ -149,25 +140,15 @@ export async function autoDevCommand(options = {}) {
|
|
|
149
140
|
return;
|
|
150
141
|
}
|
|
151
142
|
|
|
152
|
-
// ============================================================
|
|
153
|
-
// 第七步:自動 Commit
|
|
154
|
-
// ============================================================
|
|
155
|
-
logger.step('正在自動提交...');
|
|
156
|
-
try {
|
|
157
|
-
GitOperations.exec(`git add "${absoluteFilePath}" "${testResult.testFilePath}"`, { silent: true });
|
|
158
|
-
const commitMsg = `feat(auto-dev): Issue #${issue.number} - ${issue.title}\n\n生成檔案:\n- ${filePath}\n- ${testResult.testFilePath.replace(process.cwd(), '.')}`;
|
|
159
|
-
GitOperations.exec(`git commit -m "${commitMsg.split('\n')[0]}"`, { silent: true });
|
|
160
|
-
logger.success('已自動提交');
|
|
161
|
-
} catch {
|
|
162
|
-
logger.warning('自動 commit 失敗(可能已有相同 commit 或尚未 staged)');
|
|
163
|
-
}
|
|
164
|
-
|
|
165
143
|
console.log('');
|
|
166
144
|
logger.section('✅ 自動化工作流完成');
|
|
167
145
|
console.log(`Issue #${issue.number}:${issue.title}`);
|
|
168
146
|
console.log(`生成檔案:${filePath}`);
|
|
169
|
-
|
|
170
|
-
|
|
147
|
+
testResult.testFilePaths.forEach((testFilePath) => {
|
|
148
|
+
console.log(`測試檔案:${testFilePath.replace(process.cwd(), '.')}`);
|
|
149
|
+
});
|
|
150
|
+
console.log(`測試類型:${testResult.testTypes.join('、')}`);
|
|
151
|
+
console.log('狀態:已完成代碼與測試流程,未自動 commit');
|
|
171
152
|
console.log(`合計行數:${codeResult.linesCount + ' (程式碼)'}`);
|
|
172
153
|
}
|
|
173
154
|
|
|
@@ -257,8 +238,10 @@ function getLanguageFromFilePath(filePath) {
|
|
|
257
238
|
}
|
|
258
239
|
|
|
259
240
|
async function resolveTestTypeForAutoDev({ noConfirm, requestedTestType }) {
|
|
241
|
+
const defaultTestType = normalizeTestTypeSelection(requestedTestType || 'auto')[0] || 'auto';
|
|
242
|
+
|
|
260
243
|
if (noConfirm) {
|
|
261
|
-
return
|
|
244
|
+
return requestedTestType || 'auto';
|
|
262
245
|
}
|
|
263
246
|
|
|
264
247
|
const { testType } = await inquirer.prompt([{
|
|
@@ -266,75 +249,8 @@ async function resolveTestTypeForAutoDev({ noConfirm, requestedTestType }) {
|
|
|
266
249
|
name: 'testType',
|
|
267
250
|
message: '請選擇這次要執行的測試方式:',
|
|
268
251
|
choices: TEST_TYPE_CHOICES,
|
|
269
|
-
default:
|
|
252
|
+
default: defaultTestType === 'auto' ? 'both' : defaultTestType,
|
|
270
253
|
}]);
|
|
271
254
|
|
|
272
|
-
return
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
function resolveRuntimeTestType(sourceFilePath, requestedTestType) {
|
|
276
|
-
if (requestedTestType !== 'auto') {
|
|
277
|
-
return requestedTestType;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
const sourceCode = readFileSync(sourceFilePath, 'utf-8');
|
|
281
|
-
return TestGenerator.resolveTestType(sourceFilePath, sourceCode, 'auto');
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
function ensureJestAvailable(runtimeTestType = 'unit') {
|
|
285
|
-
const packageJson = readProjectPackageJson();
|
|
286
|
-
|
|
287
|
-
try {
|
|
288
|
-
execSync('npx jest --version', {
|
|
289
|
-
encoding: 'utf-8',
|
|
290
|
-
stdio: 'pipe',
|
|
291
|
-
cwd: process.cwd(),
|
|
292
|
-
});
|
|
293
|
-
ensureComponentTestDependencies(packageJson, runtimeTestType);
|
|
294
|
-
return;
|
|
295
|
-
} catch {
|
|
296
|
-
const hasJestDependency = Boolean(
|
|
297
|
-
packageJson.dependencies?.jest || packageJson.devDependencies?.jest
|
|
298
|
-
);
|
|
299
|
-
|
|
300
|
-
if (!hasJestDependency) {
|
|
301
|
-
throw new Error('目前專案未安裝 Jest,請先安裝後再執行 auto-dev。');
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
ensureComponentTestDependencies(packageJson, runtimeTestType);
|
|
305
|
-
|
|
306
|
-
throw new Error('偵測到 Jest 依賴,但無法執行 npx jest,請檢查安裝或 lockfile 狀態。');
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
function readProjectPackageJson() {
|
|
311
|
-
const packageJsonPath = resolve(process.cwd(), 'package.json');
|
|
312
|
-
if (!existsSync(packageJsonPath)) {
|
|
313
|
-
throw new Error('目前目錄沒有 package.json,無法執行 Jest 測試。');
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
try {
|
|
317
|
-
return JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
|
|
318
|
-
} catch {
|
|
319
|
-
throw new Error('無法讀取目前專案的 package.json,請確認 Jest 已安裝。');
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
function ensureComponentTestDependencies(packageJson, runtimeTestType) {
|
|
324
|
-
if (runtimeTestType !== 'component') {
|
|
325
|
-
return;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
const hasJsdomDependency = Boolean(
|
|
329
|
-
packageJson.devDependencies?.['jest-environment-jsdom']
|
|
330
|
-
|| packageJson.dependencies?.['jest-environment-jsdom']
|
|
331
|
-
);
|
|
332
|
-
const hasTestingLibraryDependency = Boolean(
|
|
333
|
-
packageJson.devDependencies?.['@testing-library/react']
|
|
334
|
-
|| packageJson.dependencies?.['@testing-library/react']
|
|
335
|
-
);
|
|
336
|
-
|
|
337
|
-
if (!hasJsdomDependency || !hasTestingLibraryDependency) {
|
|
338
|
-
throw new Error('元件測試需要 jest-environment-jsdom 與 @testing-library/react,請先安裝後再執行 auto-dev。');
|
|
339
|
-
}
|
|
255
|
+
return testType;
|
|
340
256
|
}
|
|
@@ -8,7 +8,7 @@ import { resolve } from 'path';
|
|
|
8
8
|
import inquirer from 'inquirer';
|
|
9
9
|
import { IssueReader } from '../core/issue-reader.js';
|
|
10
10
|
import { CodeGenerator } from '../core/code-generator.js';
|
|
11
|
-
import { writeAndTestCommand, TEST_TYPE_CHOICES } from './write-and-test.js';
|
|
11
|
+
import { writeAndTestCommand, TEST_TYPE_CHOICES, normalizeTestTypeSelection } from './write-and-test.js';
|
|
12
12
|
import { Logger } from '../utils/logger.js';
|
|
13
13
|
|
|
14
14
|
export async function generateCodeCommand(options = {}) {
|
|
@@ -97,13 +97,13 @@ export async function generateCodeCommand(options = {}) {
|
|
|
97
97
|
value: 'skip',
|
|
98
98
|
},
|
|
99
99
|
],
|
|
100
|
-
default: '
|
|
100
|
+
default: 'both',
|
|
101
101
|
}]);
|
|
102
102
|
|
|
103
103
|
if (nextAction !== 'skip') {
|
|
104
104
|
await writeAndTestCommand({
|
|
105
105
|
file: filePath,
|
|
106
|
-
testType: nextAction,
|
|
106
|
+
testType: normalizeTestTypeSelection(nextAction).join(','),
|
|
107
107
|
maxFixes: options.maxFixes || '2',
|
|
108
108
|
model: options.model,
|
|
109
109
|
noConfirm: true,
|
|
@@ -4,8 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { resolve, basename, dirname, join } from 'path';
|
|
7
|
-
import { existsSync } from 'fs';
|
|
8
|
-
import { execSync } from 'child_process';
|
|
7
|
+
import { existsSync, readFileSync } from 'fs';
|
|
9
8
|
import inquirer from 'inquirer';
|
|
10
9
|
import { TestGenerator } from '../core/test-generator.js';
|
|
11
10
|
import { TestRunner } from '../core/test-runner.js';
|
|
@@ -24,31 +23,75 @@ export const TEST_TYPE_CHOICES = [
|
|
|
24
23
|
name: '寫元件測試',
|
|
25
24
|
value: 'component',
|
|
26
25
|
},
|
|
26
|
+
{
|
|
27
|
+
name: '同時寫 Jest 單元測試 + 元件測試',
|
|
28
|
+
value: 'both',
|
|
29
|
+
},
|
|
27
30
|
];
|
|
28
31
|
|
|
29
|
-
export function
|
|
30
|
-
const
|
|
32
|
+
export function normalizeTestTypeSelection(testType = 'auto') {
|
|
33
|
+
const rawValues = Array.isArray(testType)
|
|
34
|
+
? testType
|
|
35
|
+
: String(testType)
|
|
36
|
+
.split(',')
|
|
37
|
+
.map((value) => value.trim())
|
|
38
|
+
.filter(Boolean);
|
|
31
39
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
40
|
+
const normalizedValues = rawValues.length > 0 ? rawValues : ['auto'];
|
|
41
|
+
const resolvedValues = normalizedValues.flatMap((value) => {
|
|
42
|
+
const normalized = value.toLowerCase();
|
|
35
43
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
44
|
+
if (normalized === 'auto' || normalized === 'unit' || normalized === 'component') {
|
|
45
|
+
return normalized;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (normalized === 'both' || normalized === 'all') {
|
|
49
|
+
return ['unit', 'component'];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (normalized === 'jest' || normalized === 'jest-unit' || normalized === 'unit-test') {
|
|
53
|
+
return 'unit';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (normalized === 'component-test' || normalized === 'react-component') {
|
|
57
|
+
return 'component';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
throw new Error(`不支援的測試類型:${value}`);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
return [...new Set(resolvedValues)];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function resolveRequestedTestTypes(sourceFilePath, requestedTestType = 'auto') {
|
|
67
|
+
const normalizedTestTypes = normalizeTestTypeSelection(requestedTestType);
|
|
39
68
|
|
|
40
|
-
if (
|
|
41
|
-
return
|
|
69
|
+
if (!normalizedTestTypes.includes('auto')) {
|
|
70
|
+
return normalizedTestTypes;
|
|
42
71
|
}
|
|
43
72
|
|
|
44
|
-
|
|
73
|
+
const sourceCode = readFileSync(sourceFilePath, 'utf-8');
|
|
74
|
+
const inferredTestType = TestGenerator.resolveTestType(sourceFilePath, sourceCode, 'auto');
|
|
75
|
+
|
|
76
|
+
return [...new Set(
|
|
77
|
+
normalizedTestTypes.flatMap((testType) => (testType === 'auto' ? inferredTestType : testType))
|
|
78
|
+
)];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function formatTestTypes(testTypes) {
|
|
82
|
+
const labels = {
|
|
83
|
+
auto: '自動判斷',
|
|
84
|
+
unit: 'Jest 單元測試',
|
|
85
|
+
component: '元件測試',
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
return testTypes.map((testType) => labels[testType] || testType).join('、');
|
|
45
89
|
}
|
|
46
90
|
|
|
47
91
|
export async function writeAndTestCommand(options = {}) {
|
|
48
92
|
const logger = new Logger();
|
|
49
93
|
|
|
50
94
|
const filePath = options.file;
|
|
51
|
-
const requestedTestType = normalizeTestType(options.testType || 'auto');
|
|
52
95
|
const maxFixes = parseInt(options.maxFixes || '2', 10);
|
|
53
96
|
|
|
54
97
|
if (!filePath) {
|
|
@@ -62,13 +105,18 @@ export async function writeAndTestCommand(options = {}) {
|
|
|
62
105
|
throw new Error(`找不到原始碼:${absoluteSourcePath}`);
|
|
63
106
|
}
|
|
64
107
|
|
|
65
|
-
|
|
66
|
-
const
|
|
108
|
+
const resolvedTestTypes = resolveRequestedTestTypes(absoluteSourcePath, options.testType || 'auto');
|
|
109
|
+
const testTargets = resolvedTestTypes.map((testType) => ({
|
|
110
|
+
testType,
|
|
111
|
+
testFilePath: deriveTestFilePath(absoluteSourcePath, testType),
|
|
112
|
+
}));
|
|
67
113
|
|
|
68
114
|
logger.header('write-and-test');
|
|
69
115
|
console.log(`原始碼 :${absoluteSourcePath}`);
|
|
70
|
-
console.log(
|
|
71
|
-
|
|
116
|
+
console.log(`測試類型:${formatTestTypes(resolvedTestTypes)}`);
|
|
117
|
+
testTargets.forEach(({ testType, testFilePath }) => {
|
|
118
|
+
console.log(`${testType === 'component' ? '元件測試' : '單元測試'}:${testFilePath}`);
|
|
119
|
+
});
|
|
72
120
|
console.log(`最大修復:${maxFixes} 次`);
|
|
73
121
|
console.log('');
|
|
74
122
|
|
|
@@ -77,7 +125,7 @@ export async function writeAndTestCommand(options = {}) {
|
|
|
77
125
|
const { confirmed } = await inquirer.prompt([{
|
|
78
126
|
type: 'confirm',
|
|
79
127
|
name: 'confirmed',
|
|
80
|
-
message: `確認為 ${filePath} 生成 ${
|
|
128
|
+
message: `確認為 ${filePath} 生成 ${formatTestTypes(resolvedTestTypes)} 並執行?`,
|
|
81
129
|
default: true,
|
|
82
130
|
}]);
|
|
83
131
|
|
|
@@ -89,14 +137,18 @@ export async function writeAndTestCommand(options = {}) {
|
|
|
89
137
|
|
|
90
138
|
// 2. 生成測試
|
|
91
139
|
logger.step('正在生成測試代碼...');
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
140
|
+
const generatedTests = [];
|
|
141
|
+
for (const { testType, testFilePath } of testTargets) {
|
|
142
|
+
const generatedTest = await TestGenerator.generateTests(
|
|
143
|
+
absoluteSourcePath,
|
|
144
|
+
testFilePath,
|
|
145
|
+
testType,
|
|
146
|
+
options.model
|
|
147
|
+
);
|
|
148
|
+
generatedTests.push(generatedTest);
|
|
149
|
+
logger.success(`測試已寫入:${testFilePath}`);
|
|
150
|
+
}
|
|
151
|
+
logger.info(`實際測試類型:${formatTestTypes(generatedTests.map(({ testType }) => testType))}`);
|
|
100
152
|
|
|
101
153
|
// 3. 執行測試 + 自動修復循環
|
|
102
154
|
let lastErrors = [];
|
|
@@ -108,16 +160,15 @@ export async function writeAndTestCommand(options = {}) {
|
|
|
108
160
|
}
|
|
109
161
|
|
|
110
162
|
logger.step(`正在執行測試${attempt > 0 ? `(第 ${attempt} 次修復後)` : ''}...`);
|
|
111
|
-
const result = await TestRunner.runTests(testFilePath);
|
|
163
|
+
const result = await TestRunner.runTests(generatedTests.map(({ testFilePath }) => testFilePath));
|
|
112
164
|
|
|
113
165
|
if (result.success) {
|
|
114
166
|
logger.success('所有測試通過! 🎉');
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
}
|
|
120
|
-
return { success: true, testFilePath, testType: generatedTest.testType };
|
|
167
|
+
return {
|
|
168
|
+
success: true,
|
|
169
|
+
testFilePaths: generatedTests.map(({ testFilePath }) => testFilePath),
|
|
170
|
+
testTypes: generatedTests.map(({ testType }) => testType),
|
|
171
|
+
};
|
|
121
172
|
}
|
|
122
173
|
|
|
123
174
|
lastErrors = result.errors;
|
|
@@ -139,12 +190,14 @@ export async function writeAndTestCommand(options = {}) {
|
|
|
139
190
|
});
|
|
140
191
|
console.log('');
|
|
141
192
|
console.log(`原始碼路徑:${absoluteSourcePath}`);
|
|
142
|
-
|
|
193
|
+
generatedTests.forEach(({ testFilePath }) => {
|
|
194
|
+
console.log(`測試路徑 :${testFilePath}`);
|
|
195
|
+
});
|
|
143
196
|
return {
|
|
144
197
|
success: false,
|
|
145
|
-
testFilePath,
|
|
198
|
+
testFilePaths: generatedTests.map(({ testFilePath }) => testFilePath),
|
|
146
199
|
errors: lastErrors,
|
|
147
|
-
|
|
200
|
+
testTypes: generatedTests.map(({ testType }) => testType),
|
|
148
201
|
};
|
|
149
202
|
}
|
|
150
203
|
}
|
|
@@ -152,25 +205,11 @@ export async function writeAndTestCommand(options = {}) {
|
|
|
152
205
|
|
|
153
206
|
/**
|
|
154
207
|
* 依據原始檔案路徑推導測試檔案路徑
|
|
155
|
-
* e.g. src/core/foo.js → src/core/__tests__/foo.test.js
|
|
156
208
|
*/
|
|
157
|
-
export function deriveTestFilePath(sourceFilePath) {
|
|
209
|
+
export function deriveTestFilePath(sourceFilePath, testType = 'unit') {
|
|
158
210
|
const dir = dirname(sourceFilePath);
|
|
159
211
|
const name = basename(sourceFilePath).replace(/\.(js|jsx|ts|tsx|mjs|cjs)$/, '');
|
|
160
212
|
const ext = /\.(ts|tsx)$/.test(sourceFilePath) ? '.ts' : '.js';
|
|
161
|
-
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* 將原始碼與測試檔案一併 commit 到 git
|
|
166
|
-
*/
|
|
167
|
-
async function commitGeneratedFiles(testFilePath, sourceFilePath, logger) {
|
|
168
|
-
try {
|
|
169
|
-
execSync(`git add "${sourceFilePath}" "${testFilePath}"`, { stdio: 'pipe' });
|
|
170
|
-
const name = basename(sourceFilePath);
|
|
171
|
-
execSync(`git commit -m "test: 為 ${name} 新增自動生成的測試"`, { stdio: 'pipe' });
|
|
172
|
-
logger.success('原始碼與測試檔案已自動 commit。');
|
|
173
|
-
} catch {
|
|
174
|
-
logger.warning('無法自動 commit 產生的檔案(可能沒有 staged 變更或不在 git 倉庫中)。');
|
|
175
|
-
}
|
|
213
|
+
const suffix = testType === 'component' ? 'component' : 'unit';
|
|
214
|
+
return join(dir, '__tests__', `${name}.${suffix}.test${ext}`);
|
|
176
215
|
}
|
package/src/core/test-runner.js
CHANGED
|
@@ -1,36 +1,86 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* TestRunner 服務
|
|
3
|
-
* 執行 Jest 測試、解析結果,並在失敗時觸發自動修復(最多 2 次)
|
|
3
|
+
* 執行 Jest / bun test 測試、解析結果,並在失敗時觸發自動修復(最多 2 次)
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { execSync } from 'child_process';
|
|
7
|
-
import { existsSync } from 'fs';
|
|
7
|
+
import { existsSync, readFileSync } from 'fs';
|
|
8
8
|
import { resolve } from 'path';
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* 偵測目前專案使用的測試執行器
|
|
12
|
+
* 優先順序:bun test > vitest > jest
|
|
13
|
+
*/
|
|
14
|
+
function detectTestRunner() {
|
|
15
|
+
const packageJsonPath = resolve(process.cwd(), 'package.json');
|
|
16
|
+
if (!existsSync(packageJsonPath)) {
|
|
17
|
+
return 'jest';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
|
|
22
|
+
const allDeps = {
|
|
23
|
+
...pkg.dependencies,
|
|
24
|
+
...pkg.devDependencies,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// 偵測 bun:test 腳本含 bun test,或安裝了 bun
|
|
28
|
+
const testScript = pkg.scripts?.test || '';
|
|
29
|
+
if (testScript.includes('bun test')) {
|
|
30
|
+
return 'bun';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// 偵測 vitest
|
|
34
|
+
if (allDeps.vitest) {
|
|
35
|
+
return 'vitest';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return 'jest';
|
|
39
|
+
} catch {
|
|
40
|
+
return 'jest';
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
10
44
|
export class TestRunner {
|
|
11
45
|
/**
|
|
12
46
|
* 執行指定的測試檔案
|
|
13
|
-
* @param {string} testFilePath - 測試檔案路徑(絕對路徑)
|
|
47
|
+
* @param {string|string[]} testFilePath - 測試檔案路徑(絕對路徑)
|
|
14
48
|
* @returns {Promise<{ success: boolean, errors: string[] }>}
|
|
15
49
|
*/
|
|
16
50
|
static async runTests(testFilePath) {
|
|
17
|
-
|
|
18
|
-
|
|
51
|
+
const testFilePaths = Array.isArray(testFilePath) ? testFilePath : [testFilePath];
|
|
52
|
+
const missingTestFilePath = testFilePaths.find((filePath) => !existsSync(filePath));
|
|
53
|
+
|
|
54
|
+
if (missingTestFilePath) {
|
|
55
|
+
return { success: false, errors: [`測試檔案不存在:${missingTestFilePath}`] };
|
|
19
56
|
}
|
|
20
57
|
|
|
58
|
+
const runner = detectTestRunner();
|
|
59
|
+
|
|
21
60
|
try {
|
|
22
|
-
const
|
|
23
|
-
|
|
61
|
+
const absoluteTestFilePaths = testFilePaths.map((filePath) => `"${resolve(filePath)}"`);
|
|
62
|
+
|
|
63
|
+
let cmd;
|
|
64
|
+
if (runner === 'bun') {
|
|
65
|
+
// bun test 接受多個檔案作為參數
|
|
66
|
+
cmd = `bun test ${absoluteTestFilePaths.join(' ')}`;
|
|
67
|
+
} else if (runner === 'vitest') {
|
|
68
|
+
cmd = `npx vitest run ${absoluteTestFilePaths.join(' ')}`;
|
|
69
|
+
} else {
|
|
70
|
+
cmd = `npx jest --runInBand --runTestsByPath ${absoluteTestFilePaths.join(' ')} --no-coverage`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
execSync(cmd, {
|
|
24
74
|
encoding: 'utf-8',
|
|
25
75
|
stdio: 'pipe',
|
|
26
76
|
timeout: 120000, // 2 分鐘超時
|
|
27
77
|
cwd: process.cwd(),
|
|
28
78
|
});
|
|
29
|
-
return { success: true, errors: [] };
|
|
79
|
+
return { success: true, errors: [], runner };
|
|
30
80
|
} catch (error) {
|
|
31
81
|
const output = (error.stdout || '') + (error.stderr || '');
|
|
32
82
|
const errors = TestRunner._parseErrors(output);
|
|
33
|
-
return { success: false, errors };
|
|
83
|
+
return { success: false, errors, runner };
|
|
34
84
|
}
|
|
35
85
|
}
|
|
36
86
|
|