'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: '\n## 🔍 AI Code Review|已建立追蹤問題\nold', }, { id: 2, user: { login: 'bot' }, body: '\n## 📋 變更摘要(送審 git diff)\nkeep', }, { id: 3, user: { login: 'someone' }, body: '\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, /^> 〔已過時〕/); });