test(ai-review): 補行號定位、誤報裁決邊界、統計與 appendExclusions 測試

This commit is contained in:
Jeffery
2026-06-23 15:36:35 +08:00
parent fbf83f1cbb
commit d03f08e19d
3 changed files with 97 additions and 2 deletions
+57 -1
View File
@@ -3,7 +3,7 @@ import assert from 'node:assert/strict';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { loadOldFindings, loadExclusions, applyExclusions, filterFalsePositivesWithAI, appendExclusions } from './findings.js';
import { loadOldFindings, loadExclusions, applyExclusions, filterFalsePositivesWithAI, appendExclusions, resolveMissingLineNumbers } from './findings.js';
import { EXCLUSIONS_PATH, FINDINGS_PATH } from './config.js';
describe('findings exclusions', () => {
@@ -59,6 +59,18 @@ describe('findings exclusions', () => {
assert.equal(merged.length, 2);
});
it('appendExclusions keeps same-path entries that have different original text', () => {
const fullPath = path.join(workspace, EXCLUSIONS_PATH);
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
fs.writeFileSync(fullPath, JSON.stringify([{ location: 'app/a.js:1', original_finding: '問題甲' }], null, 2));
appendExclusions(workspace, [{ location: 'app/a.js:5', original_finding: '問題乙', reason: 'r' }]);
const onDisk = JSON.parse(fs.readFileSync(fullPath, 'utf8'));
assert.equal(onDisk.length, 2); // 同檔但原文不同 → 視為不同排除條目,兩者皆保留
assert.deepEqual(onDisk.map(e => e.original_finding), ['問題甲', '問題乙']);
});
it('writes appended exclusions to both workspace and mirror dir', () => {
const repoRoot = path.join(workspace, 'repo');
fs.mkdirSync(repoRoot, { recursive: true });
@@ -225,6 +237,50 @@ describe('findings exclusions', () => {
assert.equal(result.length, 2);
});
it('keeps a finding when the defender returns an out-of-range verdict value', async () => {
const findings = [{ level: 'warning', role: 'Mage', location: 'a.js:1', problem: 'p', suggestion: 's' }];
const chatFn = async () => ({ verdict: 'maybe', reason: 'x' }); // 非 confirmed/false_positive
const result = await filterFalsePositivesWithAI(findings, [], chatFn);
assert.equal(result.length, 1); // 只有明確 false_positive 才剔除,其餘保守保留
});
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' },
{ level: 'warning', role: 'Leo', location: 'app/b.js:20', problem: 'p', suggestion: 's' }, // 已有行號 → 不動
];
let calls = 0;
const chatFn = async () => { calls += 1; return { line: 42 }; };
await resolveMissingLineNumbers(findings, 'diff --git a/app/a.js b/app/a.js\n@@ -1 +1 @@', { chatFn, getRole: () => ({ name: 'Maya' }) });
assert.equal(findings[0].location, 'app/a.js:42'); // 補上行號
assert.equal(findings[1].location, 'app/b.js:20'); // 不變
assert.equal(calls, 1); // 只對缺行號者呼叫
});
it('resolveMissingLineNumbers retries until a valid line appears', async () => {
const findings = [{ level: 'warning', role: 'Leo', location: 'app/x.js', problem: 'p', suggestion: 's' }];
let n = 0;
const chatFn = async () => { n += 1; return n < 3 ? { line: 0 } : { line: 7 }; };
await resolveMissingLineNumbers(findings, 'd', { chatFn, getRole: () => null, maxAttempts: 5 });
assert.equal(findings[0].location, 'app/x.js:7');
assert.equal(n, 3); // 第三次才給出有效行號
});
it('resolveMissingLineNumbers keeps the filename after exhausting retries', async () => {
const findings = [{ level: 'warning', role: 'Leo', location: 'app/y.js', problem: 'p', suggestion: 's' }];
let n = 0;
const chatFn = async () => { n += 1; return { line: 0 }; };
await resolveMissingLineNumbers(findings, 'd', { chatFn, getRole: () => null, maxAttempts: 3 });
assert.equal(findings[0].location, 'app/y.js'); // 仍保留檔名
assert.equal(n, 3); // 嘗試 3 次後放棄
});
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 });