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 42 additions and 0 deletions
Showing only changes of commit 0f7ec5c3db - Show all commits
+14
View File
3
@@ -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([
+28
View File
3
@@ -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 });