2026-06-18 14:15:30 +08:00
|
|
|
|
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: [],
|
2026-06-18 22:12:24 +08:00
|
|
|
|
summary: '很抱歉主人,目前小财管家只能帮您整理**费用申请**和**费用报销**这两类事项。',
|
2026-06-18 14:15:30 +08:00
|
|
|
|
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)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-06-18 22:12:24 +08:00
|
|
|
|
test('buildStewardPlanMessageText renders backend-provided off_topic summary verbatim', () => {
|
|
|
|
|
|
// off_topic 文案由后端(含 LLM)生成,前端只负责透传,不再拼接标题/引导
|
2026-06-18 14:15:30 +08:00
|
|
|
|
const text = buildStewardPlanMessageText(OFF_TOPIC_PLAN)
|
2026-06-18 22:12:24 +08:00
|
|
|
|
assert.equal(text, OFF_TOPIC_PLAN.summary)
|
2026-06-18 14:15:30 +08:00
|
|
|
|
// 推荐话术本身不在正文里展示,而是作为按钮单独渲染,避免重复。
|
|
|
|
|
|
for (const prompt of OFF_TOPIC_PLAN.suggested_prompts) {
|
|
|
|
|
|
assert.equal(text.includes(prompt), false, `正文不应包含推荐话术:${prompt}`)
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-06-18 22:12:24 +08:00
|
|
|
|
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 缺失时,前端有兜底文案保证体验不空白
|
2026-06-18 14:15:30 +08:00
|
|
|
|
const text = buildStewardPlanMessageText({
|
2026-06-18 22:12:24 +08:00
|
|
|
|
plan_id: 'p-empty',
|
2026-06-18 14:15:30 +08:00
|
|
|
|
plan_status: 'off_topic',
|
|
|
|
|
|
next_action: 'none',
|
2026-06-18 22:12:24 +08:00
|
|
|
|
suggested_prompts: []
|
2026-06-18 14:15:30 +08:00
|
|
|
|
})
|
2026-06-18 22:12:24 +08:00
|
|
|
|
assert.match(text, /这句话我暂时没识别到财务事项/)
|
2026-06-18 14:15:30 +08:00
|
|
|
|
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')
|
|
|
|
|
|
})
|