feat(ai-review 統計): 將統計改為新舊問題分列

This commit is contained in:
2026-06-23 02:20:44 +00:00
parent bc70aae3e0
commit 12c54212f0
3 changed files with 86 additions and 15 deletions
+45 -5
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 { saveFindings, parseLocation, postNewCriticalComments, postFindingsReview } from './comments.js';
import { saveFindings, parseLocation, postNewCriticalComments, postFindingsReview, formatFindingsStats } from './comments.js';
import { FINDINGS_PATH } from './config.js';
describe('saveFindings', () => {
@@ -96,6 +96,24 @@ describe('parseLocation', () => {
});
});
describe('formatFindingsStats', () => {
it('formats old and new findings by severity', () => {
const stats = formatFindingsStats([
{ level: 'critical', is_new: false },
{ level: 'warning', is_new: true },
{ level: 'info' },
{ level: 'custom', is_new: true },
]);
assert.equal(stats, [
'| 類型 | 🔴 嚴重 | 🟡 警告 | 🔵 建議 |',
'| --- | --- | --- | --- |',
'| 舊問題 | 1 筆 | 0 筆 | 0 筆 |',
'| 新問題 | 0 筆 | 1 筆 | 1 筆 |',
].join('\n'));
});
});
describe('postNewCriticalComments', () => {
const critical = { level: 'critical', role: 'Rex', location: 'app/preflight.js:19', suggestion: '修這個', is_new: true };
@@ -190,6 +208,11 @@ describe('postFindingsReview', () => {
const REVIEW_SEVERITY_LABELS = ['🔴 嚴重', '🟡 警告', '🔵 建議'];
const REVIEW_SEVERITY_PATTERN = new RegExp(`\\*\\*嚴重等級\\*\\*(${REVIEW_SEVERITY_LABELS.join('|')})(?:\\n|$)`);
/**
* 從 review comment body 擷取嚴重等級標籤。
* @param {object | null | undefined} comment - 預期包含 body 欄位的 review comment。
* @returns {string | undefined} 嚴重等級標籤;格式不符時回傳 undefined。
*/
function reviewSeverityLabel(comment) {
return comment?.body?.match(REVIEW_SEVERITY_PATTERN)?.[1];
}
@@ -221,14 +244,15 @@ describe('postFindingsReview', () => {
];
await postFindingsReview(findings, {
summaryFindings: findings.filter(f => f.is_new !== false),
summaryFindings: findings,
commentFindings: findings,
postReview: async (args) => { reviewCalls.push(args); },
});
assert.equal(reviewCalls.length, 1);
assert.match(reviewCalls[0].body, /\| 🔴 嚴重 \| 🟡 警告 \| 🔵 建議 \|/);
assert.match(reviewCalls[0].body, /\| 0 筆 \| 1 筆 \| 1 筆 \|/);
assert.match(reviewCalls[0].body, /\| 類型 \| 🔴 嚴重 \| 🟡 警告 \| 🔵 建議 \|/);
assert.match(reviewCalls[0].body, /\| 舊問題 \| 1 筆 \| 0 筆 \| 0 筆 \|/);
assert.match(reviewCalls[0].body, /\| 新問題 \| 0 筆 \| 1 筆 \| 1 筆 \|/);
assert.deepEqual(
reviewCalls[0].comments.map(c => c.path),
['app/a.js', 'app/b.js', 'app/c.js'],
@@ -248,6 +272,22 @@ describe('postFindingsReview', () => {
assert.match(reviewCalls[0].comments[0].body, /.*C/s);
});
it('separates old and new findings in default review statistics', async () => {
const reviewCalls = [];
await postFindingsReview([
{ level: 'critical', role: 'Rex', location: 'app/a.js:10', suggestion: 'old critical', is_new: false },
{ level: 'warning', role: 'Leo', location: 'app/b.js:20', suggestion: 'new warning', is_new: true },
{ level: 'info', role: 'Maya', location: 'app/c.js:30', suggestion: 'new info' },
], {
postReview: async (args) => { reviewCalls.push(args); },
});
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);
});
it('only adds comments for findings with parseable file and line', async () => {
const reviewCalls = [];
await postFindingsReview([
@@ -258,7 +298,7 @@ describe('postFindingsReview', () => {
});
assert.equal(reviewCalls.length, 1);
assert.match(reviewCalls[0].body, /\| 1 筆 \| 1 筆 \| 0 筆 \|/);
assert.match(reviewCalls[0].body, /\| 新問題 \| 1 筆 \| 1 筆 \| 0 筆 \|/);
assert.equal(reviewCalls[0].comments.length, 1);
assert.equal(reviewCalls[0].comments[0].path, 'app/b.js');
});