Files
X-Financial/web/tests/steward-plan-off-topic.test.mjs
caoxiaozhu 43432534d8 feat(steward): 前端支持 off_topic 与引导话术
- assistantSessionScope.js:新增 ASSISTANT_SCOPE_ACTION_FILL_COMPOSER 常量
- assistantSuggestedActionPrefill.js:识别 fill_composer 与 payload.fill_text
- stewardPlanModel.js:normalizeStewardPlan 透传 suggestedPrompts;
  buildStewardPlanMessageText / buildStewardSuggestedActions
  新增 off_topic 分支,按钮填充输入框不提交
- useStewardPlanFlow.js:isPendingStewardActionMessage 排除 off_topic
- steward-plan-off-topic.test.mjs:覆盖 normalize/文案/按钮/兼容路径
2026-06-18 14:15:30 +08:00

181 lines
6.3 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 friendly off_topic guidance', () => {
const text = buildStewardPlanMessageText(OFF_TOPIC_PLAN)
assert.match(text, /小财管家没看懂这件事/)
// 推荐话术本身不在正文里展示,而是作为按钮单独渲染,避免重复。
for (const prompt of OFF_TOPIC_PLAN.suggested_prompts) {
assert.equal(text.includes(prompt), false, `正文不应包含推荐话术:${prompt}`)
}
})
test('buildStewardPlanMessageText keeps off_topic branch ahead of pending flow branch', () => {
// 即使 summary 缺省,也走 off_topic 分支而非默认任务文案
const text = buildStewardPlanMessageText({
plan_id: 'p-off-topic-default',
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')
})