test/consolidate-app-test-20260626-111823
develop
將單元測試整併至 app/test/(移除根目錄下無法被執行的重複 test/ 目錄),並解決多輪 AI 程式碼審查的 findings(輸入驗證、錯誤訊息、測試覆蓋與測試品質)。對外契約(action inputs/outputs、版本進位與 beta 規則)維持不變。
app/test/
test/
app/
node --test
config.loadConfig
GITEA_REPOSITORY
owner/repo
..
releases
app/config.js
assertRepository
app/releases.js
Array.from
app/test/logger.test.js
captureStream
app/test/config.test.js
app/test/version.test.js
.gitea/ai-review/
package.json
A-Za-z0-9._-
/
node --test 'app/test/**/*.test.js'
app/package.json 的 npm test(node --test,cwd 為 app/)只會探索 app/test/, repo 根目錄另一份 test/ 中的 main 整合測試(index.test.js)因此從未被執行。 將 config/index/releases/version 收斂至 app/test/(修正 require 路徑), 保留 output 測試,移除重複且無法執行的 repo 根 test/。現 app/test/ 共 34 筆測試皆可執行。
- config 對 GITEA_REPOSITORY 加入 owner/repo 格式驗證,拒絕路徑穿越與特殊字元 - releases JSON 解析失敗時,錯誤訊息納入截斷的回傳內容片段以利除錯 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- 新增 logger section/info/error 的輸出格式測試 - config 補上 GITEA_REPOSITORY 驗證測試,並拆分 isUnset/requireEnv 的正反例 - version 補回 compareVersionArrays 的 major 跨區段比較案例 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
已解決 6 條(warning x6:輸入驗證、錯誤訊息、logger 測試、測試拆分與補強),1 條判定為誤報寫入 exclusions(info:withFetch 抽離屬過度設計),findings 清空。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- releases 改以 Array.from 依字元截斷回應片段,避免拆分多位元組字元造成亂碼,並標記片段內容 - config assertRepository 加入每段長度上限,並將 valid 更名為語義明確的 isRepoFormatValid Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- 補上 GITEA_REPOSITORY 空段/缺斜線/多段等邊界格式的拋錯測試 - 將 logger 測試的 captureStdout/captureStderr 合併為單一 captureStream Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
已解決 5 條(critical x1、warning x2、info x2:多位元組截斷、repository 驗證與命名、邊界測試、串流攔截 DRY),1 條判定為誤報寫入 exclusions(warning:logger 改 DI 屬過度設計),findings 清空。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
🔍 服務:opencode 模型:gemini-2.5-flash
本次審查(opencode / gemini-2.5-flash,共 17 次呼叫)
剩餘可用
剩餘可用:無法計算百分比(自架服務,無帳號額度概念)
@@ -58,0 +59,4 @@
function assertRepository(name, value) {
const parts = value.split('/');
const isRepoFormatValid = parts.length === 2
&& parts.every((part) => part.length > 0 && part.length <= 100
嚴重等級:🔴 嚴重 審查員:Maya 問題:新增的 assertRepository 函數對 GITEA_REPOSITORY 的檢查邏輯並未測試到所有邊界條件,例如:包含特殊字元(非字母數字點下底線連字號)、極長字串段(> 100 字元)、以及完全空白的 value。 建議:請在 app/test/config.test.js 中增加針對非法字元、超過 100 字元的字串段,以及空字串的測試案例,確保 assertRepository 的防禦性功能完全被驗證。
value
嚴重等級:🟡 警告 審查員:Mage 問題:在 assertRepository 中,parts.every 僅檢查了單一區段長度,未對總體長度(owner/repo)做任何限制。 建議:建議在分割前,先對 value 的總長度進行合理範圍的限制。
parts.every
@@ -58,0 +60,4 @@
&& /^[A-Za-z0-9._-]+$/.test(part)
嚴重等級:🔴 嚴重 審查員:Mage 問題:在 assertRepository 函數中,part !== '.' && part !== '..' 的檢查雖然能防止路徑穿越,但正規表達式 ^[A-Za-z0-9._-]+$ 仍然允許 . 開頭或結尾的檔案名稱(例如 .foo 或 foo.),這在某些檔案系統中可能導致意外行為或被視為隱藏檔案,且未明確禁止僅由 . 組成的名稱(儘管後面有手動排除)。 建議:建議調整正規表達式,禁止名稱以點號開頭或結尾,例如使用 /^[A-Za-z0-9_-]+([.][A-Za-z0-9_-]+)*$/ 來確保點號只能存在於中間。
part !== '.' && part !== '..'
^[A-Za-z0-9._-]+$
.
.foo
foo.
/^[A-Za-z0-9_-]+([.][A-Za-z0-9_-]+)*$/
嚴重等級:🟡 警告 審查員:Leo 問題:在 assertRepository 函式中使用了硬編碼的 100 作為長度限制,這是一個魔術數字,難以維護且無法從環境配置中調整。 建議:建議將 100 抽離為常數(例如 MAX_REPO_NAME_LENGTH),提高可維護性。
100
MAX_REPO_NAME_LENGTH
嚴重等級:🔵 建議 審查員:Bard 問題:正規表達式 /^[A-Za-z0-9._-]+$/ 描述稍顯冗長,且未利用字元類別的特性,略顯不夠優雅。 建議:考慮簡化寫法,或確保該格式驗證邏輯與專案其他地方一致並抽取為常數。
/^[A-Za-z0-9._-]+$/
@@ -58,0 +62,4 @@
&& part !== '.' && part !== '..');
if (!isRepoFormatValid) {
嚴重等級:🟡 警告 審查員:Rogue 問題:在 every 迴圈中重複宣告與編譯 Regex /^[A-Za-z0-9._-]+$/,若該函數被頻繁呼叫,將造成無謂的 CPU 與記憶體浪費。 建議:將 Regex 移至函數外層宣告為常數。
every
@@ -65,2 +65,3 @@
} catch {
throw new Error(`release API 回傳資料無法解析 (page=${page})`);
// 以字元(而非 UTF-16 碼元)截斷回傳內容片段,避免拆分多位元組字元造成亂碼
const contentSnippet = Array.from(text).slice(0, 200).join('');
嚴重等級:🔵 建議 審查員:Leo 問題:硬編碼了截斷字串長度 200,這是一個魔術數字,若未來需要調整截斷長度以容納更多偵錯資訊或減少輸出,維護者需要直接修改此處邏輯。 建議:建議定義一個具名的常數(例如 API_ERROR_SNIPPET_LENGTH),讓此數字具備語意,且未來調整時只需改動一處。
200
API_ERROR_SNIPPET_LENGTH
@@ -66,1 +66,3 @@
throw new Error(`release API 回傳資料無法解析 (page=${page}),回應內容片段:「${contentSnippet}」`);
嚴重等級:🔵 建議 審查員:Assassin 問題:將未經清洗的 API 回應片段直接納入 Error 物件,若 API 返回敏感資訊(內部路徑、堆疊追蹤、使用者資料),可能導致敏感資訊外洩。 建議:錯誤報告應僅包含概括性的描述,若必須包含內容片段,務必先對片段進行內容脫敏或格式化處理。
Error
@@ -60,2 +62,4 @@
GITEA_TOKEN: 'null',
});
assert.equal(config.token, null);
嚴重等級:🟡 警告 審查員:Bard 問題:測試檔案中的迴圈測試(for...of)將多種不同的錯誤案例綁在同一個測試名稱下,一旦其中一個案例失敗,難以快速辨識具體是哪一種格式導致驗證失效。 建議:建議將其拆分為獨立的測試案例,或在 assert.throws 中加入更具描述性的訊息。
assert.throws
@@ -55,2 +30,4 @@
assert.deepEqual(releases, [{ tag_name: 'v1.0.0' }]);
},
);
嚴重等級:🔵 建議 審查員:Bard 問題:與 config.test.js 相比,這裡的 jsonResponse 輔助函式定義在測試檔案上方,風格雖然簡潔,但若未來有更多測試檔案需要類似的 mock 工具,建議統一抽離至 test/support/ 之類的資料夾中,保持測試主體清爽。 建議:建立 app/test/support/mock.js 將這類常用的測試輔助函式集中管理。
config.test.js
jsonResponse
test/support/
app/test/support/mock.js
- config 將每段長度上限與允許字元抽為 MAX_REPO_NAME_LENGTH / REPO_SEGMENT_PATTERN(regex 提升至模組層級,避免每次重編譯) - releases 將片段截斷長度抽為 API_ERROR_SNIPPET_LENGTH Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
新增含空白/星號/冒號等非法字元,以及單段超過長度上限的拒絕測試。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
已解決 5 條(critical x1、warning x2、info x2:抽常數、提升 regex、補邊界測試),5 條判定為誤報寫入 exclusions(過度收緊 regex、迴圈測試已帶訊息、總長度冗餘、片段建議互相衝突、mock 過早抽離),findings 清空。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
本次審查(opencode / gemini-2.5-flash,共 13 次呼叫)
const REPO_SEGMENT_PATTERN = /^[A-Za-z0-9._-]+$/;
// 驗證 repository 為 owner/repo 格式(拒絕 . 與 .. 路徑穿越段、空段、過長或含非法字元的段)
嚴重等級:🔵 建議 審查員:Leo 問題:驗證邏輯 parts.every 內包含過多條件,邏輯稍顯複雜,閱讀起來需要花點時間拆解。 建議:將 parts.every 的回呼函數提取為具名函數(例如 isValidPart),以提升代碼的可讀性與未來維護的清晰度。
@@ -65,2 +68,3 @@
const contentSnippet = Array.from(text).slice(0, API_ERROR_SNIPPET_LENGTH).join('');
嚴重等級:🟡 警告 審查員:Leo 問題:為了處理多位元組字元而使用 Array.from(text),如果回傳的錯誤內容(text)非常巨大(例如幾十 MB 的 HTML 錯誤頁面),會瞬間配置大量記憶體,可能導致效能瓶頸。 建議:若預期回應可能非常大,建議先檢查 text.length 是否遠大於限制,或是考慮使用更輕量的字串處理方式來避免不必要的記憶體配置。
@@ -67,1 +69,4 @@
}
嚴重等級:🟡 警告 審查員:Mage 問題:雖然使用 Array.from(text) 可以正確處理多位元組字元(避免在 Unicode code point 中間截斷),但此操作會將整個回應內容轉為陣列,若 API 回傳的錯誤回應內容極大(例如數 MB 的 HTML 錯誤頁面),這會導致嚴重的記憶體耗用,甚至造成執行緒卡頓或記憶體溢位 (OOM)。 建議:應先對 text 進行長度檢查,或者改用 Buffer 或 string.substring() 搭配適當的 Unicode 感知邏輯來截斷,而不是將完整內容陣列化。
Array.from(text)
text
Buffer
string.substring()
@@ -70,0 +77,4 @@
test('fetchReleases 對非陣列回應拋錯', async () => {
嚴重等級:🟡 警告 審查員:Maya 問題:新增的 JSON 解析錯誤處理邏輯中,對於回應內容片段(contentSnippet)的生成與截斷行為並未進行驗證。目前的測試僅檢查錯誤訊息是否包含特定字串,未確保截斷邏輯在邊界條件(如超長回應、包含特殊字元)下是否正確。 建議:建議在 app/test/releases.test.js 中增加針對無法解析 JSON 的測試案例,並具體斷言錯誤訊息中包含預期的片段內容,特別是需要測試超長字串截斷是否符合預期。
app/test/releases.test.js
- releases 在 Array.from 前先以長度上限截斷字串,避免將可能極大的回應整個陣列化 - config 將 repository 區段驗證抽為具名函式 isValidRepoSegment 提升可讀性 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
新增超長無法解析回應的測試,斷言錯誤訊息中的片段截斷至 200 字元內。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
已解決 4 條(warning x3、info x1:片段記憶體安全、截斷測試、抽出 isValidRepoSegment),1 條判定為誤報寫入 exclusions(assertRepository 邊界測試前提已不成立),findings 清空。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
本次審查(opencode / gemini-2.5-flash,共 14 次呼叫)
// 單一 repository 區段是否合法:長度在範圍內、僅含允許字元,且非 . 或 .. 路徑穿越段
function isValidRepoSegment(part) {
return part.length > 0
&& part.length <= MAX_REPO_NAME_LENGTH
嚴重等級:🟡 警告 審查員:Bard 問題:函數 assertRepository 命名較為通用,但其實際行為僅在驗證 Gitea 的 owner/repo 格式。命名未能直接體現其檢查邏輯與該領域規則。 建議:建議重新命名為 assertGiteaRepositoryFormat,讓開發者一眼就能看出該函數在檢查特定的 Gitea 倉庫格式規範。
assertGiteaRepositoryFormat
@@ -64,3 +67,3 @@
pageJson = JSON.parse(text);
// 先以 UTF-16 長度粗略上限截斷(避免將可能極大的回應整個陣列化),
嚴重等級:🟡 警告 審查員:Maya 問題:雖然實作了針對多位元組字元的截斷處理邏輯,但目前的測試案例僅使用 ASCII 字元('x'),缺乏對於包含多位元組字元(如 Emoji 或特殊符號)的真實邊界情境驗證,無法確保在截斷邊界處不會產生亂碼或非預期的行為。 建議:請在 app/test/releases.test.js 中新增一個測試案例,使用包含多位元組字元(例如 Emoji 或代理對字元)的長字串作為 fetch 回應內容,並驗證截斷後的內容片段是否完整且無亂碼。
// 再以字元(而非 UTF-16 碼元)精準截斷,避免拆分多位元組字元造成亂碼
嚴重等級:🔵 建議 審查員:Leo 問題:在錯誤訊息中進行字串截斷處理,程式碼寫得較為複雜,使用了 Array.from(boundedText).slice(...)。雖然考慮了 UTF-16 碼元問題,但這段邏輯與核心業務功能(fetchReleases)混在一起,未來維護者若需調整錯誤訊息格式,容易誤傷功能。 建議:將此段截斷邏輯抽取為一個獨立的工具函式,例如 truncateString(text, limit),放在 utils 檔案中以提升可讀性與測試獨立性。
- releases 將片段截斷邏輯抽為可重用、可獨立測試的 truncateString(text, limit) 並 export - config 將 assertRepository 更名為 assertGiteaRepositoryFormat,凸顯其驗證 Gitea owner/repo 格式的領域語義 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
新增以 emoji(代理對)驗證截斷不拆分多位元組字元、不殘留落單代理碼元的測試。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
將 comment_token/secrets.COMMENT_TOKEN 改為 token/secrets.TOKEN,對齊 action 輸入。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
已解決 3 條(warning x2、info x1:抽出 truncateString、補多位元組截斷測試、assertRepository 改名),findings 清空。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
本次審查(opencode / gemini-2.5-flash,共 12 次呼叫)
嚴重等級:🔵 建議 審查員:Leo 問題:在 assertGiteaRepositoryFormat 函式中,當 value.split('/') 的長度不為 2 時,拋出的錯誤訊息僅籠統地說「格式錯誤」。若使用者輸入了包含多個斜線或完全沒有斜線的字串,這類訊息對修正環境變數幫助有限。 建議:建議區分「格式不符」與「內容不符」的錯誤細節,例如提示「必須為 owner/repo 格式,包含一個斜線」。
value.split('/')
@@ -6,0 +11,4 @@
function truncateString(text, limit) {
const bounded = text.slice(0, limit * 2);
return Array.from(bounded).slice(0, limit).join('');
嚴重等級:🟡 警告 審查員:Leo 問題:truncateString 使用 Array.from(bounded).slice(0, limit).join('') 的方式處理字串截斷,雖然能避免拆分代理對(surrogate pairs),但在處理極大字串(例如 API_ERROR_SNIPPET_LENGTH 很大時)會因為 Array.from 產生巨大的陣列而導致記憶體使用量激增。考慮到這是在解析失敗時處理的錯誤訊息,這種設計可能讓原本就已經吃緊的記憶體狀況雪上加霜。 建議:若不需要嚴格支援所有 Unicode 字元組合,考慮改用更節省記憶體的方式(如 Intl.Segmenter 或調整截斷邏輯),或者明確說明此處對記憶體的使用考量。若目的是為了錯誤記錄,或許直接截斷原始字串的長度後確保不要在最後一個字元產生半個代理對即可。
truncateString
Array.from(bounded).slice(0, limit).join('')
Intl.Segmenter
No dependencies set.
The note is not visible to the blocked user.
變更摘要
將單元測試整併至
app/test/(移除根目錄下無法被執行的重複test/目錄),並解決多輪 AI 程式碼審查的 findings(輸入驗證、錯誤訊息、測試覆蓋與測試品質)。對外契約(action inputs/outputs、版本進位與 beta 規則)維持不變。影響範圍
test/→app/test/(與app/原始碼同層,確保可被node --test執行)。config.loadConfig新增GITEA_REPOSITORY的owner/repo格式驗證(拒絕路徑穿越..、空段、過長與多段)。releasesJSON 解析失敗時,於錯誤訊息附上以字元安全截斷的回應內容片段。重點檔案
app/config.js:新增assertRepository(owner/repo格式 + 長度上限)。app/releases.js:JSON 解析錯誤訊息以Array.from依字元截斷片段,避免多位元組亂碼。app/test/logger.test.js(新增):以captureStream攔截 stdout/stderr,驗證 section/info/error 輸出格式。app/test/config.test.js、app/test/version.test.js:拆分混合測試、補 repository 邊界與 major 跨區段比較測試。.gitea/ai-review/:findings 清空、exclusions 累計記錄判定為誤報/過度設計的項目。風險與注意事項
app/test/(package.jsontest script)。GITEA_REPOSITORY驗證採白名單字元集(A-Za-z0-9._-)與單一/;若有特殊命名的 repo 需放寬,再行調整。驗證
node --test 'app/test/**/*.test.js':43 項測試全數通過。🤖 AI Code Review 團隊
AI Code Review 統計
🤖 AI 助理使用量
本次審查(opencode / gemini-2.5-flash,共 17 次呼叫)
剩餘可用
剩餘可用:無法計算百分比(自架服務,無帳號額度概念)
@@ -58,0 +59,4 @@function assertRepository(name, value) {const parts = value.split('/');const isRepoFormatValid = parts.length === 2&& parts.every((part) => part.length > 0 && part.length <= 100嚴重等級:🔴 嚴重
審查員:Maya
問題:新增的
assertRepository函數對GITEA_REPOSITORY的檢查邏輯並未測試到所有邊界條件,例如:包含特殊字元(非字母數字點下底線連字號)、極長字串段(> 100 字元)、以及完全空白的value。建議:請在
app/test/config.test.js中增加針對非法字元、超過 100 字元的字串段,以及空字串的測試案例,確保assertRepository的防禦性功能完全被驗證。嚴重等級:🟡 警告
審查員:Mage
問題:在
assertRepository中,parts.every僅檢查了單一區段長度,未對總體長度(owner/repo)做任何限制。建議:建議在分割前,先對
value的總長度進行合理範圍的限制。@@ -58,0 +60,4 @@const parts = value.split('/');const isRepoFormatValid = parts.length === 2&& parts.every((part) => part.length > 0 && part.length <= 100&& /^[A-Za-z0-9._-]+$/.test(part)嚴重等級:🔴 嚴重
審查員:Mage
問題:在
assertRepository函數中,part !== '.' && part !== '..'的檢查雖然能防止路徑穿越,但正規表達式^[A-Za-z0-9._-]+$仍然允許.開頭或結尾的檔案名稱(例如.foo或foo.),這在某些檔案系統中可能導致意外行為或被視為隱藏檔案,且未明確禁止僅由.組成的名稱(儘管後面有手動排除)。建議:建議調整正規表達式,禁止名稱以點號開頭或結尾,例如使用
/^[A-Za-z0-9_-]+([.][A-Za-z0-9_-]+)*$/來確保點號只能存在於中間。嚴重等級:🟡 警告
審查員:Leo
問題:在
assertRepository函式中使用了硬編碼的100作為長度限制,這是一個魔術數字,難以維護且無法從環境配置中調整。建議:建議將
100抽離為常數(例如MAX_REPO_NAME_LENGTH),提高可維護性。嚴重等級:🔵 建議
審查員:Bard
問題:正規表達式
/^[A-Za-z0-9._-]+$/描述稍顯冗長,且未利用字元類別的特性,略顯不夠優雅。建議:考慮簡化寫法,或確保該格式驗證邏輯與專案其他地方一致並抽取為常數。
@@ -58,0 +62,4 @@&& parts.every((part) => part.length > 0 && part.length <= 100&& /^[A-Za-z0-9._-]+$/.test(part)&& part !== '.' && part !== '..');if (!isRepoFormatValid) {嚴重等級:🟡 警告
審查員:Rogue
問題:在
every迴圈中重複宣告與編譯 Regex/^[A-Za-z0-9._-]+$/,若該函數被頻繁呼叫,將造成無謂的 CPU 與記憶體浪費。建議:將 Regex 移至函數外層宣告為常數。
@@ -65,2 +65,3 @@} catch {throw new Error(`release API 回傳資料無法解析 (page=${page})`);// 以字元(而非 UTF-16 碼元)截斷回傳內容片段,避免拆分多位元組字元造成亂碼const contentSnippet = Array.from(text).slice(0, 200).join('');嚴重等級:🔵 建議
審查員:Leo
問題:硬編碼了截斷字串長度
200,這是一個魔術數字,若未來需要調整截斷長度以容納更多偵錯資訊或減少輸出,維護者需要直接修改此處邏輯。建議:建議定義一個具名的常數(例如
API_ERROR_SNIPPET_LENGTH),讓此數字具備語意,且未來調整時只需改動一處。@@ -66,1 +66,3 @@throw new Error(`release API 回傳資料無法解析 (page=${page})`);// 以字元(而非 UTF-16 碼元)截斷回傳內容片段,避免拆分多位元組字元造成亂碼const contentSnippet = Array.from(text).slice(0, 200).join('');throw new Error(`release API 回傳資料無法解析 (page=${page}),回應內容片段:「${contentSnippet}」`);嚴重等級:🔵 建議
審查員:Assassin
問題:將未經清洗的 API 回應片段直接納入
Error物件,若 API 返回敏感資訊(內部路徑、堆疊追蹤、使用者資料),可能導致敏感資訊外洩。建議:錯誤報告應僅包含概括性的描述,若必須包含內容片段,務必先對片段進行內容脫敏或格式化處理。
@@ -60,2 +62,4 @@GITEA_TOKEN: 'null',});assert.equal(config.token, null);嚴重等級:🟡 警告
審查員:Bard
問題:測試檔案中的迴圈測試(for...of)將多種不同的錯誤案例綁在同一個測試名稱下,一旦其中一個案例失敗,難以快速辨識具體是哪一種格式導致驗證失效。
建議:建議將其拆分為獨立的測試案例,或在
assert.throws中加入更具描述性的訊息。@@ -55,2 +30,4 @@assert.deepEqual(releases, [{ tag_name: 'v1.0.0' }]);},);});嚴重等級:🔵 建議
審查員:Bard
問題:與
config.test.js相比,這裡的jsonResponse輔助函式定義在測試檔案上方,風格雖然簡潔,但若未來有更多測試檔案需要類似的 mock 工具,建議統一抽離至test/support/之類的資料夾中,保持測試主體清爽。建議:建立
app/test/support/mock.js將這類常用的測試輔助函式集中管理。🤖 AI Code Review 團隊
AI Code Review 統計
🤖 AI 助理使用量
本次審查(opencode / gemini-2.5-flash,共 13 次呼叫)
剩餘可用
剩餘可用:無法計算百分比(自架服務,無帳號額度概念)
@@ -58,0 +60,4 @@const REPO_SEGMENT_PATTERN = /^[A-Za-z0-9._-]+$/;// 驗證 repository 為 owner/repo 格式(拒絕 . 與 .. 路徑穿越段、空段、過長或含非法字元的段)function assertRepository(name, value) {嚴重等級:🔵 建議
審查員:Leo
問題:驗證邏輯 parts.every 內包含過多條件,邏輯稍顯複雜,閱讀起來需要花點時間拆解。
建議:將 parts.every 的回呼函數提取為具名函數(例如 isValidPart),以提升代碼的可讀性與未來維護的清晰度。
@@ -65,2 +68,3 @@} catch {throw new Error(`release API 回傳資料無法解析 (page=${page})`);// 以字元(而非 UTF-16 碼元)截斷回傳內容片段,避免拆分多位元組字元造成亂碼const contentSnippet = Array.from(text).slice(0, API_ERROR_SNIPPET_LENGTH).join('');嚴重等級:🟡 警告
審查員:Leo
問題:為了處理多位元組字元而使用 Array.from(text),如果回傳的錯誤內容(text)非常巨大(例如幾十 MB 的 HTML 錯誤頁面),會瞬間配置大量記憶體,可能導致效能瓶頸。
建議:若預期回應可能非常大,建議先檢查 text.length 是否遠大於限制,或是考慮使用更輕量的字串處理方式來避免不必要的記憶體配置。
@@ -67,1 +69,4 @@// 以字元(而非 UTF-16 碼元)截斷回傳內容片段,避免拆分多位元組字元造成亂碼const contentSnippet = Array.from(text).slice(0, API_ERROR_SNIPPET_LENGTH).join('');throw new Error(`release API 回傳資料無法解析 (page=${page}),回應內容片段:「${contentSnippet}」`);}嚴重等級:🟡 警告
審查員:Mage
問題:雖然使用
Array.from(text)可以正確處理多位元組字元(避免在 Unicode code point 中間截斷),但此操作會將整個回應內容轉為陣列,若 API 回傳的錯誤回應內容極大(例如數 MB 的 HTML 錯誤頁面),這會導致嚴重的記憶體耗用,甚至造成執行緒卡頓或記憶體溢位 (OOM)。建議:應先對
text進行長度檢查,或者改用Buffer或string.substring()搭配適當的 Unicode 感知邏輯來截斷,而不是將完整內容陣列化。@@ -70,0 +77,4 @@);});test('fetchReleases 對非陣列回應拋錯', async () => {嚴重等級:🟡 警告
審查員:Maya
問題:新增的 JSON 解析錯誤處理邏輯中,對於回應內容片段(contentSnippet)的生成與截斷行為並未進行驗證。目前的測試僅檢查錯誤訊息是否包含特定字串,未確保截斷邏輯在邊界條件(如超長回應、包含特殊字元)下是否正確。
建議:建議在
app/test/releases.test.js中增加針對無法解析 JSON 的測試案例,並具體斷言錯誤訊息中包含預期的片段內容,特別是需要測試超長字串截斷是否符合預期。🤖 AI Code Review 團隊
AI Code Review 統計
🤖 AI 助理使用量
本次審查(opencode / gemini-2.5-flash,共 14 次呼叫)
剩餘可用
剩餘可用:無法計算百分比(自架服務,無帳號額度概念)
@@ -58,0 +62,4 @@// 單一 repository 區段是否合法:長度在範圍內、僅含允許字元,且非 . 或 .. 路徑穿越段function isValidRepoSegment(part) {return part.length > 0&& part.length <= MAX_REPO_NAME_LENGTH嚴重等級:🟡 警告
審查員:Bard
問題:函數
assertRepository命名較為通用,但其實際行為僅在驗證 Gitea 的owner/repo格式。命名未能直接體現其檢查邏輯與該領域規則。建議:建議重新命名為
assertGiteaRepositoryFormat,讓開發者一眼就能看出該函數在檢查特定的 Gitea 倉庫格式規範。@@ -64,3 +67,3 @@pageJson = JSON.parse(text);} catch {throw new Error(`release API 回傳資料無法解析 (page=${page})`);// 先以 UTF-16 長度粗略上限截斷(避免將可能極大的回應整個陣列化),嚴重等級:🟡 警告
審查員:Maya
問題:雖然實作了針對多位元組字元的截斷處理邏輯,但目前的測試案例僅使用 ASCII 字元('x'),缺乏對於包含多位元組字元(如 Emoji 或特殊符號)的真實邊界情境驗證,無法確保在截斷邊界處不會產生亂碼或非預期的行為。
建議:請在 app/test/releases.test.js 中新增一個測試案例,使用包含多位元組字元(例如 Emoji 或代理對字元)的長字串作為 fetch 回應內容,並驗證截斷後的內容片段是否完整且無亂碼。
@@ -65,2 +68,3 @@} catch {throw new Error(`release API 回傳資料無法解析 (page=${page})`);// 先以 UTF-16 長度粗略上限截斷(避免將可能極大的回應整個陣列化),// 再以字元(而非 UTF-16 碼元)精準截斷,避免拆分多位元組字元造成亂碼嚴重等級:🔵 建議
審查員:Leo
問題:在錯誤訊息中進行字串截斷處理,程式碼寫得較為複雜,使用了 Array.from(boundedText).slice(...)。雖然考慮了 UTF-16 碼元問題,但這段邏輯與核心業務功能(fetchReleases)混在一起,未來維護者若需調整錯誤訊息格式,容易誤傷功能。
建議:將此段截斷邏輯抽取為一個獨立的工具函式,例如 truncateString(text, limit),放在 utils 檔案中以提升可讀性與測試獨立性。
🤖 AI Code Review 團隊
AI Code Review 統計
🤖 AI 助理使用量
本次審查(opencode / gemini-2.5-flash,共 12 次呼叫)
剩餘可用
剩餘可用:無法計算百分比(自架服務,無帳號額度概念)
@@ -58,0 +60,4 @@const REPO_SEGMENT_PATTERN = /^[A-Za-z0-9._-]+$/;// 單一 repository 區段是否合法:長度在範圍內、僅含允許字元,且非 . 或 .. 路徑穿越段function isValidRepoSegment(part) {嚴重等級:🔵 建議
審查員:Leo
問題:在
assertGiteaRepositoryFormat函式中,當value.split('/')的長度不為 2 時,拋出的錯誤訊息僅籠統地說「格式錯誤」。若使用者輸入了包含多個斜線或完全沒有斜線的字串,這類訊息對修正環境變數幫助有限。建議:建議區分「格式不符」與「內容不符」的錯誤細節,例如提示「必須為 owner/repo 格式,包含一個斜線」。
@@ -6,0 +11,4 @@function truncateString(text, limit) {const bounded = text.slice(0, limit * 2);return Array.from(bounded).slice(0, limit).join('');}嚴重等級:🟡 警告
審查員:Leo
問題:
truncateString使用Array.from(bounded).slice(0, limit).join('')的方式處理字串截斷,雖然能避免拆分代理對(surrogate pairs),但在處理極大字串(例如API_ERROR_SNIPPET_LENGTH很大時)會因為Array.from產生巨大的陣列而導致記憶體使用量激增。考慮到這是在解析失敗時處理的錯誤訊息,這種設計可能讓原本就已經吃緊的記憶體狀況雪上加霜。建議:若不需要嚴格支援所有 Unicode 字元組合,考慮改用更節省記憶體的方式(如
Intl.Segmenter或調整截斷邏輯),或者明確說明此處對記憶體的使用考量。若目的是為了錯誤記錄,或許直接截斷原始字串的長度後確保不要在最後一個字元產生半個代理對即可。Pull request closed