diff --git a/.gitea/ai-review/exclusions.json b/.gitea/ai-review/exclusions.json index 365ace9..072e4bf 100644 --- a/.gitea/ai-review/exclusions.json +++ b/.gitea/ai-review/exclusions.json @@ -329,5 +329,15 @@ "role": "Leo", "location": "action.yaml:80", "suggestion": "在 `runs.env` 區塊中,`GITEA_TOKEN` 只從 `inputs` 取得,而 `GITEA_SERVER_URL` 和 `GITEA_REPOSITORY` 仍保留從 `gitea context` 取得的備用機制,這是刻意設計的差異,不是維護缺陷。" + }, + { + "role": "Rex", + "location": "action.yaml:18", + "suggestion": "引入 `GITEA_COMMENT_TOKEN` 是一個很好的實踐,遵循最小權限原則。請確保為此 token 配置的權限確實僅限於發布評論。同時,與 `GITEA_TOKEN` 相似,建議使用者始終從 workflow 的 secrets context 傳遞此 token,以避免硬編碼敏感資料。" + }, + { + "role": "Leo", + "location": "app/log.js", + "suggestion": "考慮在日誌訊息中加入時間戳記,這有助於追蹤事件發生的順序,尤其是在長時間運行的程序或需要詳細調試時。可以在每個日誌函式內部自動添加時間戳記。" } ] diff --git a/.gitea/ai-review/findings.json b/.gitea/ai-review/findings.json index b3941b3..f5022c2 100644 --- a/.gitea/ai-review/findings.json +++ b/.gitea/ai-review/findings.json @@ -1,23 +1,16 @@ [ { "level": "info", - "role": "Rex", - "location": "action.yaml:18", - "suggestion": "引入 GITEA_COMMENT_TOKEN 是一個很好的實踐,遵循最小權限原則。請確保為此 token 配置的權限確實僅限於發布評論。同時,與 GITEA_TOKEN 類似,建議使用者始終從 workflow 的 secrets context 傳遞此 token,以避免硬編碼敏感資料。", + "role": "Maya", + "location": "app/log.js", + "suggestion": "log.js 檔案中的 ok, warn, error 函數是應用程式的日誌工具。雖然功能簡單,但為這些工具函數編寫單元測試是一個良好的實踐,以確保它們正確地呼叫 console 對應的方法(如 console.log, console.warn, console.error)並輸出預期的格式。這有助於防止未來意外的行為變更。", "is_new": false }, { "level": "info", - "role": "Leo", - "location": "app/log.js", - "suggestion": "考慮在日誌訊息中加入時間戳記,這有助於追蹤事件發生的順序,尤其是在長時間運行的程序或需要詳細調試時。可以在每個日誌函式內部自動添加時間戳記。", - "is_new": true - }, - { - "level": "info", - "role": "Leo", - "location": "app/log.js:19", - "suggestion": "在 `warn` 函式中使用 `console.warn` 而非 `console.log`。雖然目前功能相同,但 `console.warn` 在某些環境下(例如瀏覽器開發者工具)會以不同的樣式呈現警告訊息,有助於區分不同嚴重程度的日誌。", + "role": "Maya", + "location": "app/log.test.js", + "suggestion": "`log.test.js` 的新增非常棒,提供了良好的覆蓋率。為了進一步提升測試的完整性,建議考慮為 `line`, `ok`, `warn`, `error` 函數新增測試案例,以驗證當傳入空字串時的行為。雖然這些函數的行為相對簡單,但測試空字串可以確保邊界情況下的輸出符合預期。", "is_new": true } ] diff --git a/app/git.test.js b/app/git.test.js index 24abf62..fd6543d 100644 --- a/app/git.test.js +++ b/app/git.test.js @@ -185,12 +185,16 @@ describe('commitAndPush', () => { }); const logs = []; const originalLog = console.log; - console.log = (...args) => { logs.push(args.join(' ')); }; + const originalWarn = console.warn; + const capture = (...args) => { logs.push(args.join(' ')); }; + console.log = capture; + console.warn = capture; try { await commitAndPush(workspace, repoDir, spawn, sourceRoot); } finally { console.log = originalLog; + console.warn = originalWarn; } assert.ok(logs.some(line => line.includes('Step7 commit 成功但 push 失敗'))); diff --git a/app/log.js b/app/log.js index cde500f..a2155bc 100644 --- a/app/log.js +++ b/app/log.js @@ -15,7 +15,7 @@ export function ok(message) { } export function warn(message) { - console.log(` ! ${message}`); + console.warn(` ! ${message}`); } export function error(message) { diff --git a/app/log.test.js b/app/log.test.js new file mode 100644 index 0000000..c810d77 --- /dev/null +++ b/app/log.test.js @@ -0,0 +1,59 @@ +import { describe, it, afterEach, mock } from 'node:test'; +import assert from 'node:assert/strict'; +import { section, step, line, ok, warn, error } from './log.js'; + +afterEach(() => mock.restoreAll()); + +describe('log helpers', () => { + it('formats section and step messages', () => { + const calls = []; + mock.method(console, 'log', (...args) => { + calls.push(args.join(' ')); + }); + + section('Pipeline'); + step('Step1', 'Start'); + + assert.deepEqual(calls, [ + '\n=== Pipeline ===', + '\n[Step1] Start', + ]); + }); + + it('formats line and ok messages with console.log', () => { + const calls = []; + mock.method(console, 'log', (...args) => { + calls.push(args.join(' ')); + }); + + line('hello'); + ok('done'); + + assert.deepEqual(calls, [ + ' - hello', + ' ✓ done', + ]); + }); + + it('formats warn messages with console.warn', () => { + const calls = []; + mock.method(console, 'warn', (...args) => { + calls.push(args.join(' ')); + }); + + warn('careful'); + + assert.deepEqual(calls, [' ! careful']); + }); + + it('formats error messages with console.error', () => { + const calls = []; + mock.method(console, 'error', (...args) => { + calls.push(args.join(' ')); + }); + + error('boom'); + + assert.deepEqual(calls, [' x boom']); + }); +}); diff --git a/app/main.js b/app/main.js index f7fc6ba..03cd644 100644 --- a/app/main.js +++ b/app/main.js @@ -76,7 +76,7 @@ async function main() { } ok(`Step2 完成: 新 findings 總計 ${newFindings.length} 筆`); - step('Step3', 'Findings 合併'); + step('Step3', 'Findings 合併與語意去重'); let repoDir; try { repoDir = cloneRepo(WORKSPACE); @@ -90,11 +90,9 @@ async function main() { const oldFindings = loadOldFindings(repoDir || WORKSPACE); const mergedFindings = mergeFindings(oldFindings, newFindings); ok(`Step3 merged findings total=${mergedFindings.length}`); - - step('Step3b', 'AI 語意去重'); const deduped = await deduplicateWithAI(mergedFindings); const sorted = sortByLevel(deduped); - ok(`Step3b dedup findings total=${sorted.length} (critical=${sorted.filter(f=>f.level==='critical').length} warning=${sorted.filter(f=>f.level==='warning').length} info=${sorted.filter(f=>f.level==='info').length})`); + ok(`Step3 去重完成: ${mergedFindings.length} -> ${sorted.length} 筆 (critical=${sorted.filter(f=>f.level==='critical').length} warning=${sorted.filter(f=>f.level==='warning').length} info=${sorted.filter(f=>f.level==='info').length})`); step('Step4', 'AI 排除問題過濾'); const exclusions = loadExclusions(repoDir || WORKSPACE, repoState);