ai-git-tools 2.0.67 → 2.0.69

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.
@@ -1,165 +0,0 @@
1
- /**
2
- * TestGenerator 服務
3
- * 為指定的原始碼檔案使用 AIClient 生成 Jest 測試
4
- */
5
-
6
- import { readFileSync, writeFileSync, mkdirSync } from 'fs';
7
- import { dirname } from 'path';
8
- import { AIClient } from './ai-client.js';
9
-
10
- export class TestGenerator {
11
- /**
12
- * 為原始檔案生成測試並寫入測試檔案
13
- * @param {string} sourceFilePath - 原始碼路徑(絕對路徑)
14
- * @param {string} testFilePath - 測試檔案輸出路徑(絕對路徑)
15
- * @param {'auto'|'unit'|'component'} testType - 測試類型(預設 'auto')
16
- * @param {string} [model] - AI 模型(可選)
17
- * @returns {Promise<{ testFilePath, linesCount, testType: 'unit'|'component' }>}
18
- */
19
- static async generateTests(sourceFilePath, testFilePath, testType = 'auto', model) {
20
- let sourceCode;
21
- try {
22
- sourceCode = readFileSync(sourceFilePath, 'utf-8');
23
- } catch {
24
- throw new Error(`無法讀取原始碼:${sourceFilePath}`);
25
- }
26
-
27
- const resolvedTestType = TestGenerator.resolveTestType(sourceFilePath, sourceCode, testType);
28
- const prompt = TestGenerator._buildPrompt(sourceCode, sourceFilePath, resolvedTestType);
29
- const rawTests = await AIClient.sendAndWait(prompt, model);
30
- const testCode = TestGenerator._finalizeTestCode(
31
- TestGenerator._cleanCode(rawTests),
32
- resolvedTestType
33
- );
34
-
35
- // 確保目錄存在
36
- mkdirSync(dirname(testFilePath), { recursive: true });
37
- writeFileSync(testFilePath, testCode, 'utf-8');
38
-
39
- return {
40
- testFilePath,
41
- linesCount: testCode.split('\n').length,
42
- testType: resolvedTestType,
43
- };
44
- }
45
-
46
- /**
47
- * 解析最終測試類型
48
- * @param {string} sourceFilePath
49
- * @param {string} sourceCode
50
- * @param {'auto'|'unit'|'component'} requestedTestType
51
- * @returns {'unit'|'component'}
52
- */
53
- static resolveTestType(sourceFilePath, sourceCode, requestedTestType = 'auto') {
54
- if (requestedTestType === 'unit' || requestedTestType === 'component') {
55
- return requestedTestType;
56
- }
57
-
58
- const normalizedPath = sourceFilePath.toLowerCase();
59
- const looksLikeComponentPath = /\.(jsx|tsx)$/.test(normalizedPath)
60
- || /component|page|view|screen/.test(normalizedPath);
61
- const hasReactImport = /from\s+['"]react['"]|from\s+['"]react-dom['"]/.test(sourceCode);
62
- const hasTestingLibraryHint = /use(State|Effect|Memo|Callback|Reducer|Ref|Context)|React\./.test(sourceCode);
63
- const hasJsx = /<([A-Z][\w]*|[a-z][\w-]*)(\s[^>]*)?>/.test(sourceCode);
64
-
65
- return looksLikeComponentPath || hasReactImport || hasTestingLibraryHint || hasJsx
66
- ? 'component'
67
- : 'unit';
68
- }
69
-
70
- /**
71
- * 使用錯誤訊息重新生成修復後的原始碼
72
- * @param {string} sourceFilePath - 原始碼路徑
73
- * @param {string[]} testErrors - Jest 錯誤訊息陣列
74
- * @param {number} attempt - 第幾次修復嘗試(1 或 2)
75
- * @param {string} [model] - AI 模型(可選)
76
- * @returns {Promise<void>}
77
- */
78
- static async generateFix(sourceFilePath, testErrors, attempt, model) {
79
- let sourceCode;
80
- try {
81
- sourceCode = readFileSync(sourceFilePath, 'utf-8');
82
- } catch {
83
- throw new Error(`無法讀取原始碼:${sourceFilePath}`);
84
- }
85
-
86
- const prompt = TestGenerator._buildFixPrompt(sourceCode, testErrors, attempt);
87
- const rawFixed = await AIClient.sendAndWait(prompt, model);
88
- const fixedCode = TestGenerator._cleanCode(rawFixed);
89
-
90
- writeFileSync(sourceFilePath, fixedCode, 'utf-8');
91
- }
92
-
93
- /**
94
- * 建立測試生成 prompt
95
- */
96
- static _buildPrompt(sourceCode, sourceFilePath, testType) {
97
- return `你是一位 Jest 測試專家。請為以下 ${testType === 'component' ? 'React 元件' : '模組'} 撰寫全面的 ${testType} 測試。
98
-
99
- ## 原始碼(${sourceFilePath})
100
- \`\`\`
101
- ${sourceCode}
102
- \`\`\`
103
-
104
- ## 測試要求
105
- 1. 使用現代 Jest 語法(describe / it / expect)
106
- 2. 涵蓋所有主要函數及邊界情況
107
- 3. Mock 所有外部依賴(檔案系統、網路、子程序等)
108
- 4. 使用 beforeEach / afterEach 管理測試狀態
109
- 5. 測試名稱使用繁體中文描述
110
- ${testType === 'component'
111
- ? '6. 使用 @testing-library/react 撰寫互動與渲染測試\n7. 檔案最前面必須加上 /** @jest-environment jsdom */ 註解'
112
- : '6. 以純邏輯與副作用隔離為主,避免使用 DOM API'}
113
-
114
- ## 輸出規則
115
- - 只輸出可直接執行的測試程式碼
116
- - 不要 markdown 區塊(\`\`\`)、不要任何說明文字
117
- - 第一行必須是 /** @jest-environment jsdom */ 或 import / require 陳述式`.trim();
118
- }
119
-
120
- /**
121
- * 建立修復 prompt
122
- */
123
- static _buildFixPrompt(sourceCode, testErrors, attempt) {
124
- return `這是第 ${attempt} 次修復嘗試。以下是目前的原始碼和失敗的測試錯誤,請修正原始碼使測試通過。
125
-
126
- ## 當前原始碼
127
- \`\`\`
128
- ${sourceCode}
129
- \`\`\`
130
-
131
- ## 測試錯誤
132
- ${testErrors.join('\n')}
133
-
134
- ## 修復規則
135
- - 只修改原始碼,不修改測試
136
- - 保留原有功能,只修正造成測試失敗的部分
137
- - 只輸出修正後的完整原始碼,不要任何說明文字
138
- - 不要 markdown 區塊(\`\`\`)`.trim();
139
- }
140
-
141
- /**
142
- * 移除 AI 回傳中的 markdown code fence
143
- */
144
- static _cleanCode(raw) {
145
- return raw
146
- .replace(/^```[\w]*\n?/gm, '')
147
- .replace(/\n?```$/gm, '')
148
- .trim();
149
- }
150
-
151
- /**
152
- * 針對元件測試補上必要的 jsdom 設定
153
- */
154
- static _finalizeTestCode(testCode, testType) {
155
- if (testType !== 'component') {
156
- return testCode;
157
- }
158
-
159
- if (testCode.startsWith('/** @jest-environment jsdom */')) {
160
- return testCode;
161
- }
162
-
163
- return `/** @jest-environment jsdom */\n${testCode}`;
164
- }
165
- }
@@ -1,132 +0,0 @@
1
- /**
2
- * TestRunner 服務
3
- * 執行 Jest / bun test 測試、解析結果,並在失敗時觸發自動修復(最多 2 次)
4
- */
5
-
6
- import { execSync } from 'child_process';
7
- import { existsSync, readFileSync } from 'fs';
8
- import { resolve } from 'path';
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
-
44
- export class TestRunner {
45
- /**
46
- * 執行指定的測試檔案
47
- * @param {string|string[]} testFilePath - 測試檔案路徑(絕對路徑)
48
- * @returns {Promise<{ success: boolean, errors: string[] }>}
49
- */
50
- static async runTests(testFilePath) {
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}`] };
56
- }
57
-
58
- const runner = detectTestRunner();
59
-
60
- try {
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, {
74
- encoding: 'utf-8',
75
- stdio: 'pipe',
76
- timeout: 120000, // 2 分鐘超時
77
- cwd: process.cwd(),
78
- });
79
- return { success: true, errors: [], runner };
80
- } catch (error) {
81
- const output = (error.stdout || '') + (error.stderr || '');
82
- const errors = TestRunner._parseErrors(output);
83
- return { success: false, errors, runner };
84
- }
85
- }
86
-
87
- /**
88
- * 解析 Jest 輸出,提取失敗訊息
89
- * @param {string} output
90
- * @returns {string[]}
91
- */
92
- static _parseErrors(output) {
93
- const errors = [];
94
- const lines = output.split('\n');
95
-
96
- let inFailBlock = false;
97
- let currentError = [];
98
-
99
- for (const line of lines) {
100
- // 偵測失敗區塊的開始
101
- if (line.includes('● ') || line.includes('FAIL ')) {
102
- if (currentError.length > 0) {
103
- errors.push(currentError.join('\n').trim());
104
- currentError = [];
105
- }
106
- inFailBlock = true;
107
- }
108
-
109
- if (inFailBlock) {
110
- currentError.push(line);
111
-
112
- // 限制每個錯誤區塊最多 30 行,防止輸出過長
113
- if (currentError.length >= 30) {
114
- errors.push(currentError.join('\n').trim());
115
- currentError = [];
116
- inFailBlock = false;
117
- }
118
- }
119
- }
120
-
121
- if (currentError.length > 0) {
122
- errors.push(currentError.join('\n').trim());
123
- }
124
-
125
- // 若未能解析出具體錯誤,回傳原始輸出的前 50 行
126
- if (errors.length === 0) {
127
- errors.push(lines.slice(0, 50).join('\n'));
128
- }
129
-
130
- return errors;
131
- }
132
- }