diff --git a/app/comments.test.js b/app/comments.test.js index 4e4a96c..9632545 100644 --- a/app/comments.test.js +++ b/app/comments.test.js @@ -150,4 +150,37 @@ describe('postNewCriticalComments', () => { assert.equal(inlineCalls.length, 0); assert.equal(issueCalls.length, 0); }); + + it('posts nothing when given an empty findings array', async () => { + const inlineCalls = []; + const issueCalls = []; + await postNewCriticalComments([], { + postInline: async (args) => { inlineCalls.push(args); }, + postIssue: async (body) => { issueCalls.push(body); }, + }); + assert.equal(inlineCalls.length, 0); + assert.equal(issueCalls.length, 0); + }); + + it('handles multiple criticals, posting inline where possible and degrading the rest', async () => { + const inlineCalls = []; + const issueCalls = []; + const findings = [ + { ...critical, location: 'app/a.js:10', suggestion: 'A' }, // 有行號、inline 成功 + { ...critical, location: 'app/b.js', suggestion: 'B' }, // 無行號 → 降級為一般 comment + { ...critical, location: 'app/c.js:20', suggestion: 'C' }, // inline 拋錯 → 降級為一般 comment + ]; + await postNewCriticalComments(findings, { + postInline: async (args) => { + if (args.path === 'app/c.js') throw new Error('line not in diff'); + inlineCalls.push(args); + }, + postIssue: async (body) => { issueCalls.push(body); }, + }); + assert.equal(inlineCalls.length, 1); + assert.equal(inlineCalls[0].path, 'app/a.js'); + assert.equal(inlineCalls[0].line, 10); + assert.equal(issueCalls.length, 2); + assert.ok(issueCalls.every(b => /嚴重問題/.test(b))); + }); }); diff --git a/app/roles.test.js b/app/roles.test.js new file mode 100644 index 0000000..54d5c79 --- /dev/null +++ b/app/roles.test.js @@ -0,0 +1,83 @@ +import { describe, it } from 'node:test'; +import assert from 'node:assert/strict'; +import { parseRoleFile, loadRoles, loadRole, buildAnalysisPrompt, getRoleIntro } from './roles.js'; + +const SAMPLE = `--- +name: Tester +side: attack +focus: logic +badge: "🔮" +color: "#3B82F6" +personality: 冷靜嚴謹 +--- + +# Tester + +審查重點:邊界與空值。`; + +describe('parseRoleFile', () => { + it('parses frontmatter fields and trims the body', () => { + const role = parseRoleFile(SAMPLE); + assert.equal(role.name, 'Tester'); + assert.equal(role.side, 'attack'); + assert.equal(role.focus, 'logic'); + assert.equal(role.badge, '🔮'); + assert.equal(role.body, '# Tester\n\n審查重點:邊界與空值。'); + }); + + it('tolerates CRLF line endings', () => { + const role = parseRoleFile(SAMPLE.replace(/\n/g, '\r\n')); + assert.equal(role.name, 'Tester'); + assert.equal(role.focus, 'logic'); + }); + + it('throws when frontmatter is missing', () => { + assert.throws(() => parseRoleFile('# no frontmatter'), /frontmatter/); + }); +}); + +describe('loadRoles', () => { + it('loads only attack-side roles', () => { + const roles = loadRoles(); + assert.ok(roles.length > 0); + assert.ok(roles.every(r => r.side === 'attack')); + }); + + it('includes the expected attacker roster and excludes the defender', () => { + const names = loadRoles().map(r => r.name); + for (const expected of ['Bard', 'Mage', 'Rogue', 'Assassin', 'Leo', 'Maya']) { + assert.ok(names.includes(expected), `missing ${expected}`); + } + assert.ok(!names.includes('Paladin'), 'Paladin must not be an attacker'); + }); +}); + +describe('loadRole', () => { + it('returns the defender role by name, case-insensitively', () => { + const paladin = loadRole('paladin'); + assert.equal(paladin.name, 'Paladin'); + assert.equal(paladin.side, 'defend'); + }); + + it('returns null for an unknown role', () => { + assert.equal(loadRole('nobody'), null); + }); +}); + +describe('buildAnalysisPrompt', () => { + it('embeds the role name in the JSON contract and persona/body', () => { + const prompt = buildAnalysisPrompt(parseRoleFile(SAMPLE)); + assert.match(prompt, /"role": "Tester"/); + assert.match(prompt, /冷靜嚴謹/); + assert.match(prompt, /審查重點:邊界與空值/); + assert.match(prompt, /只回傳 JSON 陣列/); + }); +}); + +describe('getRoleIntro', () => { + it('renders a table row per role with its badge', () => { + const intro = getRoleIntro([parseRoleFile(SAMPLE)]); + assert.match(intro, /🔮 Tester/); + assert.match(intro, /logic/); + }); +});