ai-git-tools 2.0.48 → 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.
- package/bin/cli.js +0 -22
- package/package.json +1 -1
- package/src/commands/commit-all.js +0 -79
- package/src/commands/init.js +0 -38
- package/src/commands/pr.js +0 -63
- package/src/core/ai-client.js +15 -28
- package/src/core/config-loader.js +0 -53
- package/src/pr-modules/core/github-api.js +0 -84
- package/src/commands/autodev.js +0 -62
- package/src/dev-modules/ai/code-generator.js +0 -288
- package/src/dev-modules/core/autodev-workflow.js +0 -305
- package/src/dev-modules/core/issue-parser.js +0 -144
- package/src/dev-modules/test/executor-base.js +0 -74
- package/src/dev-modules/test/executor-factory.js +0 -29
- package/src/dev-modules/test/jest-executor.js +0 -107
- package/src/dev-modules/test/mocha-executor.js +0 -95
- package/src/dev-modules/test/result-formatter.js +0 -90
- package/src/dev-modules/test/test-detector.js +0 -170
- package/src/dev-modules/test/vitest-executor.js +0 -90
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Issue 解析模組
|
|
3
|
-
* 負責解析 Issue 編號/URL,並從 GitHub API 擷取 Issue 資料
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { execSync } from 'child_process';
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* 標準化 IssueData 結構
|
|
10
|
-
* @typedef {Object} IssueData
|
|
11
|
-
* @property {number} number - Issue 編號
|
|
12
|
-
* @property {string} title - Issue 標題
|
|
13
|
-
* @property {string} body - Issue 說明
|
|
14
|
-
* @property {string[]} labels - 標籤列表
|
|
15
|
-
* @property {string} owner - 倉庫擁有者
|
|
16
|
-
* @property {string} repo - 倉庫名稱
|
|
17
|
-
* @property {string} url - Issue URL
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
export class IssueParser {
|
|
21
|
-
/**
|
|
22
|
-
* 解析輸入(數字字串或完整 URL)為 Issue 編號
|
|
23
|
-
* @param {string|number} input - Issue 編號或 GitHub Issue URL
|
|
24
|
-
* @returns {number} Issue 編號
|
|
25
|
-
*/
|
|
26
|
-
parseIssueNumber(input) {
|
|
27
|
-
const str = String(input).trim();
|
|
28
|
-
|
|
29
|
-
// 純數字
|
|
30
|
-
if (/^\d+$/.test(str)) {
|
|
31
|
-
return parseInt(str, 10);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// GitHub URL 格式:https://github.com/owner/repo/issues/123
|
|
35
|
-
const urlMatch = str.match(/\/issues\/(\d+)/);
|
|
36
|
-
if (urlMatch) {
|
|
37
|
-
return parseInt(urlMatch[1], 10);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
throw new Error(
|
|
41
|
-
`無效的 Issue 輸入:「${input}」\n請提供 Issue 編號(如 123)或完整 URL(如 https://github.com/owner/repo/issues/123)`
|
|
42
|
-
);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* 從 GitHub URL 解析倉庫資訊
|
|
47
|
-
* @param {string} url
|
|
48
|
-
* @returns {{ owner: string, repo: string, issueNumber: number }}
|
|
49
|
-
*/
|
|
50
|
-
parseIssueURL(url) {
|
|
51
|
-
const match = url.match(/github\.com\/([^/]+)\/([^/]+)\/issues\/(\d+)/);
|
|
52
|
-
if (!match) {
|
|
53
|
-
throw new Error(`無法解析 GitHub Issue URL:${url}`);
|
|
54
|
-
}
|
|
55
|
-
return {
|
|
56
|
-
owner: match[1],
|
|
57
|
-
repo: match[2].replace(/\.git$/, ''),
|
|
58
|
-
issueNumber: parseInt(match[3], 10),
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* 從 git remote 自動偵測 owner/repo
|
|
64
|
-
* @returns {{ owner: string, repo: string }}
|
|
65
|
-
*/
|
|
66
|
-
detectRepoFromRemote() {
|
|
67
|
-
try {
|
|
68
|
-
const remoteUrl = execSync('git remote get-url origin', {
|
|
69
|
-
encoding: 'utf-8',
|
|
70
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
71
|
-
}).trim();
|
|
72
|
-
|
|
73
|
-
// HTTPS: https://github.com/owner/repo.git
|
|
74
|
-
const httpsMatch = remoteUrl.match(/github\.com\/([^/]+)\/([^/\s]+?)(?:\.git)?$/);
|
|
75
|
-
if (httpsMatch) {
|
|
76
|
-
return { owner: httpsMatch[1], repo: httpsMatch[2] };
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// SSH: git@github.com:owner/repo.git
|
|
80
|
-
const sshMatch = remoteUrl.match(/github\.com:([^/]+)\/([^/\s]+?)(?:\.git)?$/);
|
|
81
|
-
if (sshMatch) {
|
|
82
|
-
return { owner: sshMatch[1], repo: sshMatch[2] };
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
throw new Error('無法從 git remote 解析 owner/repo');
|
|
86
|
-
} catch (error) {
|
|
87
|
-
throw new Error(`偵測 git remote 失敗:${error.message}`);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* 透過 GitHub CLI 擷取 Issue 資料
|
|
93
|
-
* @param {string} owner
|
|
94
|
-
* @param {string} repo
|
|
95
|
-
* @param {number} issueNumber
|
|
96
|
-
* @returns {IssueData}
|
|
97
|
-
*/
|
|
98
|
-
async fetchIssue(owner, repo, issueNumber) {
|
|
99
|
-
try {
|
|
100
|
-
const json = execSync(
|
|
101
|
-
`gh api repos/${owner}/${repo}/issues/${issueNumber}`,
|
|
102
|
-
{ encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }
|
|
103
|
-
);
|
|
104
|
-
const data = JSON.parse(json);
|
|
105
|
-
|
|
106
|
-
return {
|
|
107
|
-
number: data.number,
|
|
108
|
-
title: data.title,
|
|
109
|
-
body: data.body || '',
|
|
110
|
-
labels: (data.labels || []).map((l) => l.name),
|
|
111
|
-
owner,
|
|
112
|
-
repo,
|
|
113
|
-
url: data.html_url,
|
|
114
|
-
};
|
|
115
|
-
} catch (error) {
|
|
116
|
-
if (error.message.includes('404')) {
|
|
117
|
-
throw new Error(
|
|
118
|
-
`Issue #${issueNumber} 不存在於 ${owner}/${repo}\n請確認 Issue 編號正確,且你有存取該倉庫的權限`
|
|
119
|
-
);
|
|
120
|
-
}
|
|
121
|
-
throw new Error(`擷取 Issue 失敗:${error.message}`);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* 解析輸入並完整擷取 Issue 資料(主要入口)
|
|
127
|
-
* @param {string|number} input - Issue 編號或 URL
|
|
128
|
-
* @returns {Promise<IssueData>}
|
|
129
|
-
*/
|
|
130
|
-
async parse(input) {
|
|
131
|
-
const str = String(input).trim();
|
|
132
|
-
|
|
133
|
-
// 如果是完整 URL,直接解析 owner/repo
|
|
134
|
-
if (str.startsWith('http')) {
|
|
135
|
-
const { owner, repo, issueNumber } = this.parseIssueURL(str);
|
|
136
|
-
return this.fetchIssue(owner, repo, issueNumber);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// 否則從 git remote 偵測
|
|
140
|
-
const issueNumber = this.parseIssueNumber(str);
|
|
141
|
-
const { owner, repo } = this.detectRepoFromRemote();
|
|
142
|
-
return this.fetchIssue(owner, repo, issueNumber);
|
|
143
|
-
}
|
|
144
|
-
}
|
|
@@ -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
|
-
}
|