78 lines
3.5 KiB
JavaScript
78 lines
3.5 KiB
JavaScript
// 能力中枢助手:确定性部分的测试(检索 / 语音清洗 / 无 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, /^<speak[\s\S]*<prosody/, 'ssml 应含 speak + prosody');
|
||
assert.equal(r.style, 'friendly');
|
||
});
|
||
|
||
test('多句话术的 ssml 含句间停顿 break', async () => {
|
||
const r = await answer('你好呀今天天气不错'); // 走总览(多句)
|
||
assert.match(r.ssml, /<break/, '多句 ssml 应含句间停顿');
|
||
});
|
||
|
||
test('无 key 降级:没命中时给平台总览引导', async () => {
|
||
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}`);
|
||
}
|
||
}
|
||
});
|