import { describe, it, afterEach } from 'node:test'; 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 } from './comments.js'; import { FINDINGS_PATH } from './config.js'; describe('saveFindings', () => { const tempDirs = []; const makeTempDir = prefix => { const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix)); tempDirs.push(dir); return dir; }; it('writes findings to workspace and mirror dirs when provided', () => { const workspace = makeTempDir('findings-ws-'); const mirrorDir = makeTempDir('findings-mirror-'); const findings = [{ level: 'warning', role: 'Leo', location: 'file.js:1', suggestion: 'test' }]; saveFindings(workspace, findings, mirrorDir); const workspaceText = fs.readFileSync(path.join(workspace, FINDINGS_PATH), 'utf8'); const mirrorText = fs.readFileSync(path.join(mirrorDir, FINDINGS_PATH), 'utf8'); assert.equal(workspaceText, JSON.stringify(findings, null, 2) + '\n'); assert.equal(mirrorText, JSON.stringify(findings, null, 2) + '\n'); }); it('writes only to workspace when mirrorDir is omitted', () => { const workspace = makeTempDir('findings-ws-'); const findings = [{ level: 'info', role: 'Maya', location: 'file.js:2', suggestion: 'note' }]; saveFindings(workspace, findings); const workspaceText = fs.readFileSync(path.join(workspace, FINDINGS_PATH), 'utf8'); assert.equal(workspaceText, JSON.stringify(findings, null, 2) + '\n'); }); it('does not duplicate writes when mirrorDir matches workspace', () => { const workspace = makeTempDir('findings-same-'); const findings = []; const writeCalls = []; const originalWriteFileSync = fs.writeFileSync; fs.writeFileSync = (...args) => { writeCalls.push(args[0]); return originalWriteFileSync(...args); }; try { saveFindings(workspace, findings, workspace); } finally { fs.writeFileSync = originalWriteFileSync; } assert.equal(writeCalls.length, 1); assert.equal(writeCalls[0], path.join(workspace, FINDINGS_PATH)); }); it('writes an empty JSON array when findings is empty', () => { const workspace = makeTempDir('findings-empty-'); saveFindings(workspace, []); const workspaceText = fs.readFileSync(path.join(workspace, FINDINGS_PATH), 'utf8'); assert.equal(workspaceText, '[]\n'); }); afterEach(() => { while (tempDirs.length > 0) { fs.rmSync(tempDirs.pop(), { recursive: true, force: true }); } }); }); describe('parseLocation', () => { it('parses file and single line', () => { assert.deepEqual(parseLocation('app/preflight.js:19'), { file: 'app/preflight.js', line: 19 }); }); it('uses the start line for a line range', () => { assert.deepEqual(parseLocation('app/preflight.js:70-82'), { file: 'app/preflight.js', line: 70 }); }); it('returns null when there is no line number', () => { assert.equal(parseLocation('app/preflight.test.js'), null); }); it('returns null when multiple files are listed', () => { assert.equal(parseLocation('Dockerfile, app/git.js, app/gitea.js'), null); }); it('returns null for non-string input', () => { assert.equal(parseLocation(undefined), null); }); }); describe('postNewCriticalComments', () => { const critical = { level: 'critical', role: 'Rex', location: 'app/preflight.js:19', suggestion: '修這個', is_new: true }; it('posts an inline review comment annotating file/line with level/role/suggestion', async () => { const inlineCalls = []; const issueCalls = []; await postNewCriticalComments([critical], { postInline: async (args) => { inlineCalls.push(args); }, postIssue: async (body) => { issueCalls.push(body); }, }); assert.equal(inlineCalls.length, 1); assert.equal(issueCalls.length, 0); assert.equal(inlineCalls[0].path, 'app/preflight.js'); assert.equal(inlineCalls[0].line, 19); assert.match(inlineCalls[0].body, /等級/); assert.match(inlineCalls[0].body, /審查員.*Rex/s); assert.match(inlineCalls[0].body, /建議.*修這個/s); }); it('falls back to a normal comment when the location has no line number', async () => { const inlineCalls = []; const issueCalls = []; await postNewCriticalComments([{ ...critical, location: 'app/preflight.js' }], { postInline: async (args) => { inlineCalls.push(args); }, postIssue: async (body) => { issueCalls.push(body); }, }); assert.equal(inlineCalls.length, 0); assert.equal(issueCalls.length, 1); assert.match(issueCalls[0], /嚴重問題/); }); it('falls back to a normal comment when the inline post fails', async () => { const issueCalls = []; await postNewCriticalComments([critical], { postInline: async () => { throw new Error('line not in diff'); }, postIssue: async (body) => { issueCalls.push(body); }, }); assert.equal(issueCalls.length, 1); assert.match(issueCalls[0], /嚴重問題/); }); it('only posts for new critical findings', async () => { const inlineCalls = []; const issueCalls = []; await postNewCriticalComments([ { ...critical, is_new: false }, { level: 'warning', role: 'Leo', location: 'a.js:1', suggestion: 'x', is_new: true }, ], { postInline: async (args) => { inlineCalls.push(args); }, postIssue: async (body) => { issueCalls.push(body); }, }); assert.equal(inlineCalls.length, 0); assert.equal(issueCalls.length, 0); }); });