diff --git a/app/comments.test.js b/app/comments.test.js index e1c0792..08991bc 100644 --- a/app/comments.test.js +++ b/app/comments.test.js @@ -307,6 +307,20 @@ describe('postFindingsReview', () => { assert.equal(reviewCalls[0].body, reviewCalls[0].body.trimEnd()); }); + it('appends usageSection verbatim after the stats block without altering structure', async () => { + const reviewCalls = []; + const usageSection = '## 🤖 AI 助理使用量\n\n| x | y |\n| - | - |\n| 1 | 2 |'; + await postFindingsReview([ + { level: 'warning', role: 'Leo', location: 'app/b.js:20', suggestion: 'W', is_new: true }, + ], { postReview: async (args) => { reviewCalls.push(args); }, usageSection }); + + const body = reviewCalls[0].body; + // 統計區塊在前、usageSection 原樣接在後(中間一個空行);不交錯、不被竄改 + assert.ok(body.startsWith('## AI Code Review 統計')); + assert.ok(body.endsWith(usageSection)); + assert.match(body, /## AI Code Review 統計[\s\S]*\n\n## 🤖 AI 助理使用量/); + }); + it('counts both new and old findings in the summary but only inline-comments new ones', async () => { const reviewCalls = []; await postFindingsReview([ diff --git a/app/findings.test.js b/app/findings.test.js index a15ae03..8897733 100644 --- a/app/findings.test.js +++ b/app/findings.test.js @@ -244,6 +244,23 @@ describe('findings exclusions', () => { assert.equal(result.length, 1); // 只有明確 false_positive 才剔除,其餘保守保留 }); + it('keeps failed and confirmed, drops only confirmed false positives (mixed parallel)', async () => { + const findings = [ + { level: 'warning', role: 'A', location: 'a.js:1', problem: 'p', suggestion: 'fail' }, + { level: 'warning', role: 'B', location: 'b.js:2', problem: 'p', suggestion: 'fp' }, + { level: 'warning', role: 'C', location: 'c.js:3', problem: 'p', suggestion: 'ok' }, + ]; + const chatFn = async (_sys, user) => { + const loc = JSON.parse(user).location; + if (loc === 'a.js:1') throw new Error('boom'); // 失敗 → 保守保留 + if (loc === 'b.js:2') return { verdict: 'false_positive' };// 誤報 → 剔除 + return { verdict: 'confirmed' }; // 成立 → 保留 + }; + + const result = await filterFalsePositivesWithAI(findings, [], chatFn); + assert.deepEqual(result.map(f => f.location).sort(), ['a.js:1', 'c.js:3']); + }); + it('resolveMissingLineNumbers fills missing line numbers by re-asking the role', async () => { const findings = [ { level: 'critical', role: 'Maya', location: 'app/a.js', problem: 'p', suggestion: 's' }, @@ -281,6 +298,17 @@ describe('findings exclusions', () => { assert.equal(n, 3); // 嘗試 3 次後放棄 }); + it('resolveMissingLineNumbers swallows chatFn exceptions and keeps the filename', async () => { + const findings = [{ level: 'warning', role: 'Leo', location: 'app/z.js', problem: 'p', suggestion: 's' }]; + let n = 0; + const chatFn = async () => { n += 1; throw new Error('LLM down'); }; + + await resolveMissingLineNumbers(findings, 'd', { chatFn, getRole: () => null, maxAttempts: 2 }); + + assert.equal(findings[0].location, 'app/z.js'); // 例外被吞、保留檔名、不中斷流程 + assert.equal(n, 2); // 每次嘗試仍呼叫、受上限約束 + }); + it('logs exclusions file metadata and repo state when loading exclusions', () => { const fullPath = path.join(workspace, EXCLUSIONS_PATH); fs.mkdirSync(path.dirname(fullPath), { recursive: true });