fix(審查流程): 修正 bot 跳過、JSON 驗證與排除比對邊界
CI / 1. BUILD (pull_request) Successful in 3s
CI / 2. TEST (pull_request) Has been skipped
CI / 3. RESULT (pull_request) Has been skipped

This commit is contained in:
2026-07-11 12:18:24 +00:00
parent 268cd05211
commit 8f909e5397
10 changed files with 94 additions and 15 deletions
+4
View File
@@ -87,6 +87,10 @@ describe('parseLocation', () => {
assert.equal(parseLocation('app/preflight.test.js'), null);
});
it('returns null when the parsed line number is zero', () => {
assert.equal(parseLocation('app/preflight.js:0'), null);
});
it('returns null when multiple files are listed', () => {
assert.equal(parseLocation('Dockerfile, app/git.js, app/gitea.js'), null);
});
+15
View File
@@ -144,6 +144,21 @@ describe('findings exclusions', () => {
assert.equal(filtered[0].location, 'README.md:12');
});
it('applies pure text exclusions using the original finding text', () => {
const findings = [
{ location: 'src/app.ts:10', role: 'Maya', suggestion: 'Update tests' },
{ location: 'src/app.ts:11', role: 'Maya', suggestion: 'Keep this' },
];
const exclusions = [
{ original_finding: 'update tests' },
];
const filtered = applyExclusions(findings, exclusions);
assert.equal(filtered.length, 1);
assert.equal(filtered[0].suggestion, 'Keep this');
});
it('dedupes repeated exclusions when loading exclusions', () => {
const fullPath = path.join(workspace, EXCLUSIONS_PATH);
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
+10 -2
View File
@@ -197,17 +197,25 @@ describe('gitea', () => {
assert.equal(await getFileContentAtRef('missing.js', 'ref'), '');
});
it('shouldSkipBotCommit returns true when either sha or branch head is bot commit', async () => {
it('shouldSkipBotCommit returns true when either sha or branch head is a bot success commit, but not failure', async () => {
mock.method(axios, 'get', async (url) => {
if (url.includes('/git/commits/sha-bot')) {
return { data: { message: 'chore: update ai-review findings [ai-review-bot][failure]' } };
}
if (url.includes('/git/commits/sha-success')) {
return { data: { message: 'chore: update ai-review findings [ai-review-bot][success]' } };
}
if (url.includes('/branches/feat%2Ftest')) {
return { data: { commit: { id: 'sha-bot' } } };
}
if (url.includes('/branches/feat%2Fsuccess')) {
return { data: { commit: { id: 'sha-success' } } };
}
return { data: { message: 'regular commit' } };
});
await assert.equal(await shouldSkipBotCommit({ sha: 'sha-bot', branch: 'feat/test' }), true);
await assert.equal(await shouldSkipBotCommit({ sha: 'sha-bot', branch: 'feat/test' }), false);
await assert.equal(await shouldSkipBotCommit({ sha: 'sha-success', branch: 'feat/success' }), true);
await assert.equal(await shouldSkipBotCommit({ sha: 'sha-success', branch: 'feat/test' }), true);
assert.equal(getBotReviewOutcome('chore: update ai-review findings [ai-review-bot][failure]'), 'failure');
assert.equal(getBotReviewOutcome('chore: update ai-review findings [ai-review-bot][success]'), 'success');
assert.equal(getBotReviewOutcome('chore: update ai-review findings [ai-review-bot]'), 'unknown');
+12
View File
@@ -77,6 +77,18 @@ describe('json helpers', () => {
assert.equal(fs.readFileSync(fullPath, 'utf8'), '[]\n');
});
it('rejects repaired JSON that is not an array', async () => {
const fullPath = path.join(workspace, '.gitea/ai-review/findings.json');
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
fs.writeFileSync(fullPath, '{broken', 'utf8');
await assert.rejects(
() => validateJSONArrayFile(fullPath, '.gitea/ai-review/findings.json', async () => '{"ok":true}'),
/不是 JSON 陣列/,
);
assert.equal(fs.readFileSync(fullPath, 'utf8'), '{broken');
});
it('reads a valid JSON file whose size equals the maximum limit', async () => {
const fullPath = path.join(workspace, '.gitea/ai-review/findings.json');
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
+24
View File
@@ -83,6 +83,17 @@ describe('groupConversations', () => {
const convos = groupConversations([{ id: 1, path: 'a.js', original_position: 7, body: 'x' }]);
assert.equal(convos[0].line, 7);
});
it('keeps multiple bot findings on the same path and line', () => {
const comments = [
{ id: 1, path: 'a.js', position: 10, body: reviewBody('🔴 嚴重', 'Assassin', 'p1', 's1') },
{ id: 2, path: 'a.js', position: 10, body: reviewBody('🟡 警告', 'Mage', 'p2', 's2') },
];
const convos = groupConversations(comments);
assert.equal(convos.length, 1);
assert.equal(convos[0].botFindings.length, 2);
assert.deepEqual(convos[0].botFindings.map(f => f.role), ['Assassin', 'Mage']);
});
});
describe('codeWindow', () => {
@@ -207,6 +218,19 @@ describe('reconcileConversations', () => {
assert.equal(result.closedCount, 3);
});
it('preserves multiple bot findings when a grouped conversation is still open', async () => {
const deps = baseDeps();
deps.listComments = async () => [
{ id: 10, path: 'a.js', position: 5, body: reviewBody('🔴 嚴重', 'Assassin', 'p', 's10') },
{ id: 11, path: 'a.js', position: 5, body: reviewBody('🟡 警告', 'Mage', 'p', 's11') },
];
deps.judge = async (items) => items.map(it => ({ idx: it.idx, verdict: 'open' }));
const result = await reconcileConversations(deps);
assert.deepEqual(result.carriedFindings.map(f => f.suggestion).sort(), ['s10', 's11']);
});
it('counts only successful closes when some resolve calls fail', async () => {
const deps = baseDeps();
// a.js(id1) 關閉成功、b.js(id2) 關閉失敗(c.js 已 resolved 略過)