ai-git-tools 2.0.66 → 2.0.67
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 +1 -1
- package/src/commands/write-and-test.js +57 -60
package/package.json
CHANGED
|
@@ -7,7 +7,6 @@ import { resolve, basename, dirname, join } from 'path';
|
|
|
7
7
|
import { existsSync, readFileSync, writeFileSync, unlinkSync } from 'fs';
|
|
8
8
|
import { execSync } from 'child_process';
|
|
9
9
|
import { tmpdir } from 'os';
|
|
10
|
-
import inquirer from 'inquirer';
|
|
11
10
|
import { TestGenerator } from '../core/test-generator.js';
|
|
12
11
|
import { TestRunner } from '../core/test-runner.js';
|
|
13
12
|
import { AIClient } from '../core/ai-client.js';
|
|
@@ -108,69 +107,72 @@ export async function writeAndTestCommand(options = {}) {
|
|
|
108
107
|
|
|
109
108
|
// 自動從 state 檔讀取 file / issue(指令列將覆蓋)
|
|
110
109
|
const state = loadDevState();
|
|
111
|
-
let filePath = options.file || state?.filePath;
|
|
112
110
|
const issueNumber = options.issue || state?.issueNumber;
|
|
113
|
-
const fromState = !options.file && Boolean(state?.filePath);
|
|
114
111
|
const maxFixes = parseInt(options.maxFixes || '2', 10);
|
|
115
112
|
|
|
116
|
-
//
|
|
113
|
+
// 決定要處理的檔案清單
|
|
114
|
+
let sourceFiles = [];
|
|
115
|
+
let fromState = false;
|
|
117
116
|
let fromGit = false;
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
117
|
+
|
|
118
|
+
if (options.file) {
|
|
119
|
+
// 明確指定單一檔案
|
|
120
|
+
sourceFiles = [options.file];
|
|
121
|
+
} else if (state?.filePath) {
|
|
122
|
+
// 從上一步 dev-from-issue 讀取
|
|
123
|
+
sourceFiles = [state.filePath];
|
|
124
|
+
fromState = true;
|
|
125
|
+
} else {
|
|
126
|
+
// 從 git 偵測所有異動的原始碼檔案
|
|
127
|
+
sourceFiles = await detectChangedSourceFiles(logger);
|
|
128
|
+
fromGit = sourceFiles.length > 0;
|
|
121
129
|
}
|
|
122
130
|
|
|
123
|
-
if (
|
|
131
|
+
if (sourceFiles.length === 0) {
|
|
124
132
|
logger.error('偵測不到異動的原始碼,請用 --file 指定目標檔案。');
|
|
125
133
|
throw new Error('缺少原始碼路徑');
|
|
126
134
|
}
|
|
127
135
|
|
|
136
|
+
logger.header('write-and-test');
|
|
137
|
+
if (fromState) {
|
|
138
|
+
logger.info('從上一步 dev-from-issue 自動讀取');
|
|
139
|
+
} else if (fromGit) {
|
|
140
|
+
logger.info(`從 git 異動記錄偵測到 ${sourceFiles.length} 個檔案`);
|
|
141
|
+
}
|
|
142
|
+
console.log('');
|
|
143
|
+
|
|
144
|
+
const allResults = [];
|
|
145
|
+
for (const filePath of sourceFiles) {
|
|
146
|
+
const result = await processOneFile({ filePath, issueNumber, maxFixes, fromState, fromGit, options, logger });
|
|
147
|
+
allResults.push(result);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return allResults.length === 1 ? allResults[0] : allResults;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function processOneFile({ filePath, issueNumber, maxFixes, fromState, fromGit, options, logger }) {
|
|
128
154
|
const absoluteSourcePath = resolve(process.cwd(), filePath);
|
|
129
155
|
if (!existsSync(absoluteSourcePath)) {
|
|
130
156
|
logger.error(`原始碼不存在:${absoluteSourcePath}`);
|
|
131
157
|
throw new Error(`找不到原始碼:${absoluteSourcePath}`);
|
|
132
158
|
}
|
|
133
159
|
|
|
134
|
-
//
|
|
160
|
+
// 自動偵測時預設 both,手動指定時依 --test-type
|
|
135
161
|
const resolvedTestTypes = resolveRequestedTestTypes(absoluteSourcePath, options.testType || (fromState || fromGit ? 'both' : 'auto'));
|
|
136
162
|
const testTargets = resolvedTestTypes.map((testType) => ({
|
|
137
163
|
testType,
|
|
138
164
|
testFilePath: deriveTestFilePath(absoluteSourcePath, testType),
|
|
139
165
|
}));
|
|
140
166
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
logger.info('從上一步 dev-from-issue 自動讀取');
|
|
144
|
-
} else if (fromGit) {
|
|
145
|
-
logger.info('從 git 異動記錄自動偵測');
|
|
146
|
-
}
|
|
147
|
-
console.log(`原始碼 :${absoluteSourcePath}`);
|
|
148
|
-
console.log(`測試類型:${formatTestTypes(resolvedTestTypes)}`);
|
|
167
|
+
console.log(`── ${filePath}`);
|
|
168
|
+
console.log(` 測試類型:${formatTestTypes(resolvedTestTypes)}`);
|
|
149
169
|
testTargets.forEach(({ testType, testFilePath }) => {
|
|
150
|
-
console.log(
|
|
170
|
+
console.log(` ${testType === 'component' ? '元件測試' : '單元測試'}:${testFilePath}`);
|
|
151
171
|
});
|
|
152
|
-
console.log(`最大修復:${maxFixes} 次`);
|
|
153
|
-
if (issueNumber) {
|
|
154
|
-
console.log(`報告 Issue:#${issueNumber}`);
|
|
155
|
-
}
|
|
156
172
|
console.log('');
|
|
157
173
|
|
|
158
|
-
// 1. 詢問是否繼續(自動從 state 讀取時跳過)
|
|
159
|
-
if (!options.noConfirm && !fromState) {
|
|
160
|
-
const { confirmed } = await inquirer.prompt([{
|
|
161
|
-
type: 'confirm',
|
|
162
|
-
name: 'confirmed',
|
|
163
|
-
message: `確認為 ${filePath} 生成 ${formatTestTypes(resolvedTestTypes)} 並執行?`,
|
|
164
|
-
default: true,
|
|
165
|
-
}]);
|
|
166
|
-
|
|
167
|
-
if (!confirmed) {
|
|
168
|
-
logger.info('已取消。');
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
174
|
|
|
173
|
-
//
|
|
175
|
+
// 1. 生成測試
|
|
174
176
|
logger.step('正在生成測試代碼...');
|
|
175
177
|
const generatedTests = [];
|
|
176
178
|
for (const { testType, testFilePath } of testTargets) {
|
|
@@ -185,7 +187,7 @@ export async function writeAndTestCommand(options = {}) {
|
|
|
185
187
|
}
|
|
186
188
|
logger.info(`實際測試類型:${formatTestTypes(generatedTests.map(({ testType }) => testType))}`);
|
|
187
189
|
|
|
188
|
-
//
|
|
190
|
+
// 2. 執行測試 + 自動修復循環
|
|
189
191
|
let lastErrors = [];
|
|
190
192
|
for (let attempt = 0; attempt <= maxFixes; attempt++) {
|
|
191
193
|
if (attempt > 0) {
|
|
@@ -366,16 +368,16 @@ export function deriveTestFilePath(sourceFilePath, testType = 'unit') {
|
|
|
366
368
|
}
|
|
367
369
|
|
|
368
370
|
/**
|
|
369
|
-
* 透過 git
|
|
370
|
-
* 優先順序:
|
|
371
|
+
* 透過 git 偵測所有異動的原始碼檔案(回傳陣列)
|
|
372
|
+
* 優先順序:staged → unstaged → untracked → 最新 commit
|
|
371
373
|
* 排除測試檔、設定檔、lock 檔
|
|
372
374
|
*/
|
|
373
|
-
async function
|
|
375
|
+
async function detectChangedSourceFiles(logger) {
|
|
374
376
|
const SOURCE_EXTENSIONS = /\.(js|jsx|ts|tsx|mjs|cjs)$/;
|
|
375
377
|
const EXCLUDED = /(__tests__|\.test\.|\.spec\.|\.config\.|node_modules|dist\/|\.lock$|package\.json)/;
|
|
376
378
|
|
|
377
|
-
function filterSourceFiles(
|
|
378
|
-
return
|
|
379
|
+
function filterSourceFiles(raw) {
|
|
380
|
+
return raw
|
|
379
381
|
.split('\n')
|
|
380
382
|
.map((f) => f.trim().replace(/^[MADRCU?!\s]+/, ''))
|
|
381
383
|
.filter((f) => f && SOURCE_EXTENSIONS.test(f) && !EXCLUDED.test(f) && existsSync(resolve(process.cwd(), f)));
|
|
@@ -387,39 +389,34 @@ async function detectChangedSourceFile(logger) {
|
|
|
387
389
|
execSync('git diff --cached --name-only', { encoding: 'utf-8', stdio: 'pipe', cwd: process.cwd() })
|
|
388
390
|
);
|
|
389
391
|
if (staged.length > 0) {
|
|
390
|
-
logger.info(`偵測到 staged
|
|
391
|
-
return staged
|
|
392
|
+
logger.info(`偵測到 staged 異動 ${staged.length} 個檔案`);
|
|
393
|
+
return staged;
|
|
392
394
|
}
|
|
393
395
|
|
|
394
|
-
// 2. unstaged
|
|
396
|
+
// 2. unstaged 工作區異動(含 untracked 合併)
|
|
395
397
|
const unstaged = filterSourceFiles(
|
|
396
398
|
execSync('git diff --name-only', { encoding: 'utf-8', stdio: 'pipe', cwd: process.cwd() })
|
|
397
399
|
);
|
|
398
|
-
if (unstaged.length > 0) {
|
|
399
|
-
logger.info(`偵測到工作區異動:${unstaged[0]}`);
|
|
400
|
-
return unstaged[0];
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
// 3. untracked 新增檔案
|
|
404
400
|
const untracked = filterSourceFiles(
|
|
405
401
|
execSync('git ls-files --others --exclude-standard', { encoding: 'utf-8', stdio: 'pipe', cwd: process.cwd() })
|
|
406
402
|
);
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
403
|
+
const working = [...new Set([...unstaged, ...untracked])];
|
|
404
|
+
if (working.length > 0) {
|
|
405
|
+
logger.info(`偵測到工作區異動 ${working.length} 個檔案`);
|
|
406
|
+
return working;
|
|
410
407
|
}
|
|
411
408
|
|
|
412
|
-
//
|
|
409
|
+
// 3. 最新 commit 的異動
|
|
413
410
|
const lastCommit = filterSourceFiles(
|
|
414
411
|
execSync('git diff-tree --no-commit-id -r --name-only HEAD', { encoding: 'utf-8', stdio: 'pipe', cwd: process.cwd() })
|
|
415
412
|
);
|
|
416
413
|
if (lastCommit.length > 0) {
|
|
417
|
-
logger.info(`偵測到最後 commit
|
|
418
|
-
return lastCommit
|
|
414
|
+
logger.info(`偵測到最後 commit 異動 ${lastCommit.length} 個檔案`);
|
|
415
|
+
return lastCommit;
|
|
419
416
|
}
|
|
420
417
|
} catch {
|
|
421
|
-
// git
|
|
418
|
+
// git 取得失敗,直接回傳空陣列
|
|
422
419
|
}
|
|
423
420
|
|
|
424
|
-
return
|
|
421
|
+
return [];
|
|
425
422
|
}
|