test(roles 與 comments): 新增腳色載入器測試與嚴重問題 comment 的邊界/多筆案例
This commit is contained in:
@@ -150,4 +150,37 @@ describe('postNewCriticalComments', () => {
|
|||||||
assert.equal(inlineCalls.length, 0);
|
assert.equal(inlineCalls.length, 0);
|
||||||
assert.equal(issueCalls.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)));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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/);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user