180 lines
5.6 KiB
JavaScript
180 lines
5.6 KiB
JavaScript
'use strict';
|
||
|
||
const assert = require('node:assert/strict');
|
||
const test = require('node:test');
|
||
|
||
const review = require('../src/lib/review');
|
||
const diagnostics = require('../src/lib/diagnostics');
|
||
|
||
test('agentFailureDetail 預設不輸出 stderr/stdout 片段', () => {
|
||
const oldDebug = process.env.ACTIONS_STEP_DEBUG;
|
||
delete process.env.ACTIONS_STEP_DEBUG;
|
||
try {
|
||
const detail = diagnostics.agentFailureDetail({
|
||
ok: false,
|
||
error: Object.assign(new Error('boom'), { code: 1 }),
|
||
stderr: 'token=super-secret-value',
|
||
output: 'stdout with password=hidden',
|
||
});
|
||
assert.match(detail, /exit 1/);
|
||
assert.doesNotMatch(detail, /super-secret-value|password|stdout|stderr/);
|
||
} finally {
|
||
if (oldDebug === undefined) delete process.env.ACTIONS_STEP_DEBUG;
|
||
else process.env.ACTIONS_STEP_DEBUG = oldDebug;
|
||
}
|
||
});
|
||
|
||
test('agentFailureDetail 在 debug 模式輸出遮罩後片段', () => {
|
||
const oldDebug = process.env.ACTIONS_STEP_DEBUG;
|
||
process.env.ACTIONS_STEP_DEBUG = 'true';
|
||
try {
|
||
const detail = diagnostics.agentFailureDetail({
|
||
ok: false,
|
||
error: Object.assign(new Error('boom'), { code: 2 }),
|
||
stderr: 'Authorization: Bearer abcdefghijklmnopqrstuvwxyz1234567890',
|
||
output: 'token=abcdefghijklmnopqrstuvwxyz1234567890TOKEN',
|
||
});
|
||
assert.match(detail, /exit 2/);
|
||
assert.match(detail, /stderr:Authorization: \*\*\*/);
|
||
assert.match(detail, /stdout:token=\*\*\*/);
|
||
assert.doesNotMatch(detail, /abcdefghijklmnopqrstuvwxyz/);
|
||
} finally {
|
||
if (oldDebug === undefined) delete process.env.ACTIONS_STEP_DEBUG;
|
||
else process.env.ACTIONS_STEP_DEBUG = oldDebug;
|
||
}
|
||
});
|
||
|
||
test('agentFailureDetail 先限長再遮罩並處理常見 PII', () => {
|
||
const oldDebug = process.env.ACTIONS_STEP_DEBUG;
|
||
process.env.ACTIONS_STEP_DEBUG = 'true';
|
||
try {
|
||
const detail = diagnostics.agentFailureDetail({
|
||
ok: false,
|
||
error: Object.assign(new Error('boom'), { code: 3 }),
|
||
stderr: `${'x'.repeat(520)} token=should-not-be-scanned`,
|
||
output: 'user@example.test eyJaaaaaaaaaa.bbbbbbbbbb.cccccccccc',
|
||
});
|
||
assert.match(detail, /exit 3/);
|
||
assert.doesNotMatch(detail, /should-not-be-scanned|user@example.test|eyJaaaaaaaaaa/);
|
||
assert.match(detail, /stdout:\*\*\* \*\*\*/);
|
||
} finally {
|
||
if (oldDebug === undefined) delete process.env.ACTIONS_STEP_DEBUG;
|
||
else process.env.ACTIONS_STEP_DEBUG = oldDebug;
|
||
}
|
||
});
|
||
|
||
test('postOthersToIssue 依序送出 issue 留言以維持排序', async () => {
|
||
const calls = [];
|
||
let active = 0;
|
||
let maxActive = 0;
|
||
const fakeGitea = {
|
||
async createCommentOnIssue(ctx, issueNumber, body) {
|
||
active += 1;
|
||
maxActive = Math.max(maxActive, active);
|
||
calls.push({ ctx, issueNumber, body });
|
||
await new Promise((resolve) => setTimeout(resolve, 20));
|
||
active -= 1;
|
||
return { id: calls.length };
|
||
},
|
||
};
|
||
|
||
await review.postOthersToIssue({
|
||
ctx: { token: 'hidden' },
|
||
gitea: fakeGitea,
|
||
issueNumber: 7,
|
||
others: [
|
||
{ severity: '警告', reviewer: 'Maya', file: 'a.js', startLine: 1, endLine: 1, problem: 'p1', suggestion: 's1' },
|
||
{ severity: '建議', reviewer: 'Bard', file: 'b.js', startLine: 2, endLine: 2, problem: 'p2', suggestion: 's2' },
|
||
],
|
||
});
|
||
|
||
assert.equal(calls.length, 2);
|
||
assert.equal(calls[0].issueNumber, 7);
|
||
assert.equal(maxActive, 1);
|
||
});
|
||
|
||
test('resultFilesToCommit 在建問題模式有嚴重問題時仍提交 findings', () => {
|
||
assert.deepEqual(
|
||
review.resultFilesToCommit({
|
||
createIssue: true,
|
||
severeCount: 1,
|
||
relativePath: '.gitea/ai-review/findings/run.json',
|
||
exclusionsChanged: false,
|
||
}),
|
||
['.gitea/ai-review/findings/run.json'],
|
||
);
|
||
});
|
||
|
||
test('resultFilesToCommit 在建問題模式無嚴重問題時只提交 exclusions 異動', () => {
|
||
assert.deepEqual(
|
||
review.resultFilesToCommit({
|
||
createIssue: true,
|
||
severeCount: 0,
|
||
relativePath: '.gitea/ai-review/findings/run.json',
|
||
exclusionsChanged: true,
|
||
}),
|
||
['.gitea/ai-review/exclusions.json'],
|
||
);
|
||
});
|
||
|
||
test('shouldFailMissingResultCommit 在結果檔需要 push 但未成功時回報失敗', () => {
|
||
assert.equal(
|
||
review.shouldFailMissingResultCommit({
|
||
filesToCommit: ['.gitea/ai-review/findings/run.json'],
|
||
resultCommitted: false,
|
||
}),
|
||
true,
|
||
);
|
||
assert.equal(
|
||
review.shouldFailMissingResultCommit({
|
||
filesToCommit: ['.gitea/ai-review/findings/run.json'],
|
||
resultCommitted: true,
|
||
}),
|
||
false,
|
||
);
|
||
assert.equal(
|
||
review.shouldFailMissingResultCommit({
|
||
filesToCommit: [],
|
||
resultCommitted: false,
|
||
}),
|
||
false,
|
||
);
|
||
});
|
||
|
||
test('resolveOldIssueLinkComments 只標註舊追蹤 issue 連結', async () => {
|
||
const edited = [];
|
||
const fakeGitea = {
|
||
async whoAmI() {
|
||
return { login: 'bot' };
|
||
},
|
||
async listIssueComments() {
|
||
return [
|
||
{
|
||
id: 1,
|
||
user: { login: 'bot' },
|
||
body: '<!-- ai-code-review -->\n## 🔍 AI Code Review|已建立追蹤問題\nold',
|
||
},
|
||
{
|
||
id: 2,
|
||
user: { login: 'bot' },
|
||
body: '<!-- ai-code-review -->\n## 📋 變更摘要(送審 git diff)\nkeep',
|
||
},
|
||
{
|
||
id: 3,
|
||
user: { login: 'someone' },
|
||
body: '<!-- ai-code-review -->\n## 🔍 AI Code Review|已建立追蹤問題\nkeep',
|
||
},
|
||
];
|
||
},
|
||
async editIssueComment(ctx, commentId, body) {
|
||
edited.push({ ctx, commentId, body });
|
||
},
|
||
};
|
||
|
||
await review.resolveOldIssueLinkComments({ ctx: { token: 'hidden' }, gitea: fakeGitea });
|
||
|
||
assert.equal(edited.length, 1);
|
||
assert.equal(edited[0].commentId, 1);
|
||
assert.match(edited[0].body, /^> 〔已過時〕/);
|
||
});
|