import test from 'node:test'; import assert from 'node:assert/strict'; import { STYLES, listErchuangAnchors, buildErchuangPrompt, buildSegmentPrompt, parseSegments } from '../src/erchuang.js'; import { buildPack } from '../src/sucaibao.js'; test('锚点名录:战役 + 她传人物 + 体裁', () => { const a = listErchuangAnchors(); assert.ok(a.zhanzhen.length >= 11, '应含全部战役'); assert.ok(a.nvxing.length >= 18, '应含全部她传人物'); assert.ok(a.zhanzhen.every((x) => x.type === 'zhanzhen' && x.title && x.sub)); assert.ok(a.nvxing.every((x) => x.type === 'nvxing' && x.title && x.sub)); for (const k of ['ci', 'fu', 'ji', 'juben', 'shi', 'weishichen', 'duitan', 'gongzhi', 'huimu', 'gongxu']) { assert.ok(a.styles.some((s) => s.key === k), `体裁应含 ${k}`); } // 每体裁有 layer 标 for (const v of Object.values(STYLES)) assert.ok(['work', 'segment'].includes(v.layer), `${v.label} 须有 layer`); }); test('buildSegmentPrompt:骨架注入 + 出处自标铁律 + 黑名单/断代守卫', () => { const pack = buildPack('zhanzhen', 'chibi'); const { messages } = buildSegmentPrompt(pack, 'weishichen', {}); const sys = messages[0].content, user = messages[1].content; assert.ok(/\[\[f:key\|/.test(sys) && /\[\[y:子类\|/.test(sys), 'system 须给出 f/y 自标语法'); assert.ok(/对话永远是 y/.test(sys), '对话恒演绎铁律'); assert.ok(/演义黑名单/.test(sys) && /借东风/.test(sys), '须注入黑名单'); assert.ok(/断代=donghan/.test(sys), '须注入断代守卫'); assert.ok(user.includes('史实骨架') && /\[c\d\]/.test(user), 'user 须带编号骨架'); }); test('parseSegments:f挂cite/表外key降y/对话保子类/黑名单剔除/标点并段', () => { const table = { c1: '《三国志·周瑜传》:操军方连船舰' }; const r = parseSegments('[[f:c1|曹军船舰首尾相接]]。[[y:对话|黄盖低声:今夜可成]]。[[f:c9|某表外句]]。[[y:叙事|借东风起了]]。', table, { blacklist: ['借东风'], era: 'donghan' }); const f = r.segs.find((s) => s.text.includes('船舰')); assert.equal(f.t, 'f'); assert.ok(f.cite.includes('周瑜传')); assert.ok(f.text.endsWith('。'), '标点应并进史实段'); const dia = r.segs.find((s) => s.text.includes('黄盖')); assert.equal(dia.t, 'y'); assert.equal(dia.kind, '对话'); assert.ok(!dia.cite, '对话永不挂cite'); const wild = r.segs.find((s) => s.text.includes('表外')); assert.equal(wild.t, 'y', '表外key必须降为演绎(护城河)'); assert.ok(!r.segs.some((s) => s.text.includes('借东风')), '演义黑名单段被剔除'); }); test('buildErchuangPrompt:把双证史实卡拼进 prompt,含红线约束', () => { const fact = { title: '赤壁之战', sub: '孙刘联军 vs 曹军 · 208年', facts: ['火攻破曹'], cites: ['《三國志·吳書·周瑜傳》:「……」'] }; const { messages, styleLabel } = buildErchuangPrompt(fact, 'ci', { seed: '写周瑜的意气', role: '周瑜部将' }); assert.equal(styleLabel, '词'); assert.equal(messages[0].role, 'system'); assert.ok(/文学演绎/.test(messages[0].content) && /相悖的硬事实/.test(messages[0].content), 'system 须含创作≠史料红线'); assert.ok(messages[1].content.includes('赤壁之战') && messages[1].content.includes('《三國志'), 'user 须带真实事件+正史依据'); assert.ok(messages[1].content.includes('周瑜部将') && messages[1].content.includes('写周瑜的意气'), 'user 须带身份+方向'); }); test('未知体裁回落古体诗', () => { const { styleLabel } = buildErchuangPrompt({ title: 't', sub: 's', facts: [], cites: [] }, 'nope', {}); assert.equal(styleLabel, STYLES.shi.label); });