feat(ai-review 對話收斂): 讀 PR review 留言判斷解決狀態並收斂 findings #42

Merged
jiantw83 merged 68 commits from ai-review-resolve/20260623-110950 into develop 2026-06-23 08:29:38 +00:00
2 changed files with 28 additions and 9 deletions
Showing only changes of commit cadfaff31d - Show all commits
+11 -9
View File
@@ -244,7 +244,7 @@ describe('postFindingsReview', () => {
assert.equal(reviewSeverityLabel({ body: '**嚴重等級**:高風險' }), undefined);
});
it('posts one review with statistics and sorted line comments', async () => {
it('posts inline comments only for new findings, not old ones', async () => {
const reviewCalls = [];
const findings = [
{ level: 'info', role: 'Maya', location: 'app/c.js:30', suggestion: 'I', is_new: true },
admin marked this conversation as resolved
Review

嚴重等級🔴 嚴重
審查員:Maya
問題:新增的 postFindingsReview 使用統計功能,但在測試中完全未驗證輸出內容。
建議:應斷言 reviewCalls[0].body 確實包含了預期的 usageSection 資訊與統計數據。

**嚴重等級**:🔴 嚴重 **審查員**:Maya **問題**:新增的 `postFindingsReview` 使用統計功能,但在測試中完全未驗證輸出內容。 **建議**:應斷言 `reviewCalls[0].body` 確實包含了預期的 `usageSection` 資訊與統計數據。
@@ -262,23 +262,23 @@ describe('postFindingsReview', () => {
assert.match(reviewCalls[0].body, /\| 類型 \| 🔴 嚴重 \| 🟡 警告 \| 🔵 建議 \|/);
assert.match(reviewCalls[0].body, /\| 舊問題 \| 1 筆 \| 0 筆 \| 0 筆 \|/);
assert.match(reviewCalls[0].body, /\| 新問題 \| 0 筆 \| 1 筆 \| 1 筆 \|/);
// 舊問題 app/a.jsis_new:false)不應被行內標註,僅新問題依嚴重等級排序後標註
assert.ok(!reviewCalls[0].comments.some(c => c.path === 'app/a.js'));
assert.deepEqual(
reviewCalls[0].comments.map(c => c.path),
['app/a.js', 'app/b.js', 'app/c.js'],
['app/b.js', 'app/c.js'],
);
assert.deepEqual(
reviewCalls[0].comments.map(reviewSeverityLabel),
REVIEW_SEVERITY_LABELS,
['🟡 警告', '🔵 建議'],
);
assert.deepEqual(
admin marked this conversation as resolved
Review

嚴重等級🟡 警告
審查員:Maya
問題:新增了 usageSection 功能,但測試案例中沒有驗證當 usageSection 為空字串或未傳入時,輸出的 body 是否正確排版(例如不會多出不必要的換行符號)。
建議:補充測試案例,驗證當 usageSection 為空時,輸出的 Markdown 結構是否如預期(沒有多餘的 `

` 結尾)。

**嚴重等級**:🟡 警告 **審查員**:Maya **問題**:新增了 `usageSection` 功能,但測試案例中沒有驗證當 `usageSection` 為空字串或未傳入時,輸出的 body 是否正確排版(例如不會多出不必要的換行符號)。 **建議**:補充測試案例,驗證當 `usageSection` 為空時,輸出的 Markdown 結構是否如預期(沒有多餘的 ` ` 結尾)。
Review

嚴重等級🔴 嚴重
審查員:Maya
問題:新增了 usageSection 功能,但測試案例中未針對該區段若包含惡意程式碼(例如注入 ## 🤖 AI 助理使用量)進行安全測試,若 usageSection 來源不可控,可能導致統計版面被偽造訊息覆蓋。
建議:補充一個測試案例,傳入帶有惡意 Markdown 格式或假統計資料的 usageSection,確認最終產出的 body 結構是否如預期被正確組裝,而非被惡意內容竄改結構。

**嚴重等級**:🔴 嚴重 **審查員**:Maya **問題**:新增了 `usageSection` 功能,但測試案例中未針對該區段若包含惡意程式碼(例如注入 `## 🤖 AI 助理使用量`)進行安全測試,若 `usageSection` 來源不可控,可能導致統計版面被偽造訊息覆蓋。 **建議**:補充一個測試案例,傳入帶有惡意 Markdown 格式或假統計資料的 `usageSection`,確認最終產出的 `body` 結構是否如預期被正確組裝,而非被惡意內容竄改結構。
reviewCalls[0].comments.map(c => c.new_position),
[10, 20, 30],
[20, 30],
);
assert.match(reviewCalls[0].comments[0].body, /嚴重等級/);
assert.match(reviewCalls[0].comments[0].body, /.*Rex/s);
assert.match(reviewCalls[0].comments[0].body, /.*/s);
assert.doesNotMatch(reviewCalls[0].comments[0].body, /.*app\/a\.js:10/s);
assert.match(reviewCalls[0].comments[0].body, /.*C/s);
assert.match(reviewCalls[0].comments[0].body, /.*Leo/s);
assert.match(reviewCalls[0].comments[0].body, /.*W/s);
});
it('appends the usage section to the review body when provided', async () => {
@@ -318,7 +318,9 @@ describe('postFindingsReview', () => {
assert.equal(reviewCalls.length, 1);
assert.match(reviewCalls[0].body, /\| 舊問題 \| 1 筆 \| 0 筆 \| 0 筆 \|/);
assert.match(reviewCalls[0].body, /\| 新問題 \| 0 筆 \| 1 筆 \| 1 筆 \|/);
assert.equal(reviewCalls[0].comments.length, 3);
// 統計含新舊(舊問題仍計入本文),但行內 comment 只給新問題(舊 critical 不標註)
assert.equal(reviewCalls[0].comments.length, 2);
assert.ok(!reviewCalls[0].comments.some(c => c.path === 'app/a.js'));
});
it('only adds comments for findings with parseable file and line', async () => {
+17
View File
2
@@ -171,6 +171,23 @@ describe('reconcileConversations', () => {
assert.deepEqual(result.carriedFindings.map(f => f.suggestion), ['fix two']);
});
it('resolves (not carries) an unresolved conversation already present in old findings', async () => {
const resolvedIds = [];
const deps = baseDeps();
deps.judge = async (items) => items.map(it => ({ idx: it.idx, resolved: false })); // 全部未修復
deps.resolveComment = async (id) => { resolvedIds.push(id); return { ok: true }; };
// b.js 的問題已存在於舊問題(檔案+建議簽章相符,行號不同不影響)→ 應解決對話、不加回
deps.oldFindings = [{ location: 'b.js:99', suggestion: 'fix two' }];
const result = await reconcileConversations(deps);
assert.deepEqual(resolvedIds, [2]); // 僅 b.js(id=2) 因已存在舊問題而被 resolve
assert.equal(result.duplicateCount, 1);
assert.equal(result.resolvedCount, 0);
assert.deepEqual(result.carriedFindings.map(f => f.suggestion), ['fix one']); // a.js 不在舊問題 → 加回
assert.deepEqual(result.resolvedFindings, []); // duplicate 不從舊問題移除
});
it('carries the conversation back when the resolve API call fails', async () => {
const deps = baseDeps();
deps.judge = async (items) => items.map(it => ({ idx: it.idx, resolved: true }));