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