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 陣列/); }); it('falls back to a default when focus is missing instead of showing undefined', () => { const prompt = buildAnalysisPrompt({ name: 'NoFocus', body: 'x' }); assert.doesNotMatch(prompt, /undefined/); }); }); 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/); }); it('renders empty cells instead of undefined when focus/personality are missing', () => { const intro = getRoleIntro([{ name: 'Bare' }]); assert.match(intro, /Bare/); assert.doesNotMatch(intro, /undefined/); }); });