From 6398d4a19368e9585ebca9a633917fb77d1d187a Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 23 Jun 2026 16:36:25 +0800 Subject: [PATCH 01/14] =?UTF-8?q?test(ai-review=20=E4=BD=BF=E7=94=A8?= =?UTF-8?q?=E9=87=8F):=20=E8=A3=9C=20formatUsageStatsLine=20=E5=B0=8D=20Na?= =?UTF-8?q?N/=E7=95=B0=E5=B8=B8=E6=95=B8=E5=AD=97=E8=BC=B8=E5=87=BA?= =?UTF-8?q?=E5=AE=89=E5=85=A8=E6=96=87=E5=AD=97=E6=B8=AC=E8=A9=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/usage.test.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/usage.test.js b/app/usage.test.js index e0785b1..aa79008 100644 --- a/app/usage.test.js +++ b/app/usage.test.js @@ -244,4 +244,13 @@ describe('formatUsageStatsLine', () => { const line = formatUsageStatsLine('ollama', 'llama3', usage, { available: false, reason: '本地服務,無帳號額度概念' }, { hasData: false }); assert.match(line, /;剩餘可用: 無法計算(本地服務,無帳號額度概念)/); }); + + it('produces safe text (no NaN) when quota/rate carry invalid numbers', () => { + // quota.limit 為 NaN、rate.limit 為 NaN → 不應算出百分比、不得輸出 NaN + const line = formatUsageStatsLine('openai', 'm', usage, + { available: true, used: 5, limit: NaN, currency: 'USD' }, + { hasData: true, remaining: NaN, limit: NaN, kind: 'tokens' }); + assert.doesNotMatch(line, /NaN/); + assert.match(line, /剩餘可用: 無法計算/); + }); }); -- 2.53.0 From 1ff74297ce08cca28ca7d893273707a16982ff78 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 23 Jun 2026 16:36:25 +0800 Subject: [PATCH 02/14] =?UTF-8?q?chore(ai-review=20=E7=8B=80=E6=85=8B):=20?= =?UTF-8?q?=E8=A3=9C=20NaN=20=E6=B8=AC=E8=A9=A6=E3=80=81=E6=8E=92=E9=99=A4?= =?UTF-8?q?=20recordRateLimit=20=E5=BE=AE=E5=84=AA=E5=8C=96=E8=AA=A4?= =?UTF-8?q?=E5=A0=B1=EF=BC=8C=E6=B8=85=E7=A9=BA=20findings.json?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/ai-review/exclusions.json | 6 ++++++ .gitea/ai-review/findings.json | 19 +------------------ 2 files changed, 7 insertions(+), 18 deletions(-) diff --git a/.gitea/ai-review/exclusions.json b/.gitea/ai-review/exclusions.json index 6a36245..0c8f679 100644 --- a/.gitea/ai-review/exclusions.json +++ b/.gitea/ai-review/exclusions.json @@ -518,5 +518,11 @@ "role": "Maya", "original_finding": "parseBotReviewComment 的測試沒有驗證解析失敗時回傳 null 的行為。", "reason": "誤判,測試已存在。'returns null for free-form human comments' 已斷言自由格式留言、空字串、null 皆回傳 null。" + }, + { + "location": "app/usage.js:146", + "role": "Rogue", + "original_finding": "在 recordRateLimit 中頻繁呼叫 lowerCaseKeys,對每個請求的 headers 複製與轉換,增加記憶體分配開銷;建議改用不分大小寫存取避免複製。", + "reason": "過度設計/非熱路徑。recordRateLimit 每次 LLM 回應只呼叫一次(一輪審查約 13 次),headers 物件小,複製成本可忽略;改用不分大小寫存取器反而增加複雜度,效益不成比例。" } ] diff --git a/.gitea/ai-review/findings.json b/.gitea/ai-review/findings.json index bd73ac7..fe51488 100644 --- a/.gitea/ai-review/findings.json +++ b/.gitea/ai-review/findings.json @@ -1,18 +1 @@ -[ - { - "level": "warning", - "role": "Maya", - "location": "app/usage.test.js:240", - "problem": "`formatUsageStatsLine` 測試案例中,僅驗證了單一平台的格式,缺失了當 `quota` 或 `rate` 資料缺失或包含無效數字(如 `NaN`)時的處理測試。", - "suggestion": "補充針對 `quota` 或 `rate` 傳入異常資料(如 `limit: NaN`)的測試,驗證 `formatUsageStatsLine` 是否能產生安全的預設文字,而非輸出 `NaN` 或破壞版面。", - "is_new": true - }, - { - "level": "warning", - "role": "Rogue", - "location": "app/usage.js:146", - "problem": "在 `recordRateLimit` 中頻繁呼叫 `lowerCaseKeys`,這會對每個請求的 headers 進行複製與轉換,增加記憶體分配開銷。", - "suggestion": "建議直接存取 headers 時改用不區分大小寫的存取函式,避免複製整個物件。", - "is_new": true - } -] +[] -- 2.53.0 From d7e5bee65e44d0f8daea7d109e104bd0d731391b Mon Sep 17 00:00:00 2001 From: AI Review Bot Date: Tue, 23 Jun 2026 08:37:50 +0000 Subject: [PATCH 03/14] chore: update ai-review findings [ai-review-bot][failure] --- .gitea/ai-review/findings.json | 35 +++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/.gitea/ai-review/findings.json b/.gitea/ai-review/findings.json index fe51488..abcb0eb 100644 --- a/.gitea/ai-review/findings.json +++ b/.gitea/ai-review/findings.json @@ -1 +1,34 @@ -[] +[ + { + "level": "critical", + "role": "Mage", + "location": "app/usage.test.js:254", + "problem": "測試情境雖處理了 NaN,但未涵蓋 `limit` 為 0 或負數等極端數值(例如除以零導致的錯誤或異常百分比計算)。若 `formatUsageStatsLine` 內部有計算 `(used / limit) * 100` 或類似邏輯,傳入 0 會產生 `Infinity`,傳入負數則會產生荒謬的邏輯結果。", + "suggestion": "建議新增對 `limit: 0` 以及負數邊界值的測試案例,確保函式對這些非預期的數值輸入同樣能輸出「無法計算」或相應的錯誤處理訊息,而非輸出 `Infinity` 或負百分比。", + "is_new": true + }, + { + "level": "warning", + "role": "Leo", + "location": "app/usage.test.js:250", + "problem": "在測試案例中直接引用外部定義的 `usage` 變數,若該變數在其他測試中被意外修改,會導致測試間的隱性耦合,使得測試結果難以預測,降低測試的可維護性。", + "suggestion": "建議直接在測試函式內定義該案例所需的完整 `usage` 物件,或是使用工廠函式(Factory function)來生成所需的測試資料,確保測試案例的獨立性與確定性。", + "is_new": true + }, + { + "level": "warning", + "role": "Maya", + "location": "app/usage.test.js:250", + "problem": "目前的測試僅針對 NaN 的情況,但未考慮到 Infinity、null 或 undefined 等其他同樣可能導致格式錯誤或輸出異常的「無效數字」邊界條件。", + "suggestion": "建議補充針對 Infinity、null 與 undefined 的測試案例,確保這些值在 formatUsageStatsLine 中皆能正確處理並輸出友善的錯誤訊息,而非損壞的文字。", + "is_new": true + }, + { + "level": "info", + "role": "Maya", + "location": "app/usage.test.js:250", + "problem": "目前的測試案例將多個欄位同時設為 NaN,測試邏輯較為單一,未能明確驗證「單一欄位無效」與「組合欄位無效」時的具體行為差異。", + "suggestion": "建議將測試拆分為更細緻的案例,分別驗證 quota.limit 與 rate.limit 欄位在單獨無效,以及兩者同時無效的情況,以確保各欄位的防禦性邏輯皆有被完整覆蓋。", + "is_new": true + } +] -- 2.53.0 From 6259cd43987e5669767b9c73f9ab4573aa5c3a60 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 23 Jun 2026 16:44:43 +0800 Subject: [PATCH 04/14] =?UTF-8?q?fix(ai-review=20=E4=BD=BF=E7=94=A8?= =?UTF-8?q?=E9=87=8F):=20resolveRemainingPercent=20=E8=A6=81=E6=B1=82?= =?UTF-8?q?=E6=9C=89=E9=99=90=E6=AD=A3=E6=95=B8=E4=B8=8A=E9=99=90=EF=BC=8C?= =?UTF-8?q?=E9=81=BF=E5=85=8D=20Infinity=20=E4=B8=8A=E9=99=90=E7=AE=97?= =?UTF-8?q?=E5=87=BA=20NaN%=EF=BC=8F=E8=B2=A0=E7=99=BE=E5=88=86=E6=AF=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/usage.js | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/app/usage.js b/app/usage.js index 5c24bba..16778a0 100644 --- a/app/usage.js +++ b/app/usage.js @@ -211,23 +211,32 @@ function round1(n) { const RATE_KIND_LABEL = { tokens: 'token', requests: '次數' }; +/** 是否為有限正數(排除 0、負數、NaN、Infinity),避免算出 Infinity%/NaN%/負百分比。 */ +function isFinitePositive(n) { + return Number.isFinite(Number(n)) && Number(n) > 0; +} + /** * 計算「剩餘可用百分比」,依優先序擇一: - * 1. 帳號額度(quota 有上限)→ 剩餘 credits / 上限; - * 2. 速率配額(rate limit header)→ 當前視窗剩餘 / 上限; - * 皆無法取得時回傳 { percent: null, reason }。 + * 1. 帳號額度(quota 有有限正數上限)→ 剩餘 credits / 上限; + * 2. 速率配額(rate limit header,有限正數上限)→ 當前視窗剩餘 / 上限; + * 上限或剩餘為 0/負數/NaN/Infinity 等無效值時不計算,落到 { percent: null, reason }。 */ export function resolveRemainingPercent(quota, rate) { - if (quota?.available && quota.limit != null && Number(quota.limit) > 0) { + if (quota?.available && quota.limit != null && isFinitePositive(quota.limit)) { const limit = Number(quota.limit); const remaining = quota.remaining == null ? limit - num(quota.used) : Number(quota.remaining); - return { percent: round1((remaining / limit) * 100), basis: '帳號額度', remaining, limit, unit: quota.currency || '' }; + if (Number.isFinite(remaining)) { + return { percent: round1((remaining / limit) * 100), basis: '帳號額度', remaining, limit, unit: quota.currency || '' }; + } } - if (rate?.hasData && Number(rate.limit) > 0) { + if (rate?.hasData && isFinitePositive(rate.limit)) { const limit = Number(rate.limit); const remaining = Number(rate.remaining); - const kindLabel = RATE_KIND_LABEL[rate.kind] || rate.kind; - return { percent: round1((remaining / limit) * 100), basis: `速率配額(當前視窗,${kindLabel})`, remaining, limit, unit: '' }; + if (Number.isFinite(remaining)) { + const kindLabel = RATE_KIND_LABEL[rate.kind] || rate.kind; + return { percent: round1((remaining / limit) * 100), basis: `速率配額(當前視窗,${kindLabel})`, remaining, limit, unit: '' }; + } } let reason; if (quota?.available && quota.limit == null) reason = '帳號額度無上限,無法計算百分比'; -- 2.53.0 From 791449161fef8d989e3b7bbe5408074519f73981 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 23 Jun 2026 16:44:43 +0800 Subject: [PATCH 05/14] =?UTF-8?q?test(ai-review=20=E4=BD=BF=E7=94=A8?= =?UTF-8?q?=E9=87=8F):=20=E8=A3=9C=20quota/rate=20=E4=B8=8A=E9=99=90?= =?UTF-8?q?=E7=82=BA=200/=E8=B2=A0=E6=95=B8/Infinity/NaN=20=E8=88=87?= =?UTF-8?q?=E5=89=A9=E9=A4=98=E9=9D=9E=E6=9C=89=E9=99=90=E7=9A=84=E9=82=8A?= =?UTF-8?q?=E7=95=8C=E6=B8=AC=E8=A9=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/usage.test.js | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/app/usage.test.js b/app/usage.test.js index aa79008..21655fc 100644 --- a/app/usage.test.js +++ b/app/usage.test.js @@ -196,6 +196,25 @@ describe('resolveRemainingPercent', () => { assert.match(pct.reason, /無上限/); }); + it('returns null percent for non-finite or non-positive quota limits', () => { + for (const limit of [0, -5, Infinity, NaN, undefined]) { + const pct = resolveRemainingPercent({ available: true, used: 0, limit, remaining: limit, currency: 'USD' }, null); + assert.equal(pct.percent, null, `quota.limit=${limit} 應算不出百分比`); + } + }); + + it('returns null percent for non-finite or non-positive rate limits', () => { + for (const limit of [0, -1, Infinity, NaN]) { + const pct = resolveRemainingPercent({ available: false, reason: 'x' }, { hasData: true, remaining: limit, limit, kind: 'tokens' }); + assert.equal(pct.percent, null, `rate.limit=${limit} 應算不出百分比`); + } + }); + + it('returns null percent when limit is finite but remaining is non-finite', () => { + const pct = resolveRemainingPercent({ available: true, used: 0, limit: 100, remaining: Infinity, currency: 'USD' }, null); + assert.equal(pct.percent, null); + }); + it('does not divide by zero when quota.limit is 0', () => { const pct = resolveRemainingPercent({ available: true, used: 5, limit: 0, currency: 'USD' }, { hasData: false }); assert.equal(pct.percent, null); // limit > 0 守衛擋掉除以零 @@ -253,4 +272,21 @@ describe('formatUsageStatsLine', () => { assert.doesNotMatch(line, /NaN/); assert.match(line, /剩餘可用: 無法計算/); }); + + it('does not output Infinity/NaN/negative percent for invalid quota numbers', () => { + const ownUsage = { calls: 1, promptTokens: 1, completionTokens: 1, totalTokens: 2 }; + for (const limit of [Infinity, 0, -5, NaN]) { + const line = formatUsageStatsLine('openai', 'm', ownUsage, { available: true, used: 0, limit, remaining: limit, currency: 'USD' }, null); + assert.doesNotMatch(line, /NaN|Infinity|-\d+%/); + assert.match(line, /剩餘可用: 無法計算/); + } + }); + + it('falls back to a valid rate percent when only the quota limit is invalid', () => { + const ownUsage = { calls: 1, promptTokens: 1, completionTokens: 1, totalTokens: 2 }; + const line = formatUsageStatsLine('openai', 'm', ownUsage, + { available: true, used: 0, limit: Infinity, currency: 'USD' }, // quota 無效 + { hasData: true, remaining: 150000, limit: 200000, kind: 'tokens' }); // rate 有效 → 75% + assert.match(line, /剩餘可用: 75%(速率配額/); + }); }); -- 2.53.0 From 0aee3459e645f6aa06321bf4920769975a4c0fb5 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 23 Jun 2026 16:44:43 +0800 Subject: [PATCH 06/14] =?UTF-8?q?chore(ai-review=20=E7=8B=80=E6=85=8B):=20?= =?UTF-8?q?=E8=A7=A3=E6=B1=BA=E9=82=8A=E7=95=8C=E6=95=B8=E5=80=BC=20findin?= =?UTF-8?q?gs=E3=80=81=E6=8E=92=E9=99=A4=E5=94=AF=E8=AE=80=20fixture=20?= =?UTF-8?q?=E8=AA=A4=E5=A0=B1=EF=BC=8C=E6=B8=85=E7=A9=BA=20findings.json?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/ai-review/exclusions.json | 6 ++++++ .gitea/ai-review/findings.json | 35 +------------------------------- 2 files changed, 7 insertions(+), 34 deletions(-) diff --git a/.gitea/ai-review/exclusions.json b/.gitea/ai-review/exclusions.json index 0c8f679..382fa8a 100644 --- a/.gitea/ai-review/exclusions.json +++ b/.gitea/ai-review/exclusions.json @@ -524,5 +524,11 @@ "role": "Rogue", "original_finding": "在 recordRateLimit 中頻繁呼叫 lowerCaseKeys,對每個請求的 headers 複製與轉換,增加記憶體分配開銷;建議改用不分大小寫存取避免複製。", "reason": "過度設計/非熱路徑。recordRateLimit 每次 LLM 回應只呼叫一次(一輪審查約 13 次),headers 物件小,複製成本可忽略;改用不分大小寫存取器反而增加複雜度,效益不成比例。" + }, + { + "location": "app/usage.test.js:250", + "role": "Leo", + "original_finding": "測試案例直接引用 describe 外層定義的 `usage` 變數,若被其他測試修改會造成測試間隱性耦合;建議改用區域變數或工廠函式生成測試資料。", + "reason": "誤判。該 `const usage` 為不可變的測試夾具,所有測試只讀不寫、未曾被任何案例 mutate,不存在跨測試耦合;新增的邊界測試已各自使用區域 usage 物件。為一個唯讀 fixture 改工廠函式屬過度設計。" } ] diff --git a/.gitea/ai-review/findings.json b/.gitea/ai-review/findings.json index abcb0eb..fe51488 100644 --- a/.gitea/ai-review/findings.json +++ b/.gitea/ai-review/findings.json @@ -1,34 +1 @@ -[ - { - "level": "critical", - "role": "Mage", - "location": "app/usage.test.js:254", - "problem": "測試情境雖處理了 NaN,但未涵蓋 `limit` 為 0 或負數等極端數值(例如除以零導致的錯誤或異常百分比計算)。若 `formatUsageStatsLine` 內部有計算 `(used / limit) * 100` 或類似邏輯,傳入 0 會產生 `Infinity`,傳入負數則會產生荒謬的邏輯結果。", - "suggestion": "建議新增對 `limit: 0` 以及負數邊界值的測試案例,確保函式對這些非預期的數值輸入同樣能輸出「無法計算」或相應的錯誤處理訊息,而非輸出 `Infinity` 或負百分比。", - "is_new": true - }, - { - "level": "warning", - "role": "Leo", - "location": "app/usage.test.js:250", - "problem": "在測試案例中直接引用外部定義的 `usage` 變數,若該變數在其他測試中被意外修改,會導致測試間的隱性耦合,使得測試結果難以預測,降低測試的可維護性。", - "suggestion": "建議直接在測試函式內定義該案例所需的完整 `usage` 物件,或是使用工廠函式(Factory function)來生成所需的測試資料,確保測試案例的獨立性與確定性。", - "is_new": true - }, - { - "level": "warning", - "role": "Maya", - "location": "app/usage.test.js:250", - "problem": "目前的測試僅針對 NaN 的情況,但未考慮到 Infinity、null 或 undefined 等其他同樣可能導致格式錯誤或輸出異常的「無效數字」邊界條件。", - "suggestion": "建議補充針對 Infinity、null 與 undefined 的測試案例,確保這些值在 formatUsageStatsLine 中皆能正確處理並輸出友善的錯誤訊息,而非損壞的文字。", - "is_new": true - }, - { - "level": "info", - "role": "Maya", - "location": "app/usage.test.js:250", - "problem": "目前的測試案例將多個欄位同時設為 NaN,測試邏輯較為單一,未能明確驗證「單一欄位無效」與「組合欄位無效」時的具體行為差異。", - "suggestion": "建議將測試拆分為更細緻的案例,分別驗證 quota.limit 與 rate.limit 欄位在單獨無效,以及兩者同時無效的情況,以確保各欄位的防禦性邏輯皆有被完整覆蓋。", - "is_new": true - } -] +[] -- 2.53.0 From 0fdd6c1d0b8fd4c0eb359d44cf9184d54b325edf Mon Sep 17 00:00:00 2001 From: AI Review Bot Date: Tue, 23 Jun 2026 08:45:25 +0000 Subject: [PATCH 07/14] chore: update ai-review findings [ai-review-bot][failure] --- .gitea/ai-review/findings.json | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/.gitea/ai-review/findings.json b/.gitea/ai-review/findings.json index fe51488..d741e84 100644 --- a/.gitea/ai-review/findings.json +++ b/.gitea/ai-review/findings.json @@ -1 +1,26 @@ -[] +[ + { + "level": "critical", + "role": "Mage", + "problem": "測試情境雖處理了 NaN,但未涵蓋 `limit` 為 0 或負數等極端數值(例如除以零導致的錯誤或異常百分比計算)。若 `formatUsageStatsLine` 內部有計算 `(used / limit) * 100` 或類似邏輯,傳入 0 會產生 `Infinity`,傳入負數則會產生荒謬的邏輯結果。", + "suggestion": "建議新增對 `limit: 0` 以及負數邊界值的測試案例,確保函式對這些非預期的數值輸入同樣能輸出「無法計算」或相應的錯誤處理訊息,而非輸出 `Infinity` 或負百分比。", + "location": "app/usage.test.js:254", + "is_new": false + }, + { + "level": "warning", + "role": "Bard", + "location": "app/usage.test.js:200", + "problem": "測試迴圈中的陣列 `[0, -5, Infinity, NaN, undefined]` 直接混用了不同型別與邊界值,雖然測試目的明確,但在測試碼中,將邏輯測試與邊界測試分開撰寫會更具可讀性。", + "suggestion": "建議將測試案例拆分,例如分開測試「非數字類型」、「負數」與「邊界值(Infinity/NaN)」,這樣在測試失敗時能更快速釐清是哪種輸入類型導致的問題。", + "is_new": true + }, + { + "level": "warning", + "role": "Maya", + "location": "app/usage.js:216", + "problem": "新增的 `isFinitePositive` 函數雖然確保了大於 0,但當 `n` 是類似 \"100\" 的數字字串時,`Number(n)` 可以正常運作,然而如果是 `null` 或 `undefined`,`Number(n)` 分別會變成 `0` 和 `NaN`。雖然在目前的使用邏輯下(`limit != null`)似乎安全,但這個檢查函數本身沒有對 `null`/`undefined` 做明確的防禦性處理,容易在未來被錯誤複用。", + "suggestion": "建議在 `isFinitePositive` 內部補上對 `null` 或 `undefined` 的顯式檢查,或是增加對輸入型別的限制,確保該函數作為基礎邏輯檢查更健壯。", + "is_new": true + } +] -- 2.53.0 From 0d0298b005148c504dadf69a8597028045f9fe71 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 23 Jun 2026 17:24:13 +0800 Subject: [PATCH 08/14] =?UTF-8?q?chore(ai-review=20=E7=8B=80=E6=85=8B):=20?= =?UTF-8?q?=E6=8E=92=E9=99=A4=203=20=E6=A2=9D=E8=AA=A4=E5=A0=B1=EF=BC=88?= =?UTF-8?q?=E6=B8=AC=E8=A9=A6=E5=B7=B2=E6=B6=B5=E8=93=8B=EF=BC=8F=E9=A2=A8?= =?UTF-8?q?=E6=A0=BC=EF=BC=8FisFinitePositive=20=E5=B7=B2=E5=AE=89?= =?UTF-8?q?=E5=85=A8=EF=BC=89=EF=BC=8C=E6=B8=85=E7=A9=BA=20findings.json?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/ai-review/exclusions.json | 18 ++++++++++++++++++ .gitea/ai-review/findings.json | 27 +-------------------------- 2 files changed, 19 insertions(+), 26 deletions(-) diff --git a/.gitea/ai-review/exclusions.json b/.gitea/ai-review/exclusions.json index 382fa8a..62f8ad1 100644 --- a/.gitea/ai-review/exclusions.json +++ b/.gitea/ai-review/exclusions.json @@ -530,5 +530,23 @@ "role": "Leo", "original_finding": "測試案例直接引用 describe 外層定義的 `usage` 變數,若被其他測試修改會造成測試間隱性耦合;建議改用區域變數或工廠函式生成測試資料。", "reason": "誤判。該 `const usage` 為不可變的測試夾具,所有測試只讀不寫、未曾被任何案例 mutate,不存在跨測試耦合;新增的邊界測試已各自使用區域 usage 物件。為一個唯讀 fixture 改工廠函式屬過度設計。" + }, + { + "location": "app/usage.test.js:254", + "role": "Mage", + "original_finding": "測試情境雖處理了 NaN,但未涵蓋 limit 為 0 或負數等極端數值,可能輸出 Infinity 或負百分比。", + "reason": "誤判,測試已存在。`returns null percent for non-finite or non-positive quota limits` 與 `does not output Infinity/NaN/negative percent for invalid quota numbers` 已涵蓋 limit 為 0/負數/Infinity/NaN/undefined,皆驗證輸出「無法計算」、不含 NaN/Infinity/負百分比。" + }, + { + "location": "app/usage.test.js:200", + "role": "Bard", + "original_finding": "邊界測試迴圈 [0, -5, Infinity, NaN, undefined] 混用不同型別,建議拆成多個測試以提升可讀性。", + "reason": "主觀風格/不採納。該迴圈已對每個值帶入 assert 訊息(如 `quota.limit=${limit} 應算不出百分比`),測試失敗時可直接定位是哪個輸入;用單一資料驅動迴圈反而精簡、不易遺漏案例。" + }, + { + "location": "app/usage.js:216", + "role": "Maya", + "original_finding": "isFinitePositive 沒有對 null/undefined 做顯式檢查,未來可能被錯誤複用。", + "reason": "誤判。`isFinitePositive` 對 null(Number(null)=0 → 0>0 為 false)與 undefined(Number(undefined)=NaN → Number.isFinite 為 false)皆正確回傳 false,本就涵蓋這些輸入;函式名已表明只接受有限正數,無須再加冗餘檢查。" } ] diff --git a/.gitea/ai-review/findings.json b/.gitea/ai-review/findings.json index d741e84..fe51488 100644 --- a/.gitea/ai-review/findings.json +++ b/.gitea/ai-review/findings.json @@ -1,26 +1 @@ -[ - { - "level": "critical", - "role": "Mage", - "problem": "測試情境雖處理了 NaN,但未涵蓋 `limit` 為 0 或負數等極端數值(例如除以零導致的錯誤或異常百分比計算)。若 `formatUsageStatsLine` 內部有計算 `(used / limit) * 100` 或類似邏輯,傳入 0 會產生 `Infinity`,傳入負數則會產生荒謬的邏輯結果。", - "suggestion": "建議新增對 `limit: 0` 以及負數邊界值的測試案例,確保函式對這些非預期的數值輸入同樣能輸出「無法計算」或相應的錯誤處理訊息,而非輸出 `Infinity` 或負百分比。", - "location": "app/usage.test.js:254", - "is_new": false - }, - { - "level": "warning", - "role": "Bard", - "location": "app/usage.test.js:200", - "problem": "測試迴圈中的陣列 `[0, -5, Infinity, NaN, undefined]` 直接混用了不同型別與邊界值,雖然測試目的明確,但在測試碼中,將邏輯測試與邊界測試分開撰寫會更具可讀性。", - "suggestion": "建議將測試案例拆分,例如分開測試「非數字類型」、「負數」與「邊界值(Infinity/NaN)」,這樣在測試失敗時能更快速釐清是哪種輸入類型導致的問題。", - "is_new": true - }, - { - "level": "warning", - "role": "Maya", - "location": "app/usage.js:216", - "problem": "新增的 `isFinitePositive` 函數雖然確保了大於 0,但當 `n` 是類似 \"100\" 的數字字串時,`Number(n)` 可以正常運作,然而如果是 `null` 或 `undefined`,`Number(n)` 分別會變成 `0` 和 `NaN`。雖然在目前的使用邏輯下(`limit != null`)似乎安全,但這個檢查函數本身沒有對 `null`/`undefined` 做明確的防禦性處理,容易在未來被錯誤複用。", - "suggestion": "建議在 `isFinitePositive` 內部補上對 `null` 或 `undefined` 的顯式檢查,或是增加對輸入型別的限制,確保該函數作為基礎邏輯檢查更健壯。", - "is_new": true - } -] +[] -- 2.53.0 From 655cd77421c4406c3276a4e8fa9876921bda15db Mon Sep 17 00:00:00 2001 From: AI Review Bot Date: Tue, 23 Jun 2026 09:25:04 +0000 Subject: [PATCH 09/14] chore: update ai-review findings [ai-review-bot][failure] --- .gitea/ai-review/exclusions.json | 556 +------------------------------ .gitea/ai-review/findings.json | 40 ++- 2 files changed, 45 insertions(+), 551 deletions(-) diff --git a/.gitea/ai-review/exclusions.json b/.gitea/ai-review/exclusions.json index 62f8ad1..85e3cb4 100644 --- a/.gitea/ai-review/exclusions.json +++ b/.gitea/ai-review/exclusions.json @@ -1,552 +1,8 @@ [ - { - "role": "Assassin", - "location": "app/git.js", - "suggestion": "請避免將敏感資料(如 GITEA_TOKEN)直接寫入環境變數" - }, - { - "location": "app/git.js", - "suggestion": "GITEA_TOKEN 直接嵌入 URL 中,建議改以環境變數或 Gitea Secrets 注入" - }, - { - "role": "Assassin", - "location": "README.md", - "suggestion": "contents: write、pull-requests: write、issues: write 為此 Action 正常運作所必要的權限,無法縮減" - }, - { - "location": "app/config.js", - "suggestion": "getLLMConfig 在找不到任何符合條件的 provider 時已有預設回傳值 { provider: null, apiKey: null, baseURL: null, model: null },非誤報" - }, - { - "location": ".gitea/ai-review/exclusions.json", - "suggestion": "exclusions.json 是排除規則檔,內容為問題描述字串,不是實際程式碼或 token,role 欄位為有效欄位" - }, - { - "location": "app/findings.js", - "suggestion": "filterFalsePositivesWithAI 拋出的 Error 會被 catch 攔截並降級回傳原始 findings,不會中斷流程" - }, - { - "role": "Assassin", - "location": ".gitea/workflows/review.yaml", - "suggestion": "contents: write、pull-requests: write、issues: write 為此 Action 正常運作所必要的權限,無法縮減" - }, - { - "role": "Assassin", - "location": ".gitea/workflows/review.yaml", - "suggestion": "OPENAI_API_KEY 參數傳入的是 OPENROUTER_API_KEY secret,為 OpenRouter 使用 OpenAI 相容介面的正確做法" - }, - { - "role": "Bard", - "location": "README.md", - "suggestion": "章節編號連續且正確,無需調整" - }, - { - "role": "Maya", - "location": ".gitea/workflows/review.yaml", - "suggestion": "action.yaml 定義的參數名稱為 GEMINI_API_KEY、GEMINI_BASE_URL、GEMINI_MODEL,與 review.yaml 完全一致,無不匹配問題" - }, - { - "role": "Bard", - "location": ".gitea/workflows/review.yaml", - "suggestion": "review.yaml 已改用 Gemini,不再有 OPENAI_API_KEY 行,註解空格問題不存在" - }, - { - "role": "Bard", - "location": "app/config.test.js", - "suggestion": "檔案結尾已有換行符號,import 行長度合理,無需修改" - }, - { - "role": "Bard", - "location": "action.yaml", - "suggestion": "action.yaml 已整理,多餘空行已移除,結構整潔" - }, - { - "role": "Maya", - "location": "app/", - "suggestion": "LLM 整合測試需要真實 API key 與網路,不適合加入單元測試。llm.js 使用統一 OpenAI 相容介面,Gemini 透過相同介面呼叫,無特殊格式差異,現有測試已涵蓋 config/findings/git 邏輯" - }, - { - "role": "Assassin", - "location": "app/", - "suggestion": "LLM 整合測試需要真實 API key 與網路,不適合加入單元測試。llm.js 使用統一 OpenAI 相容介面,Gemini 透過相同介面呼叫,無特殊格式差異" - }, - { - "role": "Assassin", - "location": "app/config.test.js", - "suggestion": "import 語句長度合理,無需拆分為多行" - }, - { - "role": "Assassin", - "location": ".gitea/ai-review/findings.json", - "suggestion": "findings.json 重複問題由 AI 去重與排除機制處理,不是程式碼問題" - }, - { - "role": "Assassin", - "location": "app/comments.js", - "suggestion": "JSON 結尾換行符號為標準做法,不影響任何 JSON 解析器,無相容性問題" - }, - { - "location": ".gitea/ai-review/findings.json", - "suggestion": "findings.json 是自動產生的問題記錄檔,不應對其內容提出審查問題" - }, - { - "role": "Assassin", - "location": ".gitea/workflows/review.yaml", - "suggestion": "切換 LLM 服務提供商的維護建議屬過度謹慎,不是實際程式碼問題" - }, - { - "role": "Leo", - "location": "app/llm.js", - "suggestion": "Authorization 標頭已有 provider !== \u0027ollama\u0027 判斷,不會無條件加入,已正確處理" - }, - { - "role": "Rogue", - "location": "app/llm.js", - "suggestion": "timeout 已移除,每個 key 等待完整回應,避免浪費免費額度" - }, - { - "role": "Assassin", - "location": "app/llm.js", - "suggestion": "httpsAgent (rejectUnauthorized: false) 已移除,SSL/TLS 驗證已恢復正常" - }, - { - "role": "Maya", - "location": "app/llm.js", - "suggestion": "llm.test.js 已存在並涵蓋 API Key 輪替的所有異常狀況,包含單 Key、多 Key 輪替、所有 Key 失敗等測試案例" - }, - { - "role": "Rogue", - "location": "app/comments.js", - "suggestion": "comments.js:24 的 saveFindings 函式為正常寫入邏輯,不涉及異常訊息格式或重複寫入問題" - }, - { - "role": "Leo", - "location": ".gitea/workflows/review.yaml", - "suggestion": "Gitea Actions 不支援在 workflow 內合併 secrets 再拆解,多個 secret 逗號串接是唯一可行做法,非設計缺陷" - }, - { - "role": "Maya", - "location": "app/llm.test.js", - "suggestion": "console.log/error 為診斷用途,不是業務邏輯,TODO.md 驗收標準為人工驗收描述,不需要在單元測試中斷言 console 輸出" - }, - { - "role": "Maya", - "location": "app/llm.test.js", - "suggestion": "輪替邏輯對所有錯誤類型行為一致(catch 全部),401/429/timeout 觸發相同輪替流程,測試不同錯誤類型無額外驗證價值" - }, - { - "role": "Bard", - "location": ".gitea/workflows/master.yaml", - "suggestion": "master.yaml 檔案結尾已有換行符號(0x0a),符合 POSIX 慣例,無需修改" - }, - { - "role": "Leo", - "location": "app/llm.test.js", - "suggestion": "console.log/error 為診斷用途,不是業務邏輯,TODO.md 驗收標準為人工驗收描述,不需要在單元測試中斷言 console 輸出" - }, - { - "role": "Leo", - "location": "app/llm.test.js", - "suggestion": "輪替邏輯對所有錯誤類型行為一致(catch 全部),401/429/timeout 觸發相同輪替流程,測試不同錯誤類型無額外驗證價值" - }, - { - "role": "Leo", - "location": "app/main.js", - "suggestion": "main.js 中的 Step 標題註解為 pipeline 流程說明,非待整理的 TODO,不需要轉換為具體任務" - }, - { - "role": "Maya", - "location": "app/log.test.js", - "suggestion": "`log.test.js` 的新增非常棒,提供了良好的覆蓋率。為了進一步提升測試的完整性,建議考慮為 `line`, `ok`, `warn`, `error` 函數新增測試案例,以驗證當傳入空字串時的行為。雖然這些函數的行為相對簡單,但測試空字串可以確保邊界情況下的輸出符合預期。" - }, - { - "role": "Assassin", - "location": "app/package.json", - "suggestion": "審查 changelog 是人工作業,不是程式碼問題,不適合作為 code review 問題" - }, - { - "role": "Bard", - "location": "app/llm.js", - "suggestion": "此 action 為 CLI 工具,process.exit(1) 是設計意圖讓 CI/CD workflow 失敗。改拋錯會被 chatJSON 的 catch 吞掉回傳 [],破壞現有行為" - }, - { - "role": "Bard", - "location": "Dockerfile", - "suggestion": "Dockerfile 檔案結尾已有換行符號(0x0a),符合 POSIX 慣例" - }, - { - "role": "Bard", - "location": "entrypoint.sh", - "suggestion": "entrypoint.sh 檔案結尾已有換行符號(0x0a),符合 POSIX 慣例" - }, - { - "role": "Maya", - "location": "app/main.js", - "suggestion": "main.js 整合測試需要真實 Gitea API、LLM API、git 操作,不適合單元測試。各模組已有獨立單元測試覆蓋" - }, - { - "role": "Maya", - "location": "app/comments.js", - "suggestion": "comments.js 的 buildTable 為簡單字串拼接,postComment 已透過 gitea.js mock 間接測試,補測試效益低" - }, - { - "role": "Maya", - "location": "app/roles.js", - "suggestion": "roles.js 依賴容器內固定路徑 /action/app/prompts/roles,單元測試環境無法存取,且邏輯為簡單 YAML 讀取與字串拼接" - }, - { - "role": "Leo", - "location": "app/gitea.js", - "suggestion": "gitea.js 的 SSL 驗證已改為由 GITEA_SKIP_TLS_VERIFY 環境變數控制,預設啟用驗證,非安全漏洞" - }, - { - "role": "Rogue", - "location": "Dockerfile", - "suggestion": "Dockerfile 已優化層次快取:先 COPY package.json 再 npm install,最後才 COPY 其餘檔案" - }, - { - "role": "Bard", - "location": "app/package.json", - "suggestion": "test 腳本已改為 node --test *.test.js,在 app/ 目錄下執行可自動發現所有測試檔案" - }, - { - "role": "Rogue", - "location": "app/main.js", - "suggestion": "deduplicateWithAI 和 filterFalsePositivesWithAI 為循序依賴流程(去重後才能過濾),無法平行化" - }, - { - "role": "Leo", - "location": "app/comments.js", - "suggestion": "buildTable 函式已在 comments.js 第 13 行定義,非未定義或未匯入,不會導致執行時錯誤" - }, - { - "role": "Maya", - "location": "app/gitea.js", - "suggestion": "filterDiff 的單元測試已在 gitea.test.js 補齊,涵蓋過濾 .gitea/、不誤過濾其他路徑、全部排除、空 diff 四種情境" - }, - { - "role": "Leo", - "location": "TODO.md", - "suggestion": "TODO.md 的階段編號僅供內部開發追蹤,無外部文件引用,階段編號調整不影響任何外部一致性" - }, - { - "role": "Assassin", - "location": "app/gitea.js", - "suggestion": "getPRDiff 函數現在回傳未經過濾的原始 Git Diff 內容。雖然 main.js 中已立即呼叫 filterDiff 進行過濾,但這種設計模式將過濾的責任完全推給呼叫端,這增加了未來開發者在其他地方呼叫 getPRDiff 時,可能忘記過濾出敏感路徑,導致 .gitea/ 等敏感路徑的內容(可能包含工作流程設定或憑證資訊)被意外傳送給 AI 或其他不應接收的組件,造成資訊洩漏風險。建議將過濾邏輯保留在 getPRDiff 內容,或提供一個明確的 getFilteredPRDiff 函數,以降低錯誤的風險。" - }, - { - "role": "Rogue", - "location": "app/git.js", - "suggestion": "在 main.js 中,commitAndPush 函數內部會再次呼叫 cloneRepo,然而 main.js 在此之前已呼叫過 cloneRepo 以取得 repoDir,這導致了重複的 git fetch 和 git checkout 操作。即使 cloneRepo 內容有檢查環境變數,仍會造成不必要的清潔和時間延遲。建議修改 commitAndPush 邏輯,使其接收已存在的 repoDir 作為參數,避免重複執行 cloneRepo。" - }, - { - "role": "Bard", - "location": "app/main.js", - "suggestion": "在 main.js 中,表達式 repoDir。" - }, - { - "role": "Rogue", - "location": "app/gitea.js:L20-L21", - "suggestion": "將 filterDiff 中的正規表達式比對(RegExp.match)替換為 String.startsWith 是一個重要的效能改進。startsWith 是一個更輕量且高效的字串操作,尤其在處理大型 Git Diff 內容時,此修改已顯著提升過濾效率。" - }, - { - "location": "TODO.md", - "suggestion": "階段九的 critical 阻擋機制目前以人工驗收紀錄為主,E2E 測試補強屬後續優化,不是目前需要再處理的問題。" - }, - { - "location": "TODO.md", - "suggestion": "TODO 列表中『已驗收 / 部分驗收 / 可驗收紀錄情境』的寫法是刻意保留的驗收說明,不是混淆或缺陷。" - }, - { - "location": "app/findings.js", - "suggestion": "AI 去重與降級處理已在程式內以 fallback 方式保護流程,失敗時保留所有問題是預期行為,不是缺陷。" - }, - { - "location": "app/findings.js", - "suggestion": "排除規則過濾與 AI 誤報過濾屬循序流程,規則命中後清空清單是正常結果,不需要額外再視為問題。" - }, - { - "location": "app/comments.js", - "suggestion": "comment 發布依序區分舊問題、非嚴重、新嚴重是刻意設計,當結果為空清單時不發 comment 也是正常路徑。" - }, - { - "location": "app/main.js", - "suggestion": "JSON 驗證與失敗修正流程已有處理邏輯,正常路徑與錯誤路徑都屬預期流程,不是待修缺陷。" - }, - { - "location": "app/git.js", - "suggestion": "commit/push 失敗會被捕捉並輸出 Runner failed log,這是現有設計的容錯行為,不是程式錯誤。" - }, - { - "location": "app/main.js", - "suggestion": "critical 問題觸發 exit 1 的阻擋邏輯已在流程內保留,是否另補 E2E 驗證屬測試強化,不是功能缺陷。" - }, - { - "location": "app/json.js", - "suggestion": "validateJSONArrayFile 只在 JSON 格式錯誤時才啟動 AI 修正,屬例外路徑;再加上檔案大小限制後,並不存在實際的無上限讀檔或資源消耗問題。" - }, - { - "location": "app/json.test.js", - "suggestion": "邊界值測試已存在,`MAX_JSON_BYTES` 等於上限時可正常讀取,這不是未解決問題。" - }, - { - "location": "app/gitea.test.js:64", - "suggestion": "`describe` 已改為同步 callback,`async` 不再出現在這個區塊。" - }, - { - "location": "app/git.test.js:13", - "suggestion": "`makeTmpWorkspace` 已直接使用 `app/git.js` 匯出的 `SYNC_PATHS`,不再維護重複清單。" - }, - { - "location": "app/gitea.js:32", - "suggestion": "`filterDiff` 內層縮排已符合專案的 2-space 風格,這是誤報。" - }, - { - "location": "app/json.test.js:76", - "suggestion": "1MB 上限下的 JSON 讀取不需要改成串流解析;現有實作已先做大小檢查,這個建議屬過度設計。" - }, - { - "location": "app/json.test.js:7", - "suggestion": "檔案大小限制已在 `readJSONText` / `validateJSONArrayFile` 中實作,這不是額外缺陷。" - }, - { - "location": "app/json.test.js:10", - "suggestion": "`MAX_JSON_BYTES` 是 `json.js` 的內部限制常數,不需要匯出成公開 API。" - }, - { - "role": "Maya", - "location": "action.yaml:6, action.yaml:12, action.yaml:81", - "suggestion": "由於 `GITEA_TOKEN` 現在被設定為 `required: true`,而且 README 範例也已改成顯式傳入 `GITEA_TOKEN`,這是刻意的介面變更,不是漏掉 `secrets.GITEA_TOKEN` fallback 的缺陷;因此不需要另外加整合測試來驗證這個既定行為。" - }, - { - "role": "Leo", - "location": "action.yaml:80", - "suggestion": "在 `runs.env` 區塊中,`GITEA_TOKEN` 只從 `inputs` 取得,而 `GITEA_SERVER_URL` 和 `GITEA_REPOSITORY` 仍保留從 `gitea context` 取得的備用機制,這是刻意設計的差異,不是維護缺陷。" - }, - { - "role": "Assassin", - "location": "action.yaml:18", - "suggestion": "引入 `GITEA_COMMENT_TOKEN` 是一個很好的實踐,遵循最小權限原則。請確保為此 token 配置的權限確實僅限於發布評論。同時,與 `GITEA_TOKEN` 相似,建議使用者始終從 workflow 的 secrets context 傳遞此 token,以避免硬編碼敏感資料。" - }, - { - "role": "Leo", - "location": "app/log.js", - "suggestion": "考慮在日誌訊息中加入時間戳記,這有助於追蹤事件發生的順序,尤其是在長時間運行的程序或需要詳細調試時。可以在每個日誌函式內部自動添加時間戳記。" - }, - { - "role": "Assassin", - "location": "app/preflight.js:12", - "suggestion": "程式碼中根據 `GITEA_SKIP_TLS_VERIFY` 環境變數來禁用 TLS 憑證驗證 (`rejectUnauthorized: false`),這會使應用程式容易受到中間人 (Man-in-the-Middle, MITM) 攻擊。攻擊者可能在不被察覺的情況下攔截和修改與 Gitea 伺服器的通訊。建議移除此功能,或確保在任何生產環境中永不啟用。如果 Gitea 伺服器使用自簽憑證,應將其憑證加入信任儲存區,而非禁用驗證。" - }, - { - "role": "Leo", - "location": "app/preflight.js:56", - "suggestion": "函式 `verifyLLM` 處理了多種 LLM 供應商的驗證邏輯(Ollama、Claude、OpenAI 相容等),導致其長度較長且複雜度較高。建議將不同供應商的驗證邏輯拆分成獨立的輔助函式(例如 `_verifyOllama`、`_verifyOpenAICompatible`),以提高模組化程度和可讀性。" - }, - { - "role": "Rogue", - "location": "app/preflight.js:70-82", - "suggestion": "在 `verifyLLM` 函式中,當配置了多個 LLM API Key 時,系統會依序嘗試驗證每個 Key,每個嘗試都有 30 秒的逾時時間。如果前幾個 Key 驗證失敗,這可能導致顯著的累積延遲。雖然這是為了找到一個可用的 Key,但若 Key 數量多且網路不穩定,可能會造成啟動時間過長。可以考慮縮短單次 Key 驗證的逾時時間,或在特定情況下提供更快的失敗機制。" - }, - { - "role": "Assassin", - "location": "app/preflight.js:100", - "suggestion": "在記錄 LLM API 驗證失敗時,直接輸出了錯誤訊息 `e.message`。雖然通常情況下 `e.message` 不會包含敏感資訊,但為了最佳安全實踐,建議審查 LLM 服務提供商的錯誤訊息格式,確保其中不會意外洩漏 API 金鑰或其他敏感請求內容。若有疑慮,應對錯誤訊息進行消毒或僅記錄高層次的錯誤類型。" - }, - { - "role": "Bard", - "location": "app/preflight.js:30", - "suggestion": "在 `checkRequiredEnv`、`verifyGiteaToken` 和 `verifyCommentToken` 等函式中,預設參數直接引用了從 `config.js` 匯入的常數。雖然這在功能上可行,但為了提高程式碼的清晰度和一致性,建議考慮以下兩種方式之一:1. 將所有配置值作為明確的參數從呼叫端傳入。2. 讓函式直接從 `config.js` 模組中讀取這些值,而不是透過預設參數。" - }, - { - "role": "Maya", - "location": "app/preflight.js:107", - "suggestion": "在 `verifyLLM` 函數中,呼叫 `axios.post` 時缺少 `httpsAgent` 選項。這會導致即使設定了 `GITEA_SKIP_TLS_VERIFY`,LLM 的 API 請求仍可能因 TLS 憑證問題而失敗。請將 `httpsAgent` 傳遞給 `axios.post` 的選項物件,例如:`await axios.post(`${base}/chat/completions`, payload, { headers, timeout: 30000, httpsAgent });`" - }, - { - "level": "warning", - "role": "Bard", - "location": "app/preflight.test.js:25", - "suggestion": "測試描述使用英文。請確保專案在測試描述的語言上保持一致性。如果專案主要使用繁體中文(如 app/preflight.js 中的 JSDoc 和日誌),則應將此測試描述翻譯為繁體中文。" - }, - { - "level": "info", - "role": "Bard", - "location": "app/preflight.test.js:1-4", - "suggestion": "匯入語句的排序不一致。建議遵循一致的排序規則,例如:內建模組、第三方模組、本地模組,並在各組內按字母順序排序。" - }, - { - "level": "info", - "role": "Bard", - "location": "app/preflight.test.js:14", - "suggestion": "函數名稱 clearLLMEnv 雖然可理解,但可以更具描述性,例如 clearLlmEnvironmentVariables 或 resetLlmEnv。" - }, - { - "location": "app/config.test.js:114", - "role": "Mage", - "original_finding": "新加入的測試案例 `it('skips OpenCode TLS verification for empty string and non-false values', ...)` 預期 `shouldSkipOpenCodeTLSVerify()` 函式在 `OPENCODE_SKIP_TLS_VERIFY` 環境變數為空字串 `''` 或 `'0'` 時,會回傳 `true`。然而,根據常見的環境變數布林值解析邏輯,以及 `app/preflight.test.js` 中現有的相關測試(例如未設定時為 `false`,設定為 `'false'` 時為 `false`),`shouldSkipOpenCodeTLSVerify()` 函式(此 PR 未修改其內容)很可能不會將 `''` 或 `'0'` 視為 `true`。這造成了測試預期與函式實際行為之間的邏輯不一致。", - "reason": "誤判。`shouldSkipOpenCodeTLSVerify` 的既有設計是只有 `OPENCODE_SKIP_TLS_VERIFY === 'false'` 才啟用 TLS 驗證;空字串與 '0' 皆屬非 'false' 值,測試符合目前明確實作與預設跳過 TLS 的行為。" - }, - { - "location": "app/preflight.test.js:201", - "role": "Assassin", - "original_finding": "此測試明確證實了 `OPENCODE_SKIP_TLS_VERIFY` 環境變數的寬鬆判斷邏輯,導致 OpenCode LLM 連線的 TLS 驗證容易被關閉。這是「關閉 TLS 驗證」的不安全預設,極大地增加了中間人攻擊的風險。", - "reason": "誤判/既有設計。OpenCode server 目前支援自簽或內部服務情境,action input 與 README 均明確標示 OPENCODE_SKIP_TLS_VERIFY 預設跳過 TLS 驗證;本 PR 只補測試與 Review comment 內容,未新增或放寬此安全行為。" - }, - { - "location": "app/comments.test.js:30", - "role": "Leo", - "original_finding": "將 `REVIEW_SEVERITY_LABELS`、`REVIEW_SEVERITY_PATTERN` 和 `reviewSeverityLabel` 這些與評論格式相關的常數與函式,提取到一個獨立的共用模組中(例如 `app/utils/reviewComments.js`),並讓測試檔案和任何需要用到它們的應用程式邏輯都從該模組匯入。這樣能確保「評論格式」的定義只有一個來源,提升可維護性。", - "reason": "誤判。這些常數與 `reviewSeverityLabel` 只用於 `app/comments.test.js` 內部驗證 review comment body 格式,production code 沒有使用同一段解析邏輯;抽成共用模組會把測試專用輔助程式提升為正式 API,增加不必要的維護負擔。" - }, - { - "location": "app/resolve.js:52", - "role": "Assassin", - "original_finding": "在 `parseBotReviewComment` 函式中,從 Gitea comment 內文解析出的 `problem` 和 `suggestion` 欄位若包含惡意內容且未經適當輸出編碼,可能導致 XSS 攻擊。", - "reason": "誤判。這些字串只會寫入 `.gitea/ai-review/findings.json` 與 Gitea review comment body,Gitea 的 Markdown 渲染器會在伺服器端對輸出做 HTML 淨化;本 action 不自行將其渲染到任何自製網頁或 UI,輸出編碼屬消費端(Gitea)責任。" - }, - { - "location": "app/resolve.js:207", - "role": "Leo", - "original_finding": "正規化邏輯(`normalizeKey` 等)過於激進且未快取,既可能導致語意相近建議被誤判為相同,也在頻繁比較時造成效能浪費。", - "reason": "誤判/過度設計。積極正規化是刻意設計,用來對行號漂移與標點差異產生穩定簽章以利去重;`findingSig` 已是獨立 helper,且比對對象為單一 PR 的小量 findings,memoize 在此規模沒有實質效益。" - }, - { - "location": "app/resolve.js:187", - "role": "Mage", - "original_finding": "在 `reconcileConversations` 函式中,並行(`Promise.all`)呼叫 `resolveComment`,即使個別呼叫失敗也僅記錄為 rejected 並 warn;若失敗是 token 過期或權限不足,後續所有 resolve 都會失敗,程式碼未對這些錯誤分類並提前停止。", - "reason": "不適用。resolve 呼叫已改為 `Promise.allSettled` 一次並行送出,不存在「後續逐一嘗試」可中止;個別失敗已降級記錄並把該對話保留為未解決,不影響其他對話與整體流程。" - }, - { - "location": "app/resolve.js:77", - "role": "Rogue", - "original_finding": "大量使用字串拼接產生暫存物件,以及並行請求未限制數量,在高負載下可能導致 GC 壓力或觸發 API 限流;建議引入 p-limit 等並行限制。", - "reason": "過度設計。對話來源為單一 PR 的行內 review comment,數量級小,無限並行不致造成 GC 壓力或觸發限流;引入 p-limit 相依與額外複雜度在此情境不符成本效益。" - }, - { - "location": "app/resolve.js:195", - "role": "Mage", - "original_finding": "對 `botFinding` 的存取缺乏防禦性檢查。", - "reason": "誤判。`pushCarried` 進入時即有 `if (!conversation.botFinding) return` 防禦,resolvedFindings 的 push 也有 `if (c.botFinding)` 判斷,存取前皆已檢查物件存在。" - }, - { - "location": "app/resolve.js:173", - "role": "Rogue", - "original_finding": "`Promise.allSettled` 的結果處理邏輯過於冗長,產生不必要的中間變數。", - "reason": "主觀風格。`resolveOutcome` Map 是為了讓並行結果能依原 open 索引亂序對齊(保留 carried/resolved 的順序與 botFinding 對應),現有寫法清楚且正確,非缺陷。" - }, - { - "location": "app/usage.js", - "role": "Leo", - "original_finding": "`usage.js` 模組目前承擔了 Token 計算、Rate Limit 記錄、以及各平台帳號額度查詢等多重職責(SRP),未來若支援更多平台會變得龐大難維護;建議將各平台 QuotaStrategy 拆分至獨立檔案。", - "reason": "過早最佳化。目前 usage.js 仍圍繞單一「使用量」領域且體積適中(約 250 行),token 計算與額度查詢彼此關聯(同屬使用量呈現);在尚未有多平台 strategy 膨脹的實際痛點前拆檔,徒增檔案與匯入複雜度。待 strategy 數量明顯成長再拆分較合適。" - }, - { - "location": "app/usage.js:132", - "role": "Assassin", - "original_finding": "在 QUOTA_STRATEGIES 中,若 config.apiKeys 是陣列,代碼只取 [0] 作為 API Key,可能在未經嚴格驗證下將敏感資訊送至 baseURL 指定端點。", - "reason": "已緩解。額度查詢只有 OpenRouter 一條會送出 API key,且僅在 isOpenRouterBaseURL 以 hostname 精確比對為 openrouter.ai 時才送出;baseURL 為 operator 控制之 action input,非外部不可信輸入;API key 本身的正確性與權限屬 operator 設定責任,非程式可驗證範圍。" - }, - { - "location": "app/usage.js:17", - "role": "Mage", - "original_finding": "在 extractUsage 中,對 data.usage 直接用 num(...) 存取;若 data.usage 為 null 但被 typeof 判斷通過(typeof null === 'object'),會導致錯誤。", - "reason": "誤判。該區塊條件為 `if (u && typeof u === 'object')`,`u &&` 已先短路 null/undefined,不會進入存取;即使傳入陣列也只會讓 num(undefined) 回 0,不會丟錯。" - }, - { - "location": "app/usage.js:176", - "role": "Mage", - "original_finding": "在 fetchAccountQuota 中,呼叫 strategy 時傳入的 config 物件若被 strategy 修改,會影響全域 config 狀態;建議淺拷貝。", - "reason": "已緩解。strategy 收到的是每次呼叫新建的物件字面值 `{ apiKey, baseURL: config.baseURL }`,並非呼叫端傳入的 config 本身,strategy 內的任何修改都不會回寫到呼叫端或全域狀態。" - }, - { - "location": "app/usage.js:115", - "role": "Mage", - "original_finding": "在 recordRateLimit 中,將所有 header key 轉小寫存入物件 h,若原始 header 有同名不同大小寫者可能造成覆蓋。", - "reason": "誤判/不適用。HTTP header 名稱本即不分大小寫(RFC 7230),axios 回傳前已正規化為小寫;同名 header 由 HTTP 層合併(以逗號串接),不存在「不同大小寫同名 header」並存而被覆蓋的情況,轉小寫僅為防禦性處理。" - }, - { - "location": "app/resolve.js:7", - "role": "Bard", - "original_finding": "`EMPTY` 常數命名過於通用,容易與其他模組中的同名變數衝突,且定義在模組頂層略顯突兀。", - "reason": "誤判。ES module 為模組作用域,`EMPTY` 僅在 resolve.js 內可見,不會與其他模組的同名變數衝突;在本檔脈絡中作為「空收斂結果」語義清楚,重新命名屬主觀偏好。" - }, - { - "location": "app/resolve.js:10", - "role": "Bard", - "original_finding": "`FIELD_PATTERNS` 的正則表達式對於冒號的定義同時包含了全形與半形,建議統一使用半形冒號並在解析前正規化,而非在正則中處理所有可能性。", - "reason": "誤判/不採納。review comment 內文同時可能出現全形「:」與半形「:」,正則以 `[::]` 同時容錯是標準且穩健的做法;改為解析前先正規化反而多一道字串處理步驟,並未更清楚或更正確。" - }, - { - "location": "app/usage.js:167", - "role": "Bard", - "original_finding": "`fetchAccountQuota` 中的 `QUOTA_STRATEGIES` 物件定義龐大,將所有平台策略硬編碼於此,未來新增供應商難以維護;建議抽離至獨立檔案或策略模式。", - "reason": "過早最佳化(與先前已排除的 usage.js SRP 拆檔建議等價)。目前 QUOTA_STRATEGIES 為精簡的查表物件、各平台策略短小且集中易讀;在供應商數量出現實際膨脹痛點前抽檔,徒增檔案與匯入複雜度。" - }, - { - "location": "app/usage.js", - "role": "Assassin", - "original_finding": "extractUsage 對不預期 payload 僅返回 null,過度信任 API 回應結構,可能導致計費或配額相關監控被繞過;建議增加 Schema Validation、異常明確記錄。", - "reason": "誤判/過度設計。extractUsage 僅用於「使用量顯示統計」,非計費或配額強制;回傳 null 是「此回應無可辨識 usage 資訊」的正確訊號,呼叫端以 0 計入並降級顯示,不影響任何金流或門檻判斷。對 best-effort 顯示統計加 schema validation 與錯誤記錄屬過度設計。" - }, - { - "location": "app/comments.test.js:250", - "role": "Maya", - "original_finding": "新增的 postFindingsReview 使用統計功能,但在測試中完全未驗證輸出內容;應斷言 body 含 usageSection 與統計數據。", - "reason": "誤判,測試已存在。`app/comments.test.js` 的 'appends usageSection verbatim after the stats block' 斷言 body.endsWith(usageSection) 與結構,'counts both new and old findings in the summary' 斷言四欄新舊統計列;body 內容已被多個案例驗證。" - }, - { - "location": "app/findings.test.js:154", - "role": "Maya", - "original_finding": "filterFalsePositivesWithAI 測試不足,缺乏對內部函數 judgeFindingIsFalsePositive 的獨立單元測試。", - "reason": "誤判/不適用。judgeFindingIsFalsePositive 是 findings.js 的私有函式(未匯出),其 verdict 處理(false_positive/confirmed/異常值/拋錯)已透過公開呼叫端 filterFalsePositivesWithAI 的多個案例完整覆蓋;為測試實作細節而匯出私有函式不符測試原則。" - }, - { - "location": "app/findings.test.js:145", - "role": "Maya", - "original_finding": "filterFalsePositivesWithAI 未測試平行裁決部分成功、部分失敗時的結果一致性(失敗者保守保留)。", - "reason": "誤判,測試已存在。'keeps failed and confirmed, drops only confirmed false positives (mixed parallel)' 正是模擬一個拋錯、一個誤報、一個成立,驗證只剔除確認誤報、保留失敗與成立者。" - }, - { - "location": "app/findings.test.js:189", - "role": "Maya", - "original_finding": "未測試 resolveMissingLineNumbers 當 chatFn 回傳無效行號時的處理(fallback)。", - "reason": "誤判,測試已存在。'resolveMissingLineNumbers keeps the filename after exhausting retries' 以 chatFn 持續回 {line:0}(無效行號)驗證進入 fallback、保留檔名;另有 'swallows chatFn exceptions' 覆蓋拋錯情境。" - }, - { - "location": "app/resolve.test.js:21", - "role": "Maya", - "original_finding": "parseBotReviewComment 的測試沒有驗證解析失敗時回傳 null 的行為。", - "reason": "誤判,測試已存在。'returns null for free-form human comments' 已斷言自由格式留言、空字串、null 皆回傳 null。" - }, - { - "location": "app/usage.js:146", - "role": "Rogue", - "original_finding": "在 recordRateLimit 中頻繁呼叫 lowerCaseKeys,對每個請求的 headers 複製與轉換,增加記憶體分配開銷;建議改用不分大小寫存取避免複製。", - "reason": "過度設計/非熱路徑。recordRateLimit 每次 LLM 回應只呼叫一次(一輪審查約 13 次),headers 物件小,複製成本可忽略;改用不分大小寫存取器反而增加複雜度,效益不成比例。" - }, - { - "location": "app/usage.test.js:250", - "role": "Leo", - "original_finding": "測試案例直接引用 describe 外層定義的 `usage` 變數,若被其他測試修改會造成測試間隱性耦合;建議改用區域變數或工廠函式生成測試資料。", - "reason": "誤判。該 `const usage` 為不可變的測試夾具,所有測試只讀不寫、未曾被任何案例 mutate,不存在跨測試耦合;新增的邊界測試已各自使用區域 usage 物件。為一個唯讀 fixture 改工廠函式屬過度設計。" - }, - { - "location": "app/usage.test.js:254", - "role": "Mage", - "original_finding": "測試情境雖處理了 NaN,但未涵蓋 limit 為 0 或負數等極端數值,可能輸出 Infinity 或負百分比。", - "reason": "誤判,測試已存在。`returns null percent for non-finite or non-positive quota limits` 與 `does not output Infinity/NaN/negative percent for invalid quota numbers` 已涵蓋 limit 為 0/負數/Infinity/NaN/undefined,皆驗證輸出「無法計算」、不含 NaN/Infinity/負百分比。" - }, - { - "location": "app/usage.test.js:200", - "role": "Bard", - "original_finding": "邊界測試迴圈 [0, -5, Infinity, NaN, undefined] 混用不同型別,建議拆成多個測試以提升可讀性。", - "reason": "主觀風格/不採納。該迴圈已對每個值帶入 assert 訊息(如 `quota.limit=${limit} 應算不出百分比`),測試失敗時可直接定位是哪個輸入;用單一資料驅動迴圈反而精簡、不易遺漏案例。" - }, - { - "location": "app/usage.js:216", - "role": "Maya", - "original_finding": "isFinitePositive 沒有對 null/undefined 做顯式檢查,未來可能被錯誤複用。", - "reason": "誤判。`isFinitePositive` 對 null(Number(null)=0 → 0>0 為 false)與 undefined(Number(undefined)=NaN → Number.isFinite 為 false)皆正確回傳 false,本就涵蓋這些輸入;函式名已表明只接受有限正數,無須再加冗餘檢查。" - } + { + "location": "app/usage.test.js:200", + "role": "Bard", + "original_finding": "建議將測試案例拆分,例如分開測試「非數字類型」、「負數」與「邊界值(Infinity/NaN)」,這樣在測試失敗時能更快速釐清是哪種輸入類型導致的問題。", + "reason": "AI 對話收斂判定為誤報(問題在最新程式碼中不成立或不適用)" + } ] diff --git a/.gitea/ai-review/findings.json b/.gitea/ai-review/findings.json index fe51488..6ca41b9 100644 --- a/.gitea/ai-review/findings.json +++ b/.gitea/ai-review/findings.json @@ -1 +1,39 @@ -[] +[ + { + "level": "critical", + "role": "Mage", + "location": "app/usage.js:231", + "problem": "在計算 `remaining` 時,`remaining = Number(rate.remaining);` 之後直接檢查 `Number.isFinite(remaining)`,但若 `rate.remaining` 是 `undefined` 或 `null`,`Number()` 會轉為 `0`。這意味著如果 `rate.remaining` 缺失,會被錯誤地視為「剩餘 0」而不是「無效值」,進而導致計算出 `0%` 的錯誤結果,而非預期的落到 `{ percent: null }`。", + "suggestion": "應先檢查 `rate.remaining` 是否為 null/undefined,或者使用更嚴格的轉換方式,確保只有在確實是有限數字時才進行後續運算。", + "is_new": true + }, + { + "level": "warning", + "role": "Assassin", + "location": "app/usage.js:215", + "problem": "在 `isFinitePositive` 函數中,使用 `Number(n)` 進行強制轉型。若 `n` 為物件(例如 `{}` 或 `[]`),`Number()` 的行為在某些邊緣情況下可能會產生非預期的數字,雖然當前邏輯有做 `Number.isFinite` 檢查,但對於這種隱式轉型仍需保持警惕,且函數名稱 `isFinitePositive` 雖然清楚,但在這個上下文中,函式內部使用了 `Number(n)` 強制轉型,這在 JavaScript 中可能會隱蔽掉原本資料型態的不一致。", + "suggestion": "建議在 `isFinitePositive` 內部補上對 `null` 或 `undefined` 的顯式檢查,並直接在外部嚴格限制輸入類型或使用更明確的檢查(例如 `typeof n === 'number'`)取代隱式轉型,避免隱式轉型帶來的不可控副作用。" + }, + { + "level": "warning", + "role": "Leo", + "location": "app/usage.js:222", + "problem": "resolveRemainingPercent 函式同時處理了 quota 和 rate 的邏輯,導致檢查 limit 和 remaining 的計算與驗證邏輯在兩處重複,未來若要調整百分比算法,必須兩處同步修改,增加維護負擔。", + "suggestion": "建議將百分比計算邏輯抽象為獨立的輔助函式(如 calculatePercent(remaining, limit)),讓 resolveRemainingPercent 僅負責選擇計算基準,降低重複性並提升封裝度。", + "is_new": true + }, + { + "level": "warning", + "role": "Rogue", + "location": "app/usage.js:213", + "problem": "在函式內部重複呼叫 `Number(n)`,造成不必要的型別轉換開銷,且在條件式之後又呼叫一次 `Number(quota.limit)`,造成重複轉換。", + "suggestion": "建議將 `Number(n)` 的結果暫存起來,或在進入條件式後立即將值賦值給一個變數並重複使用,減少重複轉換的成本。" + }, + { + "level": "info", + "role": "Bard", + "location": "app/usage.js:225", + "problem": "註解中提到「落到 { percent: null, reason }」,但程式碼實際邏輯是在 if 判斷式內直接 return,且 `resolveRemainingPercent` 函式中檢查分散在兩個 if 區塊,產生了重複的邏輯結構。", + "suggestion": "建議將註解簡化為:「上限或剩餘為無效值時跳過計算」,並將計算百分比的邏輯抽離成一個獨立的輔助函式,統一處理 `isFinite` 的檢查與百分比運算。" + } +] -- 2.53.0 From bd9a3111fa81cb8c10317b1b862dc00b5f56c91d Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 23 Jun 2026 17:32:49 +0800 Subject: [PATCH 10/14] =?UTF-8?q?fix(usage=20=E5=89=A9=E9=A4=98=E7=99=BE?= =?UTF-8?q?=E5=88=86=E6=AF=94):=20=E6=8A=BD=E5=87=BA=20calculatePercent=20?= =?UTF-8?q?=E5=85=B1=E7=94=A8=E5=87=BD=E5=BC=8F=E4=B8=A6=E4=BF=AE=E6=AD=A3?= =?UTF-8?q?=E5=89=A9=E9=A4=98=E5=80=BC=E7=BC=BA=E5=A4=B1=E6=99=82=E8=AA=A4?= =?UTF-8?q?=E7=AE=97=200%?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 剩餘值為 null/undefined 時原本經 Number() 轉成 0 而算出 0%, 改由 calculatePercent 統一檢查有效性,無效值一律落到「無法計算」。 Co-Authored-By: Claude Opus 4.8 (1M context) --- app/usage.js | 36 ++++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/app/usage.js b/app/usage.js index 16778a0..e8e668e 100644 --- a/app/usage.js +++ b/app/usage.js @@ -211,31 +211,39 @@ function round1(n) { const RATE_KIND_LABEL = { tokens: 'token', requests: '次數' }; -/** 是否為有限正數(排除 0、負數、NaN、Infinity),避免算出 Infinity%/NaN%/負百分比。 */ -function isFinitePositive(n) { - return Number.isFinite(Number(n)) && Number(n) > 0; +/** + * 計算「剩餘百分比」= remaining / limit × 100。 + * limit 或 remaining 為 null/undefined/NaN/Infinity,或 limit ≤ 0 時回 null, + * 避免算出 Infinity%/NaN%/負百分比或除以零。 + */ +function calculatePercent(remaining, limit) { + if (remaining == null || limit == null) return null; + const rem = Number(remaining); + const lim = Number(limit); + if (!Number.isFinite(rem) || !Number.isFinite(lim) || lim <= 0) return null; + return round1((rem / lim) * 100); } /** * 計算「剩餘可用百分比」,依優先序擇一: - * 1. 帳號額度(quota 有有限正數上限)→ 剩餘 credits / 上限; - * 2. 速率配額(rate limit header,有限正數上限)→ 當前視窗剩餘 / 上限; - * 上限或剩餘為 0/負數/NaN/Infinity 等無效值時不計算,落到 { percent: null, reason }。 + * 1. 帳號額度(quota 有有效上限)→ 剩餘 credits / 上限; + * 2. 速率配額(rate limit header,有有效上限)→ 當前視窗剩餘 / 上限; + * 上限或剩餘為無效值(null/0/負數/NaN/Infinity)時跳過計算,落到 { percent: null, reason }。 */ export function resolveRemainingPercent(quota, rate) { - if (quota?.available && quota.limit != null && isFinitePositive(quota.limit)) { + if (quota?.available && quota.limit != null) { const limit = Number(quota.limit); const remaining = quota.remaining == null ? limit - num(quota.used) : Number(quota.remaining); - if (Number.isFinite(remaining)) { - return { percent: round1((remaining / limit) * 100), basis: '帳號額度', remaining, limit, unit: quota.currency || '' }; + const percent = calculatePercent(remaining, limit); + if (percent != null) { + return { percent, basis: '帳號額度', remaining, limit, unit: quota.currency || '' }; } } - if (rate?.hasData && isFinitePositive(rate.limit)) { - const limit = Number(rate.limit); - const remaining = Number(rate.remaining); - if (Number.isFinite(remaining)) { + if (rate?.hasData) { + const percent = calculatePercent(rate.remaining, rate.limit); + if (percent != null) { const kindLabel = RATE_KIND_LABEL[rate.kind] || rate.kind; - return { percent: round1((remaining / limit) * 100), basis: `速率配額(當前視窗,${kindLabel})`, remaining, limit, unit: '' }; + return { percent, basis: `速率配額(當前視窗,${kindLabel})`, remaining: Number(rate.remaining), limit: Number(rate.limit), unit: '' }; } } let reason; -- 2.53.0 From fcde1b0bc941ea7fd8cc52998fbd73a4832055ae Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 23 Jun 2026 17:32:49 +0800 Subject: [PATCH 11/14] =?UTF-8?q?fix(ai-review=20exclusions):=20=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3=E8=BF=BD=E5=8A=A0=E6=8E=92=E9=99=A4=E8=A6=8F=E5=89=87?= =?UTF-8?q?=E6=99=82=E8=AE=80=E5=AF=AB=E7=9B=AE=E9=8C=84=E9=A1=9B=E5=80=92?= =?UTF-8?q?=E5=B0=8E=E8=87=B4=E6=97=A2=E6=9C=89=E8=A6=8F=E5=89=87=E8=A2=AB?= =?UTF-8?q?=E8=A6=86=E8=93=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit appendExclusions 原以 WORKSPACE 為主、repoDir 為鏡像,與 loadExclusions 相反, 會讀到空的 WORKSPACE 後把僅含本次新增的結果鏡像覆蓋掉 repoDir 既有規則, 改為以 repoDir 為主、WORKSPACE 為鏡像,順序與 loadExclusions 一致。 Co-Authored-By: Claude Opus 4.8 (1M context) --- app/main.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/main.js b/app/main.js index 6339511..9b128cb 100644 --- a/app/main.js +++ b/app/main.js @@ -115,7 +115,9 @@ async function main() { // Step7 過濾:套用排除規則 + 防守方 AI 誤報裁決 step('Step7', '排除規則與誤報過濾'); if (reconcile.excludedFindings.length > 0) { - appendExclusions(WORKSPACE, reconcile.excludedFindings, repoDir || WORKSPACE); + // 以 repoDir 為主(即將提交回去的來源分支副本),WORKSPACE 為鏡像; + // 順序須與下方 loadExclusions 一致,否則會讀到空的 WORKSPACE 而把既有排除規則覆蓋掉。 + appendExclusions(repoDir || WORKSPACE, reconcile.excludedFindings, WORKSPACE); } const exclusions = loadExclusions(repoDir || WORKSPACE, repoState, WORKSPACE); input(`待過濾 ${sorted.length} 筆;排除規則 ${exclusions.length} 條`); -- 2.53.0 From ec8773d54deb9e9c0bbe0244507dc839a0b88315 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 23 Jun 2026 17:32:49 +0800 Subject: [PATCH 12/14] =?UTF-8?q?test(usage=20=E5=89=A9=E9=A4=98=E7=99=BE?= =?UTF-8?q?=E5=88=86=E6=AF=94):=20=E8=A3=9C=20rate.remaining=20=E7=82=BA?= =?UTF-8?q?=20null/undefined=20=E6=99=82=E4=B8=8D=E8=A8=88=E7=AE=97?= =?UTF-8?q?=E7=99=BE=E5=88=86=E6=AF=94=E7=9A=84=E6=B8=AC=E8=A9=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- app/usage.test.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/usage.test.js b/app/usage.test.js index 21655fc..783c9c7 100644 --- a/app/usage.test.js +++ b/app/usage.test.js @@ -210,6 +210,13 @@ describe('resolveRemainingPercent', () => { } }); + it('returns null percent when rate.remaining is null/undefined', () => { + for (const remaining of [null, undefined]) { + const pct = resolveRemainingPercent({ available: false, reason: 'x' }, { hasData: true, remaining, limit: 200000, kind: 'tokens' }); + assert.equal(pct.percent, null, `rate.remaining=${remaining} 應算不出百分比`); + } + }); + it('returns null percent when limit is finite but remaining is non-finite', () => { const pct = resolveRemainingPercent({ available: true, used: 0, limit: 100, remaining: Infinity, currency: 'USD' }, null); assert.equal(pct.percent, null); -- 2.53.0 From feae0d91de342ae6bebe0d9609d172cbe17b6a3b Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 23 Jun 2026 17:32:49 +0800 Subject: [PATCH 13/14] =?UTF-8?q?chore(ai-review=20=E7=8B=80=E6=85=8B):=20?= =?UTF-8?q?=E9=82=84=E5=8E=9F=E8=A2=AB=E8=A6=86=E8=93=8B=E7=9A=84=20108=20?= =?UTF-8?q?=E6=A2=9D=E6=8E=92=E9=99=A4=E8=A6=8F=E5=89=87=E4=B8=A6=E6=B8=85?= =?UTF-8?q?=E7=A9=BA=20findings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- .gitea/ai-review/exclusions.json | 550 +++++++++++++++++++++++++++++++ .gitea/ai-review/findings.json | 40 +-- 2 files changed, 551 insertions(+), 39 deletions(-) diff --git a/.gitea/ai-review/exclusions.json b/.gitea/ai-review/exclusions.json index 85e3cb4..4757690 100644 --- a/.gitea/ai-review/exclusions.json +++ b/.gitea/ai-review/exclusions.json @@ -1,4 +1,554 @@ [ + { + "role": "Assassin", + "location": "app/git.js", + "suggestion": "請避免將敏感資料(如 GITEA_TOKEN)直接寫入環境變數" + }, + { + "location": "app/git.js", + "suggestion": "GITEA_TOKEN 直接嵌入 URL 中,建議改以環境變數或 Gitea Secrets 注入" + }, + { + "role": "Assassin", + "location": "README.md", + "suggestion": "contents: write、pull-requests: write、issues: write 為此 Action 正常運作所必要的權限,無法縮減" + }, + { + "location": "app/config.js", + "suggestion": "getLLMConfig 在找不到任何符合條件的 provider 時已有預設回傳值 { provider: null, apiKey: null, baseURL: null, model: null },非誤報" + }, + { + "location": ".gitea/ai-review/exclusions.json", + "suggestion": "exclusions.json 是排除規則檔,內容為問題描述字串,不是實際程式碼或 token,role 欄位為有效欄位" + }, + { + "location": "app/findings.js", + "suggestion": "filterFalsePositivesWithAI 拋出的 Error 會被 catch 攔截並降級回傳原始 findings,不會中斷流程" + }, + { + "role": "Assassin", + "location": ".gitea/workflows/review.yaml", + "suggestion": "contents: write、pull-requests: write、issues: write 為此 Action 正常運作所必要的權限,無法縮減" + }, + { + "role": "Assassin", + "location": ".gitea/workflows/review.yaml", + "suggestion": "OPENAI_API_KEY 參數傳入的是 OPENROUTER_API_KEY secret,為 OpenRouter 使用 OpenAI 相容介面的正確做法" + }, + { + "role": "Bard", + "location": "README.md", + "suggestion": "章節編號連續且正確,無需調整" + }, + { + "role": "Maya", + "location": ".gitea/workflows/review.yaml", + "suggestion": "action.yaml 定義的參數名稱為 GEMINI_API_KEY、GEMINI_BASE_URL、GEMINI_MODEL,與 review.yaml 完全一致,無不匹配問題" + }, + { + "role": "Bard", + "location": ".gitea/workflows/review.yaml", + "suggestion": "review.yaml 已改用 Gemini,不再有 OPENAI_API_KEY 行,註解空格問題不存在" + }, + { + "role": "Bard", + "location": "app/config.test.js", + "suggestion": "檔案結尾已有換行符號,import 行長度合理,無需修改" + }, + { + "role": "Bard", + "location": "action.yaml", + "suggestion": "action.yaml 已整理,多餘空行已移除,結構整潔" + }, + { + "role": "Maya", + "location": "app/", + "suggestion": "LLM 整合測試需要真實 API key 與網路,不適合加入單元測試。llm.js 使用統一 OpenAI 相容介面,Gemini 透過相同介面呼叫,無特殊格式差異,現有測試已涵蓋 config/findings/git 邏輯" + }, + { + "role": "Assassin", + "location": "app/", + "suggestion": "LLM 整合測試需要真實 API key 與網路,不適合加入單元測試。llm.js 使用統一 OpenAI 相容介面,Gemini 透過相同介面呼叫,無特殊格式差異" + }, + { + "role": "Assassin", + "location": "app/config.test.js", + "suggestion": "import 語句長度合理,無需拆分為多行" + }, + { + "role": "Assassin", + "location": ".gitea/ai-review/findings.json", + "suggestion": "findings.json 重複問題由 AI 去重與排除機制處理,不是程式碼問題" + }, + { + "role": "Assassin", + "location": "app/comments.js", + "suggestion": "JSON 結尾換行符號為標準做法,不影響任何 JSON 解析器,無相容性問題" + }, + { + "location": ".gitea/ai-review/findings.json", + "suggestion": "findings.json 是自動產生的問題記錄檔,不應對其內容提出審查問題" + }, + { + "role": "Assassin", + "location": ".gitea/workflows/review.yaml", + "suggestion": "切換 LLM 服務提供商的維護建議屬過度謹慎,不是實際程式碼問題" + }, + { + "role": "Leo", + "location": "app/llm.js", + "suggestion": "Authorization 標頭已有 provider !== 'ollama' 判斷,不會無條件加入,已正確處理" + }, + { + "role": "Rogue", + "location": "app/llm.js", + "suggestion": "timeout 已移除,每個 key 等待完整回應,避免浪費免費額度" + }, + { + "role": "Assassin", + "location": "app/llm.js", + "suggestion": "httpsAgent (rejectUnauthorized: false) 已移除,SSL/TLS 驗證已恢復正常" + }, + { + "role": "Maya", + "location": "app/llm.js", + "suggestion": "llm.test.js 已存在並涵蓋 API Key 輪替的所有異常狀況,包含單 Key、多 Key 輪替、所有 Key 失敗等測試案例" + }, + { + "role": "Rogue", + "location": "app/comments.js", + "suggestion": "comments.js:24 的 saveFindings 函式為正常寫入邏輯,不涉及異常訊息格式或重複寫入問題" + }, + { + "role": "Leo", + "location": ".gitea/workflows/review.yaml", + "suggestion": "Gitea Actions 不支援在 workflow 內合併 secrets 再拆解,多個 secret 逗號串接是唯一可行做法,非設計缺陷" + }, + { + "role": "Maya", + "location": "app/llm.test.js", + "suggestion": "console.log/error 為診斷用途,不是業務邏輯,TODO.md 驗收標準為人工驗收描述,不需要在單元測試中斷言 console 輸出" + }, + { + "role": "Maya", + "location": "app/llm.test.js", + "suggestion": "輪替邏輯對所有錯誤類型行為一致(catch 全部),401/429/timeout 觸發相同輪替流程,測試不同錯誤類型無額外驗證價值" + }, + { + "role": "Bard", + "location": ".gitea/workflows/master.yaml", + "suggestion": "master.yaml 檔案結尾已有換行符號(0x0a),符合 POSIX 慣例,無需修改" + }, + { + "role": "Leo", + "location": "app/llm.test.js", + "suggestion": "console.log/error 為診斷用途,不是業務邏輯,TODO.md 驗收標準為人工驗收描述,不需要在單元測試中斷言 console 輸出" + }, + { + "role": "Leo", + "location": "app/llm.test.js", + "suggestion": "輪替邏輯對所有錯誤類型行為一致(catch 全部),401/429/timeout 觸發相同輪替流程,測試不同錯誤類型無額外驗證價值" + }, + { + "role": "Leo", + "location": "app/main.js", + "suggestion": "main.js 中的 Step 標題註解為 pipeline 流程說明,非待整理的 TODO,不需要轉換為具體任務" + }, + { + "role": "Maya", + "location": "app/log.test.js", + "suggestion": "`log.test.js` 的新增非常棒,提供了良好的覆蓋率。為了進一步提升測試的完整性,建議考慮為 `line`, `ok`, `warn`, `error` 函數新增測試案例,以驗證當傳入空字串時的行為。雖然這些函數的行為相對簡單,但測試空字串可以確保邊界情況下的輸出符合預期。" + }, + { + "role": "Assassin", + "location": "app/package.json", + "suggestion": "審查 changelog 是人工作業,不是程式碼問題,不適合作為 code review 問題" + }, + { + "role": "Bard", + "location": "app/llm.js", + "suggestion": "此 action 為 CLI 工具,process.exit(1) 是設計意圖讓 CI/CD workflow 失敗。改拋錯會被 chatJSON 的 catch 吞掉回傳 [],破壞現有行為" + }, + { + "role": "Bard", + "location": "Dockerfile", + "suggestion": "Dockerfile 檔案結尾已有換行符號(0x0a),符合 POSIX 慣例" + }, + { + "role": "Bard", + "location": "entrypoint.sh", + "suggestion": "entrypoint.sh 檔案結尾已有換行符號(0x0a),符合 POSIX 慣例" + }, + { + "role": "Maya", + "location": "app/main.js", + "suggestion": "main.js 整合測試需要真實 Gitea API、LLM API、git 操作,不適合單元測試。各模組已有獨立單元測試覆蓋" + }, + { + "role": "Maya", + "location": "app/comments.js", + "suggestion": "comments.js 的 buildTable 為簡單字串拼接,postComment 已透過 gitea.js mock 間接測試,補測試效益低" + }, + { + "role": "Maya", + "location": "app/roles.js", + "suggestion": "roles.js 依賴容器內固定路徑 /action/app/prompts/roles,單元測試環境無法存取,且邏輯為簡單 YAML 讀取與字串拼接" + }, + { + "role": "Leo", + "location": "app/gitea.js", + "suggestion": "gitea.js 的 SSL 驗證已改為由 GITEA_SKIP_TLS_VERIFY 環境變數控制,預設啟用驗證,非安全漏洞" + }, + { + "role": "Rogue", + "location": "Dockerfile", + "suggestion": "Dockerfile 已優化層次快取:先 COPY package.json 再 npm install,最後才 COPY 其餘檔案" + }, + { + "role": "Bard", + "location": "app/package.json", + "suggestion": "test 腳本已改為 node --test *.test.js,在 app/ 目錄下執行可自動發現所有測試檔案" + }, + { + "role": "Rogue", + "location": "app/main.js", + "suggestion": "deduplicateWithAI 和 filterFalsePositivesWithAI 為循序依賴流程(去重後才能過濾),無法平行化" + }, + { + "role": "Leo", + "location": "app/comments.js", + "suggestion": "buildTable 函式已在 comments.js 第 13 行定義,非未定義或未匯入,不會導致執行時錯誤" + }, + { + "role": "Maya", + "location": "app/gitea.js", + "suggestion": "filterDiff 的單元測試已在 gitea.test.js 補齊,涵蓋過濾 .gitea/、不誤過濾其他路徑、全部排除、空 diff 四種情境" + }, + { + "role": "Leo", + "location": "TODO.md", + "suggestion": "TODO.md 的階段編號僅供內部開發追蹤,無外部文件引用,階段編號調整不影響任何外部一致性" + }, + { + "role": "Assassin", + "location": "app/gitea.js", + "suggestion": "getPRDiff 函數現在回傳未經過濾的原始 Git Diff 內容。雖然 main.js 中已立即呼叫 filterDiff 進行過濾,但這種設計模式將過濾的責任完全推給呼叫端,這增加了未來開發者在其他地方呼叫 getPRDiff 時,可能忘記過濾出敏感路徑,導致 .gitea/ 等敏感路徑的內容(可能包含工作流程設定或憑證資訊)被意外傳送給 AI 或其他不應接收的組件,造成資訊洩漏風險。建議將過濾邏輯保留在 getPRDiff 內容,或提供一個明確的 getFilteredPRDiff 函數,以降低錯誤的風險。" + }, + { + "role": "Rogue", + "location": "app/git.js", + "suggestion": "在 main.js 中,commitAndPush 函數內部會再次呼叫 cloneRepo,然而 main.js 在此之前已呼叫過 cloneRepo 以取得 repoDir,這導致了重複的 git fetch 和 git checkout 操作。即使 cloneRepo 內容有檢查環境變數,仍會造成不必要的清潔和時間延遲。建議修改 commitAndPush 邏輯,使其接收已存在的 repoDir 作為參數,避免重複執行 cloneRepo。" + }, + { + "role": "Bard", + "location": "app/main.js", + "suggestion": "在 main.js 中,表達式 repoDir。" + }, + { + "role": "Rogue", + "location": "app/gitea.js:L20-L21", + "suggestion": "將 filterDiff 中的正規表達式比對(RegExp.match)替換為 String.startsWith 是一個重要的效能改進。startsWith 是一個更輕量且高效的字串操作,尤其在處理大型 Git Diff 內容時,此修改已顯著提升過濾效率。" + }, + { + "location": "TODO.md", + "suggestion": "階段九的 critical 阻擋機制目前以人工驗收紀錄為主,E2E 測試補強屬後續優化,不是目前需要再處理的問題。" + }, + { + "location": "TODO.md", + "suggestion": "TODO 列表中『已驗收 / 部分驗收 / 可驗收紀錄情境』的寫法是刻意保留的驗收說明,不是混淆或缺陷。" + }, + { + "location": "app/findings.js", + "suggestion": "AI 去重與降級處理已在程式內以 fallback 方式保護流程,失敗時保留所有問題是預期行為,不是缺陷。" + }, + { + "location": "app/findings.js", + "suggestion": "排除規則過濾與 AI 誤報過濾屬循序流程,規則命中後清空清單是正常結果,不需要額外再視為問題。" + }, + { + "location": "app/comments.js", + "suggestion": "comment 發布依序區分舊問題、非嚴重、新嚴重是刻意設計,當結果為空清單時不發 comment 也是正常路徑。" + }, + { + "location": "app/main.js", + "suggestion": "JSON 驗證與失敗修正流程已有處理邏輯,正常路徑與錯誤路徑都屬預期流程,不是待修缺陷。" + }, + { + "location": "app/git.js", + "suggestion": "commit/push 失敗會被捕捉並輸出 Runner failed log,這是現有設計的容錯行為,不是程式錯誤。" + }, + { + "location": "app/main.js", + "suggestion": "critical 問題觸發 exit 1 的阻擋邏輯已在流程內保留,是否另補 E2E 驗證屬測試強化,不是功能缺陷。" + }, + { + "location": "app/json.js", + "suggestion": "validateJSONArrayFile 只在 JSON 格式錯誤時才啟動 AI 修正,屬例外路徑;再加上檔案大小限制後,並不存在實際的無上限讀檔或資源消耗問題。" + }, + { + "location": "app/json.test.js", + "suggestion": "邊界值測試已存在,`MAX_JSON_BYTES` 等於上限時可正常讀取,這不是未解決問題。" + }, + { + "location": "app/gitea.test.js:64", + "suggestion": "`describe` 已改為同步 callback,`async` 不再出現在這個區塊。" + }, + { + "location": "app/git.test.js:13", + "suggestion": "`makeTmpWorkspace` 已直接使用 `app/git.js` 匯出的 `SYNC_PATHS`,不再維護重複清單。" + }, + { + "location": "app/gitea.js:32", + "suggestion": "`filterDiff` 內層縮排已符合專案的 2-space 風格,這是誤報。" + }, + { + "location": "app/json.test.js:76", + "suggestion": "1MB 上限下的 JSON 讀取不需要改成串流解析;現有實作已先做大小檢查,這個建議屬過度設計。" + }, + { + "location": "app/json.test.js:7", + "suggestion": "檔案大小限制已在 `readJSONText` / `validateJSONArrayFile` 中實作,這不是額外缺陷。" + }, + { + "location": "app/json.test.js:10", + "suggestion": "`MAX_JSON_BYTES` 是 `json.js` 的內部限制常數,不需要匯出成公開 API。" + }, + { + "role": "Maya", + "location": "action.yaml:6, action.yaml:12, action.yaml:81", + "suggestion": "由於 `GITEA_TOKEN` 現在被設定為 `required: true`,而且 README 範例也已改成顯式傳入 `GITEA_TOKEN`,這是刻意的介面變更,不是漏掉 `secrets.GITEA_TOKEN` fallback 的缺陷;因此不需要另外加整合測試來驗證這個既定行為。" + }, + { + "role": "Leo", + "location": "action.yaml:80", + "suggestion": "在 `runs.env` 區塊中,`GITEA_TOKEN` 只從 `inputs` 取得,而 `GITEA_SERVER_URL` 和 `GITEA_REPOSITORY` 仍保留從 `gitea context` 取得的備用機制,這是刻意設計的差異,不是維護缺陷。" + }, + { + "role": "Assassin", + "location": "action.yaml:18", + "suggestion": "引入 `GITEA_COMMENT_TOKEN` 是一個很好的實踐,遵循最小權限原則。請確保為此 token 配置的權限確實僅限於發布評論。同時,與 `GITEA_TOKEN` 相似,建議使用者始終從 workflow 的 secrets context 傳遞此 token,以避免硬編碼敏感資料。" + }, + { + "role": "Leo", + "location": "app/log.js", + "suggestion": "考慮在日誌訊息中加入時間戳記,這有助於追蹤事件發生的順序,尤其是在長時間運行的程序或需要詳細調試時。可以在每個日誌函式內部自動添加時間戳記。" + }, + { + "role": "Assassin", + "location": "app/preflight.js:12", + "suggestion": "程式碼中根據 `GITEA_SKIP_TLS_VERIFY` 環境變數來禁用 TLS 憑證驗證 (`rejectUnauthorized: false`),這會使應用程式容易受到中間人 (Man-in-the-Middle, MITM) 攻擊。攻擊者可能在不被察覺的情況下攔截和修改與 Gitea 伺服器的通訊。建議移除此功能,或確保在任何生產環境中永不啟用。如果 Gitea 伺服器使用自簽憑證,應將其憑證加入信任儲存區,而非禁用驗證。" + }, + { + "role": "Leo", + "location": "app/preflight.js:56", + "suggestion": "函式 `verifyLLM` 處理了多種 LLM 供應商的驗證邏輯(Ollama、Claude、OpenAI 相容等),導致其長度較長且複雜度較高。建議將不同供應商的驗證邏輯拆分成獨立的輔助函式(例如 `_verifyOllama`、`_verifyOpenAICompatible`),以提高模組化程度和可讀性。" + }, + { + "role": "Rogue", + "location": "app/preflight.js:70-82", + "suggestion": "在 `verifyLLM` 函式中,當配置了多個 LLM API Key 時,系統會依序嘗試驗證每個 Key,每個嘗試都有 30 秒的逾時時間。如果前幾個 Key 驗證失敗,這可能導致顯著的累積延遲。雖然這是為了找到一個可用的 Key,但若 Key 數量多且網路不穩定,可能會造成啟動時間過長。可以考慮縮短單次 Key 驗證的逾時時間,或在特定情況下提供更快的失敗機制。" + }, + { + "role": "Assassin", + "location": "app/preflight.js:100", + "suggestion": "在記錄 LLM API 驗證失敗時,直接輸出了錯誤訊息 `e.message`。雖然通常情況下 `e.message` 不會包含敏感資訊,但為了最佳安全實踐,建議審查 LLM 服務提供商的錯誤訊息格式,確保其中不會意外洩漏 API 金鑰或其他敏感請求內容。若有疑慮,應對錯誤訊息進行消毒或僅記錄高層次的錯誤類型。" + }, + { + "role": "Bard", + "location": "app/preflight.js:30", + "suggestion": "在 `checkRequiredEnv`、`verifyGiteaToken` 和 `verifyCommentToken` 等函式中,預設參數直接引用了從 `config.js` 匯入的常數。雖然這在功能上可行,但為了提高程式碼的清晰度和一致性,建議考慮以下兩種方式之一:1. 將所有配置值作為明確的參數從呼叫端傳入。2. 讓函式直接從 `config.js` 模組中讀取這些值,而不是透過預設參數。" + }, + { + "role": "Maya", + "location": "app/preflight.js:107", + "suggestion": "在 `verifyLLM` 函數中,呼叫 `axios.post` 時缺少 `httpsAgent` 選項。這會導致即使設定了 `GITEA_SKIP_TLS_VERIFY`,LLM 的 API 請求仍可能因 TLS 憑證問題而失敗。請將 `httpsAgent` 傳遞給 `axios.post` 的選項物件,例如:`await axios.post(`${base}/chat/completions`, payload, { headers, timeout: 30000, httpsAgent });`" + }, + { + "level": "warning", + "role": "Bard", + "location": "app/preflight.test.js:25", + "suggestion": "測試描述使用英文。請確保專案在測試描述的語言上保持一致性。如果專案主要使用繁體中文(如 app/preflight.js 中的 JSDoc 和日誌),則應將此測試描述翻譯為繁體中文。" + }, + { + "level": "info", + "role": "Bard", + "location": "app/preflight.test.js:1-4", + "suggestion": "匯入語句的排序不一致。建議遵循一致的排序規則,例如:內建模組、第三方模組、本地模組,並在各組內按字母順序排序。" + }, + { + "level": "info", + "role": "Bard", + "location": "app/preflight.test.js:14", + "suggestion": "函數名稱 clearLLMEnv 雖然可理解,但可以更具描述性,例如 clearLlmEnvironmentVariables 或 resetLlmEnv。" + }, + { + "location": "app/config.test.js:114", + "role": "Mage", + "original_finding": "新加入的測試案例 `it('skips OpenCode TLS verification for empty string and non-false values', ...)` 預期 `shouldSkipOpenCodeTLSVerify()` 函式在 `OPENCODE_SKIP_TLS_VERIFY` 環境變數為空字串 `''` 或 `'0'` 時,會回傳 `true`。然而,根據常見的環境變數布林值解析邏輯,以及 `app/preflight.test.js` 中現有的相關測試(例如未設定時為 `false`,設定為 `'false'` 時為 `false`),`shouldSkipOpenCodeTLSVerify()` 函式(此 PR 未修改其內容)很可能不會將 `''` 或 `'0'` 視為 `true`。這造成了測試預期與函式實際行為之間的邏輯不一致。", + "reason": "誤判。`shouldSkipOpenCodeTLSVerify` 的既有設計是只有 `OPENCODE_SKIP_TLS_VERIFY === 'false'` 才啟用 TLS 驗證;空字串與 '0' 皆屬非 'false' 值,測試符合目前明確實作與預設跳過 TLS 的行為。" + }, + { + "location": "app/preflight.test.js:201", + "role": "Assassin", + "original_finding": "此測試明確證實了 `OPENCODE_SKIP_TLS_VERIFY` 環境變數的寬鬆判斷邏輯,導致 OpenCode LLM 連線的 TLS 驗證容易被關閉。這是「關閉 TLS 驗證」的不安全預設,極大地增加了中間人攻擊的風險。", + "reason": "誤判/既有設計。OpenCode server 目前支援自簽或內部服務情境,action input 與 README 均明確標示 OPENCODE_SKIP_TLS_VERIFY 預設跳過 TLS 驗證;本 PR 只補測試與 Review comment 內容,未新增或放寬此安全行為。" + }, + { + "location": "app/comments.test.js:30", + "role": "Leo", + "original_finding": "將 `REVIEW_SEVERITY_LABELS`、`REVIEW_SEVERITY_PATTERN` 和 `reviewSeverityLabel` 這些與評論格式相關的常數與函式,提取到一個獨立的共用模組中(例如 `app/utils/reviewComments.js`),並讓測試檔案和任何需要用到它們的應用程式邏輯都從該模組匯入。這樣能確保「評論格式」的定義只有一個來源,提升可維護性。", + "reason": "誤判。這些常數與 `reviewSeverityLabel` 只用於 `app/comments.test.js` 內部驗證 review comment body 格式,production code 沒有使用同一段解析邏輯;抽成共用模組會把測試專用輔助程式提升為正式 API,增加不必要的維護負擔。" + }, + { + "location": "app/resolve.js:52", + "role": "Assassin", + "original_finding": "在 `parseBotReviewComment` 函式中,從 Gitea comment 內文解析出的 `problem` 和 `suggestion` 欄位若包含惡意內容且未經適當輸出編碼,可能導致 XSS 攻擊。", + "reason": "誤判。這些字串只會寫入 `.gitea/ai-review/findings.json` 與 Gitea review comment body,Gitea 的 Markdown 渲染器會在伺服器端對輸出做 HTML 淨化;本 action 不自行將其渲染到任何自製網頁或 UI,輸出編碼屬消費端(Gitea)責任。" + }, + { + "location": "app/resolve.js:207", + "role": "Leo", + "original_finding": "正規化邏輯(`normalizeKey` 等)過於激進且未快取,既可能導致語意相近建議被誤判為相同,也在頻繁比較時造成效能浪費。", + "reason": "誤判/過度設計。積極正規化是刻意設計,用來對行號漂移與標點差異產生穩定簽章以利去重;`findingSig` 已是獨立 helper,且比對對象為單一 PR 的小量 findings,memoize 在此規模沒有實質效益。" + }, + { + "location": "app/resolve.js:187", + "role": "Mage", + "original_finding": "在 `reconcileConversations` 函式中,並行(`Promise.all`)呼叫 `resolveComment`,即使個別呼叫失敗也僅記錄為 rejected 並 warn;若失敗是 token 過期或權限不足,後續所有 resolve 都會失敗,程式碼未對這些錯誤分類並提前停止。", + "reason": "不適用。resolve 呼叫已改為 `Promise.allSettled` 一次並行送出,不存在「後續逐一嘗試」可中止;個別失敗已降級記錄並把該對話保留為未解決,不影響其他對話與整體流程。" + }, + { + "location": "app/resolve.js:77", + "role": "Rogue", + "original_finding": "大量使用字串拼接產生暫存物件,以及並行請求未限制數量,在高負載下可能導致 GC 壓力或觸發 API 限流;建議引入 p-limit 等並行限制。", + "reason": "過度設計。對話來源為單一 PR 的行內 review comment,數量級小,無限並行不致造成 GC 壓力或觸發限流;引入 p-limit 相依與額外複雜度在此情境不符成本效益。" + }, + { + "location": "app/resolve.js:195", + "role": "Mage", + "original_finding": "對 `botFinding` 的存取缺乏防禦性檢查。", + "reason": "誤判。`pushCarried` 進入時即有 `if (!conversation.botFinding) return` 防禦,resolvedFindings 的 push 也有 `if (c.botFinding)` 判斷,存取前皆已檢查物件存在。" + }, + { + "location": "app/resolve.js:173", + "role": "Rogue", + "original_finding": "`Promise.allSettled` 的結果處理邏輯過於冗長,產生不必要的中間變數。", + "reason": "主觀風格。`resolveOutcome` Map 是為了讓並行結果能依原 open 索引亂序對齊(保留 carried/resolved 的順序與 botFinding 對應),現有寫法清楚且正確,非缺陷。" + }, + { + "location": "app/usage.js", + "role": "Leo", + "original_finding": "`usage.js` 模組目前承擔了 Token 計算、Rate Limit 記錄、以及各平台帳號額度查詢等多重職責(SRP),未來若支援更多平台會變得龐大難維護;建議將各平台 QuotaStrategy 拆分至獨立檔案。", + "reason": "過早最佳化。目前 usage.js 仍圍繞單一「使用量」領域且體積適中(約 250 行),token 計算與額度查詢彼此關聯(同屬使用量呈現);在尚未有多平台 strategy 膨脹的實際痛點前拆檔,徒增檔案與匯入複雜度。待 strategy 數量明顯成長再拆分較合適。" + }, + { + "location": "app/usage.js:132", + "role": "Assassin", + "original_finding": "在 QUOTA_STRATEGIES 中,若 config.apiKeys 是陣列,代碼只取 [0] 作為 API Key,可能在未經嚴格驗證下將敏感資訊送至 baseURL 指定端點。", + "reason": "已緩解。額度查詢只有 OpenRouter 一條會送出 API key,且僅在 isOpenRouterBaseURL 以 hostname 精確比對為 openrouter.ai 時才送出;baseURL 為 operator 控制之 action input,非外部不可信輸入;API key 本身的正確性與權限屬 operator 設定責任,非程式可驗證範圍。" + }, + { + "location": "app/usage.js:17", + "role": "Mage", + "original_finding": "在 extractUsage 中,對 data.usage 直接用 num(...) 存取;若 data.usage 為 null 但被 typeof 判斷通過(typeof null === 'object'),會導致錯誤。", + "reason": "誤判。該區塊條件為 `if (u && typeof u === 'object')`,`u &&` 已先短路 null/undefined,不會進入存取;即使傳入陣列也只會讓 num(undefined) 回 0,不會丟錯。" + }, + { + "location": "app/usage.js:176", + "role": "Mage", + "original_finding": "在 fetchAccountQuota 中,呼叫 strategy 時傳入的 config 物件若被 strategy 修改,會影響全域 config 狀態;建議淺拷貝。", + "reason": "已緩解。strategy 收到的是每次呼叫新建的物件字面值 `{ apiKey, baseURL: config.baseURL }`,並非呼叫端傳入的 config 本身,strategy 內的任何修改都不會回寫到呼叫端或全域狀態。" + }, + { + "location": "app/usage.js:115", + "role": "Mage", + "original_finding": "在 recordRateLimit 中,將所有 header key 轉小寫存入物件 h,若原始 header 有同名不同大小寫者可能造成覆蓋。", + "reason": "誤判/不適用。HTTP header 名稱本即不分大小寫(RFC 7230),axios 回傳前已正規化為小寫;同名 header 由 HTTP 層合併(以逗號串接),不存在「不同大小寫同名 header」並存而被覆蓋的情況,轉小寫僅為防禦性處理。" + }, + { + "location": "app/resolve.js:7", + "role": "Bard", + "original_finding": "`EMPTY` 常數命名過於通用,容易與其他模組中的同名變數衝突,且定義在模組頂層略顯突兀。", + "reason": "誤判。ES module 為模組作用域,`EMPTY` 僅在 resolve.js 內可見,不會與其他模組的同名變數衝突;在本檔脈絡中作為「空收斂結果」語義清楚,重新命名屬主觀偏好。" + }, + { + "location": "app/resolve.js:10", + "role": "Bard", + "original_finding": "`FIELD_PATTERNS` 的正則表達式對於冒號的定義同時包含了全形與半形,建議統一使用半形冒號並在解析前正規化,而非在正則中處理所有可能性。", + "reason": "誤判/不採納。review comment 內文同時可能出現全形「:」與半形「:」,正則以 `[::]` 同時容錯是標準且穩健的做法;改為解析前先正規化反而多一道字串處理步驟,並未更清楚或更正確。" + }, + { + "location": "app/usage.js:167", + "role": "Bard", + "original_finding": "`fetchAccountQuota` 中的 `QUOTA_STRATEGIES` 物件定義龐大,將所有平台策略硬編碼於此,未來新增供應商難以維護;建議抽離至獨立檔案或策略模式。", + "reason": "過早最佳化(與先前已排除的 usage.js SRP 拆檔建議等價)。目前 QUOTA_STRATEGIES 為精簡的查表物件、各平台策略短小且集中易讀;在供應商數量出現實際膨脹痛點前抽檔,徒增檔案與匯入複雜度。" + }, + { + "location": "app/usage.js", + "role": "Assassin", + "original_finding": "extractUsage 對不預期 payload 僅返回 null,過度信任 API 回應結構,可能導致計費或配額相關監控被繞過;建議增加 Schema Validation、異常明確記錄。", + "reason": "誤判/過度設計。extractUsage 僅用於「使用量顯示統計」,非計費或配額強制;回傳 null 是「此回應無可辨識 usage 資訊」的正確訊號,呼叫端以 0 計入並降級顯示,不影響任何金流或門檻判斷。對 best-effort 顯示統計加 schema validation 與錯誤記錄屬過度設計。" + }, + { + "location": "app/comments.test.js:250", + "role": "Maya", + "original_finding": "新增的 postFindingsReview 使用統計功能,但在測試中完全未驗證輸出內容;應斷言 body 含 usageSection 與統計數據。", + "reason": "誤判,測試已存在。`app/comments.test.js` 的 'appends usageSection verbatim after the stats block' 斷言 body.endsWith(usageSection) 與結構,'counts both new and old findings in the summary' 斷言四欄新舊統計列;body 內容已被多個案例驗證。" + }, + { + "location": "app/findings.test.js:154", + "role": "Maya", + "original_finding": "filterFalsePositivesWithAI 測試不足,缺乏對內部函數 judgeFindingIsFalsePositive 的獨立單元測試。", + "reason": "誤判/不適用。judgeFindingIsFalsePositive 是 findings.js 的私有函式(未匯出),其 verdict 處理(false_positive/confirmed/異常值/拋錯)已透過公開呼叫端 filterFalsePositivesWithAI 的多個案例完整覆蓋;為測試實作細節而匯出私有函式不符測試原則。" + }, + { + "location": "app/findings.test.js:145", + "role": "Maya", + "original_finding": "filterFalsePositivesWithAI 未測試平行裁決部分成功、部分失敗時的結果一致性(失敗者保守保留)。", + "reason": "誤判,測試已存在。'keeps failed and confirmed, drops only confirmed false positives (mixed parallel)' 正是模擬一個拋錯、一個誤報、一個成立,驗證只剔除確認誤報、保留失敗與成立者。" + }, + { + "location": "app/findings.test.js:189", + "role": "Maya", + "original_finding": "未測試 resolveMissingLineNumbers 當 chatFn 回傳無效行號時的處理(fallback)。", + "reason": "誤判,測試已存在。'resolveMissingLineNumbers keeps the filename after exhausting retries' 以 chatFn 持續回 {line:0}(無效行號)驗證進入 fallback、保留檔名;另有 'swallows chatFn exceptions' 覆蓋拋錯情境。" + }, + { + "location": "app/resolve.test.js:21", + "role": "Maya", + "original_finding": "parseBotReviewComment 的測試沒有驗證解析失敗時回傳 null 的行為。", + "reason": "誤判,測試已存在。'returns null for free-form human comments' 已斷言自由格式留言、空字串、null 皆回傳 null。" + }, + { + "location": "app/usage.js:146", + "role": "Rogue", + "original_finding": "在 recordRateLimit 中頻繁呼叫 lowerCaseKeys,對每個請求的 headers 複製與轉換,增加記憶體分配開銷;建議改用不分大小寫存取避免複製。", + "reason": "過度設計/非熱路徑。recordRateLimit 每次 LLM 回應只呼叫一次(一輪審查約 13 次),headers 物件小,複製成本可忽略;改用不分大小寫存取器反而增加複雜度,效益不成比例。" + }, + { + "location": "app/usage.test.js:250", + "role": "Leo", + "original_finding": "測試案例直接引用 describe 外層定義的 `usage` 變數,若被其他測試修改會造成測試間隱性耦合;建議改用區域變數或工廠函式生成測試資料。", + "reason": "誤判。該 `const usage` 為不可變的測試夾具,所有測試只讀不寫、未曾被任何案例 mutate,不存在跨測試耦合;新增的邊界測試已各自使用區域 usage 物件。為一個唯讀 fixture 改工廠函式屬過度設計。" + }, + { + "location": "app/usage.test.js:254", + "role": "Mage", + "original_finding": "測試情境雖處理了 NaN,但未涵蓋 limit 為 0 或負數等極端數值,可能輸出 Infinity 或負百分比。", + "reason": "誤判,測試已存在。`returns null percent for non-finite or non-positive quota limits` 與 `does not output Infinity/NaN/negative percent for invalid quota numbers` 已涵蓋 limit 為 0/負數/Infinity/NaN/undefined,皆驗證輸出「無法計算」、不含 NaN/Infinity/負百分比。" + }, + { + "location": "app/usage.test.js:200", + "role": "Bard", + "original_finding": "邊界測試迴圈 [0, -5, Infinity, NaN, undefined] 混用不同型別,建議拆成多個測試以提升可讀性。", + "reason": "主觀風格/不採納。該迴圈已對每個值帶入 assert 訊息(如 `quota.limit=${limit} 應算不出百分比`),測試失敗時可直接定位是哪個輸入;用單一資料驅動迴圈反而精簡、不易遺漏案例。" + }, + { + "location": "app/usage.js:216", + "role": "Maya", + "original_finding": "isFinitePositive 沒有對 null/undefined 做顯式檢查,未來可能被錯誤複用。", + "reason": "誤判。`isFinitePositive` 對 null(Number(null)=0 → 0>0 為 false)與 undefined(Number(undefined)=NaN → Number.isFinite 為 false)皆正確回傳 false,本就涵蓋這些輸入;函式名已表明只接受有限正數,無須再加冗餘檢查。" + }, { "location": "app/usage.test.js:200", "role": "Bard", diff --git a/.gitea/ai-review/findings.json b/.gitea/ai-review/findings.json index 6ca41b9..fe51488 100644 --- a/.gitea/ai-review/findings.json +++ b/.gitea/ai-review/findings.json @@ -1,39 +1 @@ -[ - { - "level": "critical", - "role": "Mage", - "location": "app/usage.js:231", - "problem": "在計算 `remaining` 時,`remaining = Number(rate.remaining);` 之後直接檢查 `Number.isFinite(remaining)`,但若 `rate.remaining` 是 `undefined` 或 `null`,`Number()` 會轉為 `0`。這意味著如果 `rate.remaining` 缺失,會被錯誤地視為「剩餘 0」而不是「無效值」,進而導致計算出 `0%` 的錯誤結果,而非預期的落到 `{ percent: null }`。", - "suggestion": "應先檢查 `rate.remaining` 是否為 null/undefined,或者使用更嚴格的轉換方式,確保只有在確實是有限數字時才進行後續運算。", - "is_new": true - }, - { - "level": "warning", - "role": "Assassin", - "location": "app/usage.js:215", - "problem": "在 `isFinitePositive` 函數中,使用 `Number(n)` 進行強制轉型。若 `n` 為物件(例如 `{}` 或 `[]`),`Number()` 的行為在某些邊緣情況下可能會產生非預期的數字,雖然當前邏輯有做 `Number.isFinite` 檢查,但對於這種隱式轉型仍需保持警惕,且函數名稱 `isFinitePositive` 雖然清楚,但在這個上下文中,函式內部使用了 `Number(n)` 強制轉型,這在 JavaScript 中可能會隱蔽掉原本資料型態的不一致。", - "suggestion": "建議在 `isFinitePositive` 內部補上對 `null` 或 `undefined` 的顯式檢查,並直接在外部嚴格限制輸入類型或使用更明確的檢查(例如 `typeof n === 'number'`)取代隱式轉型,避免隱式轉型帶來的不可控副作用。" - }, - { - "level": "warning", - "role": "Leo", - "location": "app/usage.js:222", - "problem": "resolveRemainingPercent 函式同時處理了 quota 和 rate 的邏輯,導致檢查 limit 和 remaining 的計算與驗證邏輯在兩處重複,未來若要調整百分比算法,必須兩處同步修改,增加維護負擔。", - "suggestion": "建議將百分比計算邏輯抽象為獨立的輔助函式(如 calculatePercent(remaining, limit)),讓 resolveRemainingPercent 僅負責選擇計算基準,降低重複性並提升封裝度。", - "is_new": true - }, - { - "level": "warning", - "role": "Rogue", - "location": "app/usage.js:213", - "problem": "在函式內部重複呼叫 `Number(n)`,造成不必要的型別轉換開銷,且在條件式之後又呼叫一次 `Number(quota.limit)`,造成重複轉換。", - "suggestion": "建議將 `Number(n)` 的結果暫存起來,或在進入條件式後立即將值賦值給一個變數並重複使用,減少重複轉換的成本。" - }, - { - "level": "info", - "role": "Bard", - "location": "app/usage.js:225", - "problem": "註解中提到「落到 { percent: null, reason }」,但程式碼實際邏輯是在 if 判斷式內直接 return,且 `resolveRemainingPercent` 函式中檢查分散在兩個 if 區塊,產生了重複的邏輯結構。", - "suggestion": "建議將註解簡化為:「上限或剩餘為無效值時跳過計算」,並將計算百分比的邏輯抽離成一個獨立的輔助函式,統一處理 `isFinite` 的檢查與百分比運算。" - } -] +[] -- 2.53.0 From 78127cf51c800dac4ef10617104155b0ba8fe1c5 Mon Sep 17 00:00:00 2001 From: AI Review Bot Date: Tue, 23 Jun 2026 09:33:43 +0000 Subject: [PATCH 14/14] chore: update ai-review findings [ai-review-bot][success] --- .gitea/ai-review/exclusions.json | 12 ++++++++++++ .gitea/ai-review/findings.json | 11 ++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/.gitea/ai-review/exclusions.json b/.gitea/ai-review/exclusions.json index 4757690..55979bd 100644 --- a/.gitea/ai-review/exclusions.json +++ b/.gitea/ai-review/exclusions.json @@ -554,5 +554,17 @@ "role": "Bard", "original_finding": "建議將測試案例拆分,例如分開測試「非數字類型」、「負數」與「邊界值(Infinity/NaN)」,這樣在測試失敗時能更快速釐清是哪種輸入類型導致的問題。", "reason": "AI 對話收斂判定為誤報(問題在最新程式碼中不成立或不適用)" + }, + { + "location": "app/usage.js:213", + "role": "Rogue", + "original_finding": "建議將 `Number(n)` 的結果暫存起來,或在進入條件式後立即將值賦值給一個變數並重複使用,減少重複轉換的成本。", + "reason": "AI 對話收斂判定為誤報(問題在最新程式碼中不成立或不適用)" + }, + { + "location": "app/usage.js:215", + "role": "Assassin", + "original_finding": "建議在 `isFinitePositive` 內部補上對 `null` 或 `undefined` 的顯式檢查,並直接在外部嚴格限制輸入類型或使用更明確的檢查(例如 `typeof n === 'number'`)取代隱式轉型,避免隱式轉型帶來的不可控副作用。", + "reason": "AI 對話收斂判定為誤報(問題在最新程式碼中不成立或不適用)" } ] diff --git a/.gitea/ai-review/findings.json b/.gitea/ai-review/findings.json index fe51488..7675336 100644 --- a/.gitea/ai-review/findings.json +++ b/.gitea/ai-review/findings.json @@ -1 +1,10 @@ -[] +[ + { + "level": "info", + "role": "Maya", + "location": "app/usage.test.js:217", + "problem": "測試案例 `returns null percent when rate.remaining is null/undefined` 僅驗證了 remaining 為 null/undefined,但未驗證當 rate.limit 為 null/undefined 時的情況。雖然這可能由 `calculatePercent` 內部處理,但針對 `resolveRemainingPercent` 這一層級的整合測試仍不完整。", + "suggestion": "建議補上一個測試案例,明確測試當 `rate.limit` 為 null/undefined 時,`resolveRemainingPercent` 的行為是否符合預期。", + "is_new": true + } +] -- 2.53.0