Files
prop-ai-hr/mobile-uni/tests/realtime-practice.test.mjs
T
admin 35b37b07f7 feat(practice): server-owned realtime personas with knowledge tool calling
- AihrRealtimePersonaRegistry whitelist: owner-calm (default), owner-impatient,
  digital-mentor (search_knowledge); persona binds default voice
- /realtime/session assembles session.update server-side (instructions/tools);
  browser forwards payload verbatim; Redis session record 30min with sliding renewal
- /realtime/tools/invoke validates login identity + Redis session ownership +
  persona tool whitelist, then read-only searchAuthorized; per-account rate limits
- source=realtime skips sop review and knowledge-gap persistence (Beta no-persist)
- frontend: persona cards replace 56-voice picker; serialized tool-call chain with
  soft-landing fallback
2026-07-25 11:13:04 +08:00

74 lines
3.9 KiB
JavaScript

import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
import test from 'node:test';
const source = (path) => readFile(new URL(path, import.meta.url), 'utf8');
test('实时陪练通过受认证代理接入,前端不携带供应商密钥、不自组会话配置', async () => {
const [service, page, backendService, backendSessionService, backendPersonaRegistry] = await Promise.all([
source('../src/services/realtime-practice.ts'),
source('../src/pages/user/practice/index.vue'),
source('../../backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrRealtimePracticeService.java'),
source('../../backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrRealtimeSessionService.java'),
source('../../backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrRealtimePersonaRegistry.java')
]);
// 传输与事件协议
assert.match(service, /\/api\/train\/practice\/realtime\/sdp/);
assert.match(service, /conversation\.item\.input_audio_transcription\.completed/);
assert.match(service, /response\.audio_transcript\.done/);
assert.match(service, /session\.created/);
assert.match(service, /session\.updated/);
assert.match(service, /activeAttempt/);
assert.match(service, /resumeAudio/);
// 方案B 架构:session.update 由服务端组装,浏览器只原样转发;instructions/音色不下放前端
assert.match(service, /\/api\/train\/practice\/realtime\/session/);
assert.match(service, /sendSessionUpdate/);
assert.doesNotMatch(service, /instructions/);
assert.doesNotMatch(service, /REALTIME_PRACTICE_VOICES|createRealtimePracticeSessionUpdate|normalizeRealtimePracticeVoice/);
assert.doesNotMatch(service, /semantic_vad/);
assert.doesNotMatch(service, /DASHSCOPE_API_KEY|Authorization:\s*Bearer/);
// function calling 工具桥:事件分支、串行链、软着陆兜底、回传协议
assert.match(service, /\/api\/train\/practice\/realtime\/tools\/invoke/);
assert.match(service, /response\.function_call_arguments\.done/);
assert.match(service, /function_call_output/);
assert.match(service, /response\.create/);
assert.match(service, /toolCallChain/);
assert.match(service, /工具暂时不可用,请直接回应员工/);
assert.match(service, /calling-tool/);
// 服务端是会话配置的唯一组装点
assert.match(backendSessionService, /session\.update/);
assert.match(backendSessionService, /semantic_vad/);
assert.match(backendSessionService, /TOOL_SEARCH_KNOWLEDGE/);
assert.match(backendPersonaRegistry, /search_knowledge/);
assert.match(backendPersonaRegistry, /owner-calm/);
assert.match(backendPersonaRegistry, /digital-mentor/);
assert.match(backendService, /实时陪练限流暂时不可用/);
// 页面:人设卡片选择 + 本地持久化;音色 picker 已删除
assert.match(page, /实时陪练 Beta/);
assert.match(page, /实时陪练人设/);
assert.match(page, /aihr_realtime_practice_persona/);
assert.match(page, /selectRealtimePersona/);
assert.match(page, /fetchRealtimePersonas/);
assert.match(page, /createRealtimeSession/);
assert.match(page, /正在查知识库/);
assert.doesNotMatch(page, /realtimeVoice|selectRealtimeVoice|aihr_realtime_practice_voice/);
});
test('工具调用串行化与竞态防护:链式执行、attempt 双检、stop 重置', async () => {
const service = await source('../src/services/realtime-practice.ts');
// 串行链必须基于上一次调用收尾,且带末端 catch 防毒化
assert.match(service, /toolCallChain = toolCallChain\.then/);
assert.match(service, /\.catch\(\(\) => \{/);
// stop() 递增 attempt 使迟到的工具回调失效,并重置链
assert.match(service, /activeAttempt \+= 1/);
assert.match(service, /toolCallChain = Promise\.resolve\(\)/);
// 回传前双检:attempt 仍当前 + 数据通道仍打开
assert.match(service, /!isCurrent\(attempt\) \|\| channel\.readyState !== 'open'/);
});