// 能力中枢助手:确定性部分的测试(检索 / 语音清洗 / 无 key 降级 / 总览)。 // 不设 DASHSCOPE / ANTHROPIC key,走 fallback 分支,无需网络。 delete process.env.DASHSCOPE_API_KEY; delete process.env.ANTHROPIC_API_KEY; import { test } from 'node:test'; import assert from 'node:assert/strict'; const { searchCapabilities, CAPABILITIES, getCapability, listCapabilities } = await import('../src/capabilities.js'); const { speechClean, toSegments, answer, overviewText } = await import('../src/services/assistant.js'); test('检索:口语提问能命中对应功能', () => { assert.equal(searchCapabilities('怎么克隆我的声音')[0].id, 'voice'); assert.equal(searchCapabilities('想让分身出镜说话拍视频')[0].id, 'talkvideo'); assert.equal(searchCapabilities('只想发给特定的人不公开')[0].id, 'share'); assert.equal(searchCapabilities('养得好不好怎么看')[0].id, 'grounding'); }); // 目录只能列【页面上真的存在】的功能。收费模式受 FEATURE_MARKETPLACE 门控、默认关闭—— // 关着的时候它必须彻底检索不到,否则助手会把用户送去一个页面上根本没有的按钮。 test('门控:marketplace 关闭时,收费模式不出现在目录/检索里', () => { assert.equal(searchCapabilities('怎么用分身挣钱').length, 0); assert.equal(getCapability('paid'), null); assert.ok(!listCapabilities().some((c) => c.id === 'paid')); }); test('语音清洗:去掉 Markdown / 网址 / 表情 / 序号', () => { const dirty = '# 标题\n1. 第一步 **加粗** `代码` 😀 看 https://x.com/y [链接](https://z.com)'; const clean = speechClean(dirty); assert.doesNotMatch(clean, /[#*`]/, '不应残留 Markdown 符号'); assert.doesNotMatch(clean, /https?:\/\//, '不应残留网址'); assert.doesNotMatch(clean, /😀/, '不应残留表情'); assert.match(clean, /链接/, '链接文字应保留'); }); test('分句:按句末标点切分', () => { const segs = toSegments('第一句。第二句!第三句?'); assert.equal(segs.length, 3); assert.equal(segs[0], '第一句。'); }); test('无 key 降级:命中功能时产出可朗读话术(不含 Markdown)', async () => { const r = await answer('怎么克隆声音'); assert.equal(r.source, 'fallback'); assert.ok(r.spoken.length > 0); assert.equal(r.matched[0].id, 'voice'); assert.ok(r.segments.length >= 1); assert.doesNotMatch(r.spoken, /[#*`]|https?:\/\//); // 韵律/情感输出:ssml(语调)+ style(情感风格)供合成网关使用。 assert.match(r.ssml, /^ { const r = await answer('你好呀今天天气不错'); // 走总览(多句) assert.match(r.ssml, / { const r = await answer('你好呀今天天气不错'); assert.equal(r.source, 'fallback'); assert.match(r.spoken, /能力助手|数字分身|想先了解/); }); test('总览话术非空且可朗读', () => { const t = overviewText(); assert.ok(t.length > 20); assert.doesNotMatch(t, /[#*`]/); }); test('每条能力都具备助手所需字段', () => { for (const c of CAPABILITIES) { for (const k of ['id', 'name', 'category', 'summary', 'howto', 'where', 'speak']) { assert.ok(c[k] !== undefined && c[k] !== '', `能力 ${c.id} 缺字段 ${k}`); } } });