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') })