Files
X-Financial/web/tests/steward-plan-off-topic.test.mjs
caoxiaozhu 0cde1f8990 feat(web): 工作台 AI 模式与差旅/风险建议交互优化
- 新增 PersonalWorkbenchAiMode 组件、AI 侧边栏与 orb 机器人视觉资源
- 新增 aiApplicationDraftModel / aiExpenseDraftModel / aiWorkbenchConversationStore
  及业务准入 aiSidebarBusinessAccess,支撑 AI 模式下的申请与报销草稿
- 顶栏、侧边栏、工作台样式重构,适配 AI 模式切换与响应式布局
- 同步 steward plan/off_topic、差旅报销引导流、风险建议卡片等测试
2026-06-18 22:12:24 +08:00

217 lines
8.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import assert from 'node:assert/strict'
import test from 'node:test'
import {
ASSISTANT_SCOPE_ACTION_FILL_COMPOSER
} from '../src/utils/assistantSessionScope.js'
import {
buildStewardPlanMessageText,
buildStewardSuggestedActions,
isOffTopicStewardPlan,
normalizeStewardPlan
} from '../src/views/scripts/stewardPlanModel.js'
import {
resolveSuggestedActionPrefill
} from '../src/utils/assistantSuggestedActionPrefill.js'
const OFF_TOPIC_PLAN = {
plan_id: 'steward-plan-off-topic',
plan_status: 'off_topic',
next_action: 'none',
tasks: [],
attachment_groups: [],
confirmation_groups: [],
candidate_flows: [],
summary: '很抱歉主人,目前小财管家只能帮您整理**费用申请**和**费用报销**这两类事项。',
suggested_prompts: [
'我想要申请明天去北京出差3天支撑客户现场实施',
'我要报销上周去上海的高铁票',
'差旅住宿标准是多少'
],
thinking_events: [
{
event_id: 'off-topic-explain',
stage: 'intent_review',
title: '未识别到财务业务意图',
content: '用户输入的内容没有匹配到费用申请或费用报销相关的业务信号。',
status: 'completed'
}
]
}
test('normalizeStewardPlan passes suggested_prompts through as trimmed strings', () => {
const normalized = normalizeStewardPlan({
plan_id: 'p-1',
plan_status: 'off_topic',
suggested_prompts: [
' 申请出差 ',
'',
null,
'报销高铁票'
]
})
assert.deepEqual(normalized.suggestedPrompts, ['申请出差', '报销高铁票'])
})
test('normalizeStewardPlan falls back to empty suggested prompts when missing', () => {
const normalized = normalizeStewardPlan({ plan_id: 'p-2', plan_status: 'off_topic' })
assert.deepEqual(normalized.suggestedPrompts, [])
})
test('isOffTopicStewardPlan returns true only for off_topic plan status', () => {
assert.equal(isOffTopicStewardPlan({ plan_status: 'off_topic' }), true)
assert.equal(isOffTopicStewardPlan({ plan_status: 'needs_flow_confirmation' }), false)
assert.equal(isOffTopicStewardPlan({}), false)
})
test('buildStewardPlanMessageText renders backend-provided off_topic summary verbatim', () => {
// off_topic 文案由后端(含 LLM生成前端只负责透传不再拼接标题/引导
const text = buildStewardPlanMessageText(OFF_TOPIC_PLAN)
assert.equal(text, OFF_TOPIC_PLAN.summary)
// 推荐话术本身不在正文里展示,而是作为按钮单独渲染,避免重复。
for (const prompt of OFF_TOPIC_PLAN.suggested_prompts) {
assert.equal(text.includes(prompt), false, `正文不应包含推荐话术:${prompt}`)
}
})
test('buildStewardPlanMessageText adapts greeting vs meaningless vs off_business summaries', () => {
// 问候场景:礼貌回应主人
const greetingText = buildStewardPlanMessageText({
plan_id: 'p-greeting',
plan_status: 'off_topic',
next_action: 'none',
summary: '### 您好主人,很高兴为您服务\n\n请问您今天要办理什么业务',
suggested_prompts: ['我想要申请明天去北京出差3天']
})
assert.match(greetingText, /您好主人/)
assert.match(greetingText, /请问您今天要办理什么业务/)
// 无意义场景:温和解释 + 引导换种说法
const meaninglessText = buildStewardPlanMessageText({
plan_id: 'p-meaningless',
plan_status: 'off_topic',
next_action: 'none',
summary: '### 这句话我暂时没识别到财务事项\n\n很抱歉主人目前小财管家只能帮您整理**费用申请**和**费用报销**。',
suggested_prompts: ['我想要申请明天去北京出差3天']
})
assert.match(meaninglessText, /这句话我暂时没识别到财务事项/)
assert.match(meaninglessText, /很抱歉主人/)
// 有意义但非业务场景LLM 生成的文案(这里 mock 模拟)
const llmText = '### 抱歉主人,这句话我暂时帮不上忙\n\n主人聊的是天气小财管家目前只能帮您整理**费用申请**和**费用报销**。'
const offBusinessText = buildStewardPlanMessageText({
plan_id: 'p-off-business',
plan_status: 'off_topic',
next_action: 'none',
summary: llmText,
suggested_prompts: ['我想要申请明天去北京出差3天']
})
assert.equal(offBusinessText, llmText)
})
test('buildStewardPlanMessageText falls back to client template when summary is missing', () => {
// 后端 summary 缺失时,前端有兜底文案保证体验不空白
const text = buildStewardPlanMessageText({
plan_id: 'p-empty',
plan_status: 'off_topic',
next_action: 'none',
suggested_prompts: []
})
assert.match(text, /这句话我暂时没识别到财务事项/)
assert.match(text, /费用申请.*费用报销|费用报销.*费用申请/)
})
test('buildStewardSuggestedActions returns fill_composer actions for off_topic plan', () => {
const actions = buildStewardSuggestedActions(OFF_TOPIC_PLAN)
assert.equal(actions.length, OFF_TOPIC_PLAN.suggested_prompts.length)
for (const action of actions) {
assert.equal(action.action_type, ASSISTANT_SCOPE_ACTION_FILL_COMPOSER)
assert.equal(typeof action.payload.fill_text, 'string')
assert.ok(action.payload.fill_text.length > 0)
assert.equal(action.payload.steward_plan_id, OFF_TOPIC_PLAN.plan_id)
}
})
test('buildStewardSuggestedActions truncates long off_topic prompt labels', () => {
const longPrompt = '我想要申请明天去北京出差3天支撑客户现场实施需要预订酒店和高铁票'
const actions = buildStewardSuggestedActions({
plan_id: 'p-long',
plan_status: 'off_topic',
suggested_prompts: [longPrompt]
})
assert.equal(actions.length, 1)
assert.ok(actions[0].label.length <= 27) // 24 字符 + "..."
assert.ok(actions[0].label.endsWith('...'))
// payload.fill_text 必须保留完整话术,不被截断
assert.equal(actions[0].payload.fill_text, longPrompt)
})
test('buildStewardSuggestedActions keeps short off_topic prompt labels intact', () => {
const shortPrompt = '差旅住宿标准是多少'
const actions = buildStewardSuggestedActions({
plan_id: 'p-short',
plan_status: 'off_topic',
suggested_prompts: [shortPrompt]
})
assert.equal(actions[0].label, shortPrompt)
assert.equal(actions[0].payload.fill_text, shortPrompt)
})
test('buildStewardSuggestedActions returns empty array for off_topic plan without prompts', () => {
const actions = buildStewardSuggestedActions({
plan_id: 'p-empty',
plan_status: 'off_topic',
suggested_prompts: []
})
assert.deepEqual(actions, [])
})
test('off_topic fill_composer action is resolved as composer prefill (fill not submit)', () => {
const actions = buildStewardSuggestedActions(OFF_TOPIC_PLAN)
const firstAction = actions[0]
const prefill = resolveSuggestedActionPrefill(firstAction)
assert.equal(prefill, OFF_TOPIC_PLAN.suggested_prompts[0])
})
test('resolveSuggestedActionPrefill reads payload.fill_text directly', () => {
assert.equal(
resolveSuggestedActionPrefill({
action_type: ASSISTANT_SCOPE_ACTION_FILL_COMPOSER,
payload: { fill_text: '帮我申请下周出差' }
}),
'帮我申请下周出差'
)
// 空字符串/空白应被忽略
assert.equal(
resolveSuggestedActionPrefill({
action_type: ASSISTANT_SCOPE_ACTION_FILL_COMPOSER,
payload: { fill_text: ' ' }
}),
''
)
})
test('off_topic branch does not break pending flow confirmation actions', () => {
// pending flow 不应被 off_topic 分支拦截
const actions = buildStewardSuggestedActions({
plan_id: 'steward-plan-pending-flow',
plan_status: 'needs_flow_confirmation',
next_action: 'confirm_flow',
pending_flow_confirmation: {
status: 'pending',
reason: '缺少申请或报销动作词。',
candidate_flows: [
{
flow_id: 'travel_application',
label: '补办出差申请',
confidence: 0.6,
ontology_fields: { location: '上海' },
missing_fields: []
}
]
}
})
assert.equal(actions.length, 1)
assert.equal(actions[0].payload.flow_id, 'travel_application')
})