ai-git-tools 2.0.47 → 2.0.49

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,74 +0,0 @@
1
- /**
2
- * 測試執行器基礎類別
3
- * 定義標準介面與 TestResults 結構
4
- */
5
-
6
- /**
7
- * @typedef {Object} TestCase
8
- * @property {string} name - 測試名稱
9
- * @property {string} suite - 所屬 suite(describe 區塊)
10
- * @property {boolean} passed
11
- * @property {number} duration - 毫秒
12
- * @property {string|null} error - 失敗時的錯誤訊息
13
- */
14
-
15
- /**
16
- * @typedef {Object} TestResults
17
- * @property {string} framework
18
- * @property {number} total
19
- * @property {number} passed
20
- * @property {number} failed
21
- * @property {number} skipped
22
- * @property {number} duration - 總毫秒數
23
- * @property {TestCase[]} tests
24
- * @property {string|null} rawOutput
25
- */
26
-
27
- export class ExecutorBase {
28
- /**
29
- * @param {Object} options
30
- * @param {number} [options.timeout=300000] - 5 分鐘
31
- */
32
- constructor(options = {}) {
33
- this.timeout = options.timeout ?? 300_000;
34
- }
35
-
36
- /**
37
- * 執行測試(子類別實作)
38
- * @param {string[]} testPaths
39
- * @param {Object} options
40
- * @returns {Promise<TestResults>}
41
- */
42
- // eslint-disable-next-line no-unused-vars
43
- async run(testPaths, options = {}) {
44
- throw new Error('子類別必須實作 run() 方法');
45
- }
46
-
47
- /**
48
- * 解析 JSON 輸出(子類別實作)
49
- * @param {string} output
50
- * @returns {TestResults}
51
- */
52
- // eslint-disable-next-line no-unused-vars
53
- parseResults(output) {
54
- throw new Error('子類別必須實作 parseResults() 方法');
55
- }
56
-
57
- /**
58
- * 建立空的 TestResults 骨架
59
- * @param {string} framework
60
- * @returns {TestResults}
61
- */
62
- emptyResults(framework) {
63
- return {
64
- framework,
65
- total: 0,
66
- passed: 0,
67
- failed: 0,
68
- skipped: 0,
69
- duration: 0,
70
- tests: [],
71
- rawOutput: null,
72
- };
73
- }
74
- }
@@ -1,29 +0,0 @@
1
- /**
2
- * 測試執行器工廠
3
- * 依框架名稱回傳對應的執行器實例
4
- */
5
-
6
- import { JestExecutor } from './jest-executor.js';
7
- import { VitestExecutor } from './vitest-executor.js';
8
- import { MochaExecutor } from './mocha-executor.js';
9
- import { FRAMEWORKS } from './test-detector.js';
10
-
11
- /**
12
- * @param {string} framework - 'jest' | 'vitest' | 'mocha'
13
- * @param {Object} options - 傳遞給執行器的選項(如 timeout)
14
- * @returns {import('./executor-base.js').ExecutorBase}
15
- */
16
- export function createExecutor(framework, options = {}) {
17
- switch (framework) {
18
- case FRAMEWORKS.JEST:
19
- return new JestExecutor(options);
20
- case FRAMEWORKS.VITEST:
21
- return new VitestExecutor(options);
22
- case FRAMEWORKS.MOCHA:
23
- return new MochaExecutor(options);
24
- default:
25
- throw new Error(
26
- `不支援的測試框架:「${framework}」\n目前支援:jest、vitest、mocha\n請在 .ai-git-config.js 的 autodev.framework 指定框架`
27
- );
28
- }
29
- }
@@ -1,107 +0,0 @@
1
- /**
2
- * Jest 測試執行器
3
- * 使用 --json 取得結構化輸出
4
- */
5
-
6
- import { execSync } from 'child_process';
7
- import { existsSync } from 'fs';
8
- import { join } from 'path';
9
- import { ExecutorBase } from './executor-base.js';
10
-
11
- export class JestExecutor extends ExecutorBase {
12
- constructor(options = {}) {
13
- super(options);
14
- this.framework = 'jest';
15
- }
16
-
17
- /**
18
- * 執行 Jest,回傳標準化 TestResults
19
- * @param {string[]} testPaths - 若為空則執行全部
20
- * @param {Object} options
21
- * @param {boolean} [options.verbose]
22
- * @returns {Promise<import('./executor-base.js').TestResults>}
23
- */
24
- async run(testPaths = [], options = {}) {
25
- const pathArgs = testPaths.length > 0 ? testPaths.join(' ') : '';
26
- const cmd = `npx jest --json --forceExit --passWithNoTests ${pathArgs}`;
27
- const projectRoot = options.projectRoot || process.cwd();
28
-
29
- // 若 node_modules 不存在,自動執行 npm install 安裝依賴
30
- const nodeModulesPath = join(projectRoot, 'node_modules');
31
- if (!existsSync(nodeModulesPath)) {
32
- console.log(' 📦 偵測到 node_modules 不存在,自動執行 npm install...');
33
- try {
34
- execSync('npm install', {
35
- cwd: projectRoot,
36
- stdio: 'inherit',
37
- timeout: 120_000, // 2 分鐘安裝時間上限
38
- });
39
- console.log(' ✅ 依賴安裝完成');
40
- } catch (err) {
41
- console.warn(` ⚠️ npm install 失敗:${err.message}`);
42
- console.warn(' 跳過測試步驟,代碼已生成但未驗證');
43
- return this.emptyResults(this.framework);
44
- }
45
- }
46
-
47
- if (options.verbose) {
48
- console.log(` 執行命令:${cmd}`);
49
- }
50
-
51
- let output = '';
52
- try {
53
- output = execSync(cmd, {
54
- encoding: 'utf-8',
55
- timeout: this.timeout,
56
- stdio: ['pipe', 'pipe', 'pipe'],
57
- cwd: projectRoot,
58
- });
59
- } catch (error) {
60
- // Jest 測試失敗時 exit code 非 0,但 stdout 仍有 JSON
61
- output = error.stdout || error.output?.[1] || '';
62
- }
63
-
64
- return this.parseResults(output);
65
- }
66
-
67
- /**
68
- * 將 Jest --json 輸出解析為標準 TestResults
69
- * @param {string} output
70
- * @returns {import('./executor-base.js').TestResults}
71
- */
72
- parseResults(output) {
73
- const result = this.emptyResults(this.framework);
74
- result.rawOutput = output;
75
-
76
- let json;
77
- try {
78
- // Jest --json 可能混有非 JSON 前綴,找第一個 { 開始
79
- const start = output.indexOf('{');
80
- if (start === -1) return result;
81
- json = JSON.parse(output.slice(start));
82
- } catch (_) {
83
- return result;
84
- }
85
-
86
- result.total = json.numTotalTests ?? 0;
87
- result.passed = json.numPassedTests ?? 0;
88
- result.failed = json.numFailedTests ?? 0;
89
- result.skipped = (json.numPendingTests ?? 0) + (json.numTodoTests ?? 0);
90
- result.duration = json.testResults?.reduce((acc, s) => acc + (s.perfStats?.runtime ?? 0), 0) ?? 0;
91
-
92
- for (const suite of json.testResults ?? []) {
93
- const suiteName = suite.testFilePath?.replace(process.cwd(), '').replace(/^\//, '') ?? '';
94
- for (const t of suite.testResults ?? []) {
95
- result.tests.push({
96
- name: t.fullName ?? t.title,
97
- suite: suiteName,
98
- passed: t.status === 'passed',
99
- duration: t.duration ?? 0,
100
- error: t.failureMessages?.join('\n') || null,
101
- });
102
- }
103
- }
104
-
105
- return result;
106
- }
107
- }
@@ -1,95 +0,0 @@
1
- /**
2
- * Mocha 測試執行器
3
- * 使用 --reporter json 取得結構化輸出
4
- */
5
-
6
- import { execSync } from 'child_process';
7
- import { ExecutorBase } from './executor-base.js';
8
-
9
- export class MochaExecutor extends ExecutorBase {
10
- constructor(options = {}) {
11
- super(options);
12
- this.framework = 'mocha';
13
- }
14
-
15
- /**
16
- * 執行 Mocha,回傳標準化 TestResults
17
- * @param {string[]} testPaths
18
- * @param {Object} options
19
- * @returns {Promise<import('./executor-base.js').TestResults>}
20
- */
21
- async run(testPaths = [], options = {}) {
22
- const pathArgs =
23
- testPaths.length > 0 ? testPaths.map((p) => `"${p}"`).join(' ') : '"test/**/*.js" "tests/**/*.js"';
24
- const cmd = `npx mocha --reporter json ${pathArgs}`;
25
-
26
- if (options.verbose) {
27
- console.log(` 執行命令:${cmd}`);
28
- }
29
-
30
- let output = '';
31
- try {
32
- output = execSync(cmd, {
33
- encoding: 'utf-8',
34
- timeout: this.timeout,
35
- stdio: ['pipe', 'pipe', 'pipe'],
36
- cwd: options.projectRoot || process.cwd(),
37
- });
38
- } catch (error) {
39
- // Mocha 失敗時 exit code 非 0,stdout 仍有 JSON
40
- output = error.stdout || error.output?.[1] || '';
41
- }
42
-
43
- return this.parseResults(output);
44
- }
45
-
46
- /**
47
- * 將 Mocha --reporter json 輸出解析為標準 TestResults
48
- * @param {string} output
49
- * @returns {import('./executor-base.js').TestResults}
50
- */
51
- parseResults(output) {
52
- const result = this.emptyResults(this.framework);
53
- result.rawOutput = output;
54
-
55
- let json;
56
- try {
57
- const start = output.indexOf('{');
58
- if (start === -1) return result;
59
- json = JSON.parse(output.slice(start));
60
- } catch (_) {
61
- return result;
62
- }
63
-
64
- const stats = json.stats || {};
65
- result.total = stats.tests ?? 0;
66
- result.passed = stats.passes ?? 0;
67
- result.failed = stats.failures ?? 0;
68
- result.skipped = stats.pending ?? 0;
69
- result.duration = stats.duration ?? 0;
70
-
71
- // passes
72
- for (const t of json.passes ?? []) {
73
- result.tests.push({
74
- name: t.fullTitle ?? t.title,
75
- suite: t.fullTitle?.replace(t.title, '').trim() ?? '',
76
- passed: true,
77
- duration: t.duration ?? 0,
78
- error: null,
79
- });
80
- }
81
-
82
- // failures
83
- for (const t of json.failures ?? []) {
84
- result.tests.push({
85
- name: t.fullTitle ?? t.title,
86
- suite: t.fullTitle?.replace(t.title, '').trim() ?? '',
87
- passed: false,
88
- duration: t.duration ?? 0,
89
- error: t.err?.message || t.err?.stack || null,
90
- });
91
- }
92
-
93
- return result;
94
- }
95
- }
@@ -1,90 +0,0 @@
1
- /**
2
- * 測試結果格式化模組
3
- * 將 TestResults 轉換為 GitHub Markdown 格式,供發佈到 Issue
4
- */
5
-
6
- /** 唯一識別標記,用於在多次執行時更新同一則評論 */
7
- export const COMMENT_MARKER = '<!-- ai-autodev-test-results -->';
8
-
9
- export class ResultFormatter {
10
- /**
11
- * 將 TestResults 格式化為 Markdown 字串
12
- * @param {import('./executor-base.js').TestResults} results
13
- * @param {import('../core/issue-parser.js').IssueData} issueData
14
- * @param {Object} [meta] - 額外的 meta 資訊
15
- * @returns {string}
16
- */
17
- formatAsMarkdown(results, issueData, meta = {}) {
18
- const now = new Date().toISOString().replace('T', ' ').slice(0, 19);
19
- const passRate = results.total > 0
20
- ? ((results.passed / results.total) * 100).toFixed(1)
21
- : '0.0';
22
- const allPassed = results.failed === 0 && results.total > 0;
23
- const statusEmoji = allPassed ? '✅' : '❌';
24
- const durationSec = (results.duration / 1000).toFixed(2);
25
-
26
- const lines = [
27
- COMMENT_MARKER,
28
- '',
29
- `## ${statusEmoji} 自動測試報告`,
30
- '',
31
- `> 由 \`ai autodev\` 自動執行 · ${now} UTC`,
32
- '',
33
- '### 摘要',
34
- '',
35
- '| 項目 | 結果 |',
36
- '|------|------|',
37
- `| 框架 | \`${results.framework}\` |`,
38
- `| 總測試數 | ${results.total} |`,
39
- `| ✅ 通過 | ${results.passed} |`,
40
- `| ❌ 失敗 | ${results.failed} |`,
41
- `| ⏭️ 跳過 | ${results.skipped} |`,
42
- `| 通過率 | **${passRate}%** |`,
43
- `| 執行時間 | ${durationSec}s |`,
44
- '',
45
- ];
46
-
47
- // 失敗詳情
48
- const failedTests = results.tests.filter((t) => !t.passed);
49
- if (failedTests.length > 0) {
50
- lines.push('### ❌ 失敗測試');
51
- lines.push('');
52
- for (const t of failedTests.slice(0, 20)) {
53
- lines.push(`#### \`${t.name}\``);
54
- if (t.suite) lines.push(`- **Suite**: ${t.suite}`);
55
- if (t.error) {
56
- lines.push('```');
57
- lines.push(t.error.slice(0, 500));
58
- lines.push('```');
59
- }
60
- lines.push('');
61
- }
62
- if (failedTests.length > 20) {
63
- lines.push(`> ... 還有 ${failedTests.length - 20} 個失敗測試未顯示`);
64
- lines.push('');
65
- }
66
- }
67
-
68
- // 後續步驟
69
- lines.push('### 下一步');
70
- lines.push('');
71
- if (allPassed) {
72
- lines.push('- 🎉 所有測試通過!');
73
- if (meta.commitHash) {
74
- lines.push(`- 📦 已自動提交:\`${meta.commitHash}\``);
75
- }
76
- if (meta.prUrl) {
77
- lines.push(`- 🔗 已自動建立 PR:${meta.prUrl}`);
78
- }
79
- } else {
80
- lines.push('- 請修復上方列出的失敗測試');
81
- lines.push('- 修復後重新執行 `ai autodev <issue>`');
82
- if (results.failed > 0) {
83
- lines.push(`- 或執行 \`npx ${results.framework} --watch\` 進行偵錯`);
84
- }
85
- }
86
-
87
- lines.push('');
88
- return lines.join('\n');
89
- }
90
- }
@@ -1,170 +0,0 @@
1
- /**
2
- * 測試框架檢測模組
3
- * 自動偵測專案使用的測試框架(Jest / Vitest / Mocha)
4
- * 並發現所有相關測試檔案
5
- */
6
-
7
- import { existsSync, readdirSync, statSync, readFileSync } from 'fs';
8
- import { resolve, join } from 'path';
9
-
10
- /** 支援的框架識別字 */
11
- export const FRAMEWORKS = {
12
- JEST: 'jest',
13
- VITEST: 'vitest',
14
- MOCHA: 'mocha',
15
- };
16
-
17
- export class TestDetector {
18
- /**
19
- * @param {string} projectRoot - 專案根目錄(預設 process.cwd())
20
- */
21
- constructor(projectRoot = process.cwd()) {
22
- this.projectRoot = projectRoot;
23
- }
24
-
25
- /**
26
- * 偵測測試框架
27
- * 優先順序:設定檔 > package.json scripts > package.json devDependencies
28
- * @returns {string|null} 框架名稱或 null
29
- */
30
- detectFramework() {
31
- const root = this.projectRoot;
32
-
33
- // 1. 專用設定檔
34
- if (
35
- existsSync(join(root, 'vitest.config.ts')) ||
36
- existsSync(join(root, 'vitest.config.js')) ||
37
- existsSync(join(root, 'vitest.config.mjs'))
38
- ) {
39
- return FRAMEWORKS.VITEST;
40
- }
41
-
42
- if (
43
- existsSync(join(root, 'jest.config.js')) ||
44
- existsSync(join(root, 'jest.config.ts')) ||
45
- existsSync(join(root, 'jest.config.mjs')) ||
46
- existsSync(join(root, 'jest.config.json'))
47
- ) {
48
- return FRAMEWORKS.JEST;
49
- }
50
-
51
- if (
52
- existsSync(join(root, '.mocharc.js')) ||
53
- existsSync(join(root, '.mocharc.cjs')) ||
54
- existsSync(join(root, '.mocharc.json')) ||
55
- existsSync(join(root, '.mocharc.yml')) ||
56
- existsSync(join(root, '.mocharc.yaml'))
57
- ) {
58
- return FRAMEWORKS.MOCHA;
59
- }
60
-
61
- // 2. package.json
62
- const pkgPath = join(root, 'package.json');
63
- if (existsSync(pkgPath)) {
64
- try {
65
- const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
66
-
67
- // test script
68
- const testScript = pkg.scripts?.test || '';
69
- if (/\bvitest\b/.test(testScript)) return FRAMEWORKS.VITEST;
70
- if (/\bjest\b/.test(testScript)) return FRAMEWORKS.JEST;
71
- if (/\bmocha\b/.test(testScript)) return FRAMEWORKS.MOCHA;
72
-
73
- // devDependencies / dependencies
74
- const allDeps = {
75
- ...pkg.devDependencies,
76
- ...pkg.dependencies,
77
- };
78
- if (allDeps.vitest) return FRAMEWORKS.VITEST;
79
- if (allDeps.jest || allDeps['@jest/core']) return FRAMEWORKS.JEST;
80
- if (allDeps.mocha) return FRAMEWORKS.MOCHA;
81
- } catch (_) {
82
- // 解析失敗,繼續往下
83
- }
84
- }
85
-
86
- return null;
87
- }
88
-
89
- /**
90
- * 遞迴搜尋符合 patterns 的檔案
91
- * @param {string} dir
92
- * @param {RegExp[]} patterns
93
- * @param {string[]} excludes - 排除目錄名稱
94
- * @returns {string[]}
95
- */
96
- _glob(dir, patterns, excludes = ['node_modules', 'dist', '.next', 'coverage', '.git']) {
97
- const results = [];
98
-
99
- let entries;
100
- try {
101
- entries = readdirSync(dir);
102
- } catch (_) {
103
- return results;
104
- }
105
-
106
- for (const entry of entries) {
107
- if (excludes.includes(entry)) continue;
108
- const fullPath = join(dir, entry);
109
- let stat;
110
- try {
111
- stat = statSync(fullPath);
112
- } catch (_) {
113
- continue;
114
- }
115
-
116
- if (stat.isDirectory()) {
117
- results.push(...this._glob(fullPath, patterns, excludes));
118
- } else if (patterns.some((p) => p.test(entry))) {
119
- results.push(fullPath);
120
- }
121
- }
122
-
123
- return results;
124
- }
125
-
126
- /**
127
- * 依框架發現測試檔案
128
- * @param {string} framework
129
- * @param {Object} [config] - 可選:{ testPaths?: string[] }
130
- * @returns {string[]}
131
- */
132
- discoverTests(framework, config = {}) {
133
- // 若使用者明確指定路徑,直接回傳(轉為絕對路徑)
134
- if (config.testPaths && config.testPaths.length > 0) {
135
- return config.testPaths.map((p) => resolve(this.projectRoot, p));
136
- }
137
-
138
- const commonPatterns = [
139
- /\.test\.(js|ts|jsx|tsx|mjs|cjs)$/,
140
- /\.spec\.(js|ts|jsx|tsx|mjs|cjs)$/,
141
- ];
142
-
143
- switch (framework) {
144
- case FRAMEWORKS.JEST:
145
- case FRAMEWORKS.VITEST:
146
- return this._glob(this.projectRoot, commonPatterns);
147
-
148
- case FRAMEWORKS.MOCHA: {
149
- // Mocha 慣例:test/ 或 tests/ 目錄
150
- const mochaPatterns = [/\.(js|ts|mjs|cjs)$/];
151
- const dirs = ['test', 'tests', 'spec', 'specs'].map((d) =>
152
- join(this.projectRoot, d)
153
- );
154
- const results = [];
155
- for (const d of dirs) {
156
- if (existsSync(d)) {
157
- results.push(...this._glob(d, mochaPatterns));
158
- }
159
- }
160
- // 也搜尋根目錄的 .test. / .spec. 檔案
161
- results.push(...this._glob(this.projectRoot, commonPatterns));
162
- return [...new Set(results)];
163
- }
164
-
165
- default:
166
- // 未知框架,使用通用 patterns
167
- return this._glob(this.projectRoot, commonPatterns);
168
- }
169
- }
170
- }
@@ -1,90 +0,0 @@
1
- /**
2
- * Vitest 測試執行器
3
- * 使用 --reporter=json 取得結構化輸出
4
- */
5
-
6
- import { execSync } from 'child_process';
7
- import { ExecutorBase } from './executor-base.js';
8
-
9
- export class VitestExecutor extends ExecutorBase {
10
- constructor(options = {}) {
11
- super(options);
12
- this.framework = 'vitest';
13
- }
14
-
15
- /**
16
- * 執行 Vitest,回傳標準化 TestResults
17
- * @param {string[]} testPaths
18
- * @param {Object} options
19
- * @returns {Promise<import('./executor-base.js').TestResults>}
20
- */
21
- async run(testPaths = [], options = {}) {
22
- const pathArgs = testPaths.length > 0 ? testPaths.join(' ') : '';
23
- const cmd = `npx vitest run --reporter=json ${pathArgs}`;
24
-
25
- if (options.verbose) {
26
- console.log(` 執行命令:${cmd}`);
27
- }
28
-
29
- let output = '';
30
- try {
31
- output = execSync(cmd, {
32
- encoding: 'utf-8',
33
- timeout: this.timeout,
34
- stdio: ['pipe', 'pipe', 'pipe'],
35
- cwd: options.projectRoot || process.cwd(),
36
- });
37
- } catch (error) {
38
- output = error.stdout || error.output?.[1] || '';
39
- }
40
-
41
- return this.parseResults(output);
42
- }
43
-
44
- /**
45
- * 將 Vitest --reporter=json 輸出解析為標準 TestResults
46
- * @param {string} output
47
- * @returns {import('./executor-base.js').TestResults}
48
- */
49
- parseResults(output) {
50
- const result = this.emptyResults(this.framework);
51
- result.rawOutput = output;
52
-
53
- let json;
54
- try {
55
- const start = output.indexOf('{');
56
- if (start === -1) return result;
57
- json = JSON.parse(output.slice(start));
58
- } catch (_) {
59
- return result;
60
- }
61
-
62
- // Vitest JSON 結構
63
- const sum = (json.numPassedTests ?? 0) + (json.numFailedTests ?? 0) + (json.numPendingTests ?? 0);
64
- const numTotalTests = json.numTotalTests ?? sum;
65
- result.total = numTotalTests;
66
- result.passed = json.numPassedTests ?? 0;
67
- result.failed = json.numFailedTests ?? 0;
68
- result.skipped = json.numPendingTests ?? 0;
69
- result.duration = json.testResults?.reduce(
70
- (acc, s) => acc + (s.perfStats?.runtime ?? s.duration ?? 0),
71
- 0
72
- ) ?? 0;
73
-
74
- for (const suite of json.testResults ?? []) {
75
- const suiteName =
76
- suite.name?.replace(process.cwd(), '').replace(/^\//, '') ?? '';
77
- for (const t of suite.assertionResults ?? []) {
78
- result.tests.push({
79
- name: t.fullName ?? t.title,
80
- suite: suiteName,
81
- passed: t.status === 'passed',
82
- duration: t.duration ?? 0,
83
- error: t.failureMessages?.join('\n') || null,
84
- });
85
- }
86
- }
87
-
88
- return result;
89
- }
90
- }