ai-git-tools 2.0.60 → 2.0.61
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/package.json
CHANGED
package/src/commands/auto-dev.js
CHANGED
|
@@ -12,7 +12,8 @@ import { IssueReader } from '../core/issue-reader.js';
|
|
|
12
12
|
import { CodeGenerator } from '../core/code-generator.js';
|
|
13
13
|
import { AIClient } from '../core/ai-client.js';
|
|
14
14
|
import { GitOperations } from '../core/git-operations.js';
|
|
15
|
-
import {
|
|
15
|
+
import { TestGenerator } from '../core/test-generator.js';
|
|
16
|
+
import { writeAndTestCommand, TEST_TYPE_CHOICES, normalizeTestType } from './write-and-test.js';
|
|
16
17
|
import { Logger } from '../utils/logger.js';
|
|
17
18
|
|
|
18
19
|
export async function autoDevCommand(options = {}) {
|
|
@@ -107,7 +108,7 @@ export async function autoDevCommand(options = {}) {
|
|
|
107
108
|
try {
|
|
108
109
|
codeResult = await CodeGenerator.generateFile(issue, absoluteFilePath, {
|
|
109
110
|
maxLines: parseInt(options.maxLines || '500', 10),
|
|
110
|
-
language: filePath
|
|
111
|
+
language: getLanguageFromFilePath(filePath),
|
|
111
112
|
extraContext: options.context || '',
|
|
112
113
|
model: options.model,
|
|
113
114
|
});
|
|
@@ -119,8 +120,15 @@ export async function autoDevCommand(options = {}) {
|
|
|
119
120
|
|
|
120
121
|
console.log('');
|
|
121
122
|
|
|
123
|
+
const selectedTestType = await resolveTestTypeForAutoDev({
|
|
124
|
+
noConfirm,
|
|
125
|
+
requestedTestType: options.testType,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const resolvedRuntimeTestType = resolveRuntimeTestType(absoluteFilePath, selectedTestType);
|
|
129
|
+
|
|
122
130
|
logger.step('正在驗證專案可用的測試腳本...');
|
|
123
|
-
ensureJestAvailable();
|
|
131
|
+
ensureJestAvailable(resolvedRuntimeTestType);
|
|
124
132
|
logger.success('已確認可執行 Jest 測試。');
|
|
125
133
|
console.log('');
|
|
126
134
|
|
|
@@ -129,7 +137,7 @@ export async function autoDevCommand(options = {}) {
|
|
|
129
137
|
// ============================================================
|
|
130
138
|
const testResult = await writeAndTestCommand({
|
|
131
139
|
file: filePath,
|
|
132
|
-
testType:
|
|
140
|
+
testType: selectedTestType,
|
|
133
141
|
maxFixes: options.maxFixes || '2',
|
|
134
142
|
model: options.model,
|
|
135
143
|
noConfirm: true,
|
|
@@ -244,26 +252,47 @@ function isValidTargetPath(value) {
|
|
|
244
252
|
&& !value.startsWith('/');
|
|
245
253
|
}
|
|
246
254
|
|
|
247
|
-
function
|
|
255
|
+
function getLanguageFromFilePath(filePath) {
|
|
256
|
+
return /\.(ts|tsx)$/.test(filePath) ? 'ts' : 'js';
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async function resolveTestTypeForAutoDev({ noConfirm, requestedTestType }) {
|
|
260
|
+
if (noConfirm) {
|
|
261
|
+
return normalizeTestType(requestedTestType || 'auto');
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const { testType } = await inquirer.prompt([{
|
|
265
|
+
type: 'list',
|
|
266
|
+
name: 'testType',
|
|
267
|
+
message: '請選擇這次要執行的測試方式:',
|
|
268
|
+
choices: TEST_TYPE_CHOICES,
|
|
269
|
+
default: normalizeTestType(requestedTestType || 'auto'),
|
|
270
|
+
}]);
|
|
271
|
+
|
|
272
|
+
return normalizeTestType(testType);
|
|
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
|
+
|
|
248
287
|
try {
|
|
249
288
|
execSync('npx jest --version', {
|
|
250
289
|
encoding: 'utf-8',
|
|
251
290
|
stdio: 'pipe',
|
|
252
291
|
cwd: process.cwd(),
|
|
253
292
|
});
|
|
293
|
+
ensureComponentTestDependencies(packageJson, runtimeTestType);
|
|
294
|
+
return;
|
|
254
295
|
} catch {
|
|
255
|
-
const packageJsonPath = resolve(process.cwd(), 'package.json');
|
|
256
|
-
if (!existsSync(packageJsonPath)) {
|
|
257
|
-
throw new Error('目前目錄沒有 package.json,無法執行 Jest 測試。');
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
let packageJson;
|
|
261
|
-
try {
|
|
262
|
-
packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
|
|
263
|
-
} catch {
|
|
264
|
-
throw new Error('無法讀取目前專案的 package.json,請確認 Jest 已安裝。');
|
|
265
|
-
}
|
|
266
|
-
|
|
267
296
|
const hasJestDependency = Boolean(
|
|
268
297
|
packageJson.dependencies?.jest || packageJson.devDependencies?.jest
|
|
269
298
|
);
|
|
@@ -272,6 +301,40 @@ function ensureJestAvailable() {
|
|
|
272
301
|
throw new Error('目前專案未安裝 Jest,請先安裝後再執行 auto-dev。');
|
|
273
302
|
}
|
|
274
303
|
|
|
304
|
+
ensureComponentTestDependencies(packageJson, runtimeTestType);
|
|
305
|
+
|
|
275
306
|
throw new Error('偵測到 Jest 依賴,但無法執行 npx jest,請檢查安裝或 lockfile 狀態。');
|
|
276
307
|
}
|
|
277
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
|
+
}
|
|
340
|
+
}
|
|
@@ -8,6 +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
12
|
import { Logger } from '../utils/logger.js';
|
|
12
13
|
|
|
13
14
|
export async function generateCodeCommand(options = {}) {
|
|
@@ -61,7 +62,7 @@ export async function generateCodeCommand(options = {}) {
|
|
|
61
62
|
|
|
62
63
|
const result = await CodeGenerator.generateFile(issue, absolutePath, {
|
|
63
64
|
maxLines,
|
|
64
|
-
language:
|
|
65
|
+
language: /\.(ts|tsx)$/.test(absolutePath) ? 'ts' : 'js',
|
|
65
66
|
extraContext,
|
|
66
67
|
model: options.model,
|
|
67
68
|
});
|
|
@@ -84,5 +85,31 @@ export async function generateCodeCommand(options = {}) {
|
|
|
84
85
|
console.log('\n💡 下一步:執行 write-and-test 為此檔案生成並執行測試');
|
|
85
86
|
console.log(` ai-git-tools write-and-test --file ${filePath}`);
|
|
86
87
|
|
|
88
|
+
if (!options.noConfirm) {
|
|
89
|
+
const { nextAction } = await inquirer.prompt([{
|
|
90
|
+
type: 'list',
|
|
91
|
+
name: 'nextAction',
|
|
92
|
+
message: '代碼已生成,接下來要如何處理測試?',
|
|
93
|
+
choices: [
|
|
94
|
+
...TEST_TYPE_CHOICES,
|
|
95
|
+
{
|
|
96
|
+
name: '先不執行測試',
|
|
97
|
+
value: 'skip',
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
default: 'auto',
|
|
101
|
+
}]);
|
|
102
|
+
|
|
103
|
+
if (nextAction !== 'skip') {
|
|
104
|
+
await writeAndTestCommand({
|
|
105
|
+
file: filePath,
|
|
106
|
+
testType: nextAction,
|
|
107
|
+
maxFixes: options.maxFixes || '2',
|
|
108
|
+
model: options.model,
|
|
109
|
+
noConfirm: true,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
87
114
|
return result;
|
|
88
115
|
}
|
|
@@ -11,11 +11,44 @@ import { TestGenerator } from '../core/test-generator.js';
|
|
|
11
11
|
import { TestRunner } from '../core/test-runner.js';
|
|
12
12
|
import { Logger } from '../utils/logger.js';
|
|
13
13
|
|
|
14
|
+
export const TEST_TYPE_CHOICES = [
|
|
15
|
+
{
|
|
16
|
+
name: '寫測試(自動判斷 Jest 單元測試 / 元件測試)',
|
|
17
|
+
value: 'auto',
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
name: '寫 Jest 單元測試',
|
|
21
|
+
value: 'unit',
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
name: '寫元件測試',
|
|
25
|
+
value: 'component',
|
|
26
|
+
},
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
export function normalizeTestType(testType = 'auto') {
|
|
30
|
+
const normalized = String(testType).trim().toLowerCase();
|
|
31
|
+
|
|
32
|
+
if (normalized === 'auto' || normalized === 'unit' || normalized === 'component') {
|
|
33
|
+
return normalized;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (normalized === 'jest' || normalized === 'jest-unit' || normalized === 'unit-test') {
|
|
37
|
+
return 'unit';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (normalized === 'component-test' || normalized === 'react-component') {
|
|
41
|
+
return 'component';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
throw new Error(`不支援的測試類型:${testType}`);
|
|
45
|
+
}
|
|
46
|
+
|
|
14
47
|
export async function writeAndTestCommand(options = {}) {
|
|
15
48
|
const logger = new Logger();
|
|
16
49
|
|
|
17
50
|
const filePath = options.file;
|
|
18
|
-
const requestedTestType = options.testType || 'auto';
|
|
51
|
+
const requestedTestType = normalizeTestType(options.testType || 'auto');
|
|
19
52
|
const maxFixes = parseInt(options.maxFixes || '2', 10);
|
|
20
53
|
|
|
21
54
|
if (!filePath) {
|
|
@@ -121,10 +154,10 @@ export async function writeAndTestCommand(options = {}) {
|
|
|
121
154
|
* 依據原始檔案路徑推導測試檔案路徑
|
|
122
155
|
* e.g. src/core/foo.js → src/core/__tests__/foo.test.js
|
|
123
156
|
*/
|
|
124
|
-
function deriveTestFilePath(sourceFilePath) {
|
|
157
|
+
export function deriveTestFilePath(sourceFilePath) {
|
|
125
158
|
const dir = dirname(sourceFilePath);
|
|
126
|
-
const name = basename(sourceFilePath).replace(/\.(js|ts|mjs|cjs)$/, '');
|
|
127
|
-
const ext =
|
|
159
|
+
const name = basename(sourceFilePath).replace(/\.(js|jsx|ts|tsx|mjs|cjs)$/, '');
|
|
160
|
+
const ext = /\.(ts|tsx)$/.test(sourceFilePath) ? '.ts' : '.js';
|
|
128
161
|
return join(dir, '__tests__', `${name}.test${ext}`);
|
|
129
162
|
}
|
|
130
163
|
|