- 新增 WorkbenchAiFilePreviewDialog 附件预览对话框及 useWorkbenchAiFilePreview,附件支持点击预览 - 新增 attachmentAssociationJobs/linkedReimbursementDraftJobs 前端服务与对应 composable,接入后台任务轮询与状态展示 - 新增 travelReimbursementDraftBranchModel 草稿分支模型,报销关联门控支持跳过/选择草稿 - PersonalWorkbenchAiMode 及各 composable(expense/document/steward/application-preview/attachment-association)重构适配,WorkbenchAiComposer/FileStrip 样式与交互完善 - DocumentsCenter/ReceiptFolder/TravelReimbursementCreate 等视图及 scripts 重构,风险/差旅规划/审批等工具适配 - 新增/更新前端测试:application-result-card、reimbursement-list-preview-fetch、guided-flow、composer-components 等
515 lines
25 KiB
JavaScript
515 lines
25 KiB
JavaScript
import assert from 'node:assert/strict'
|
||
import { readFileSync } from 'node:fs'
|
||
import test from 'node:test'
|
||
import { fileURLToPath } from 'node:url'
|
||
|
||
import {
|
||
APPLICATION_WELCOME_QUICK_ACTIONS,
|
||
APPROVAL_WELCOME_QUICK_ACTIONS,
|
||
ASSISTANT_SESSION_MODE_OPTIONS,
|
||
EXPENSE_WELCOME_QUICK_ACTIONS,
|
||
SESSION_TYPE_APPLICATION,
|
||
SESSION_TYPE_APPROVAL,
|
||
SESSION_TYPE_EXPENSE,
|
||
SESSION_TYPE_KNOWLEDGE,
|
||
buildWelcomeQuickActions
|
||
} from '../src/views/scripts/travelReimbursementConversationModel.js'
|
||
import {
|
||
GUIDED_ACTION_CONFIRM_REIMBURSEMENT_REVIEW,
|
||
GUIDED_ACTION_CONTINUE_FILLING,
|
||
GUIDED_ACTION_OPEN_TRAVEL_CALCULATOR,
|
||
GUIDED_ACTION_START_APPLICATION,
|
||
GUIDED_ACTION_PROCESS_INTERRUPTION,
|
||
GUIDED_ACTION_SELECT_EXPENSE_TYPE,
|
||
GUIDED_ACTION_SELECT_REQUIRED_APPLICATION,
|
||
GUIDED_ACTION_SELECT_QUERY_MODE,
|
||
GUIDED_ACTION_SELECT_QUERY_STATUS,
|
||
GUIDED_ACTION_START_REIMBURSEMENT,
|
||
GUIDED_ACTION_START_STATUS_QUERY,
|
||
GUIDED_FLOW_MODE_REIMBURSEMENT,
|
||
GUIDED_FLOW_MODE_STATUS_QUERY,
|
||
applyGuidedReimbursementAnswer,
|
||
buildGuidedExpenseTypeActions,
|
||
buildGuidedInterruptionActions,
|
||
buildGuidedQueryModeActions,
|
||
buildGuidedQueryStatusActions,
|
||
buildGuidedReimbursementSummaryText,
|
||
buildGuidedReviewConfirmationActions,
|
||
buildGuidedReviewSubmitOptions,
|
||
buildGuidedStatusQueryText,
|
||
buildGuidedStepPromptText,
|
||
createEmptyGuidedFlowState,
|
||
createGuidedReimbursementState,
|
||
createGuidedStatusQueryState,
|
||
isGuidedReimbursementReadyForReview,
|
||
normalizeGuidedFlowState,
|
||
selectGuidedRequiredApplication,
|
||
selectGuidedExpenseType,
|
||
selectGuidedQueryMode,
|
||
shouldConfirmGuidedInterruption,
|
||
waitForGuidedApplicationSelection
|
||
} from '../src/views/scripts/travelReimbursementGuidedFlowModel.js'
|
||
import {
|
||
buildRequiredApplicationActions,
|
||
buildRequiredApplicationMissingText,
|
||
buildRequiredApplicationSelectionText,
|
||
filterRequiredApplicationCandidates,
|
||
requiresApplicationBeforeReimbursement
|
||
} from '../src/views/scripts/travelReimbursementApplicationLinkModel.js'
|
||
import {
|
||
SKIP_REQUIRED_APPLICATION_LINK_ACTION,
|
||
buildReimbursementAssociationActions,
|
||
buildReimbursementAssociationMissingText,
|
||
buildReimbursementAssociationQueryPayload,
|
||
buildReimbursementAssociationSelectionText,
|
||
buildReimbursementAssociationSubmitOptions,
|
||
buildReimbursementAssociationThinkingEvents
|
||
} from '../src/views/scripts/travelReimbursementAssociationGateModel.js'
|
||
import {
|
||
ASSISTANT_SCOPE_ACTION_SWITCH,
|
||
resolveAssistantScopeGuard
|
||
} from '../src/utils/assistantSessionScope.js'
|
||
|
||
const createViewScript = readFileSync(
|
||
fileURLToPath(new URL('../src/views/scripts/TravelReimbursementCreateView.js', import.meta.url)),
|
||
'utf8'
|
||
)
|
||
const guidedFlowScript = readFileSync(
|
||
fileURLToPath(new URL('../src/views/scripts/useTravelReimbursementGuidedFlow.js', import.meta.url)),
|
||
'utf8'
|
||
)
|
||
const suggestedActionsScript = readFileSync(
|
||
fileURLToPath(new URL('../src/views/scripts/useTravelReimbursementSuggestedActions.js', import.meta.url)),
|
||
'utf8'
|
||
)
|
||
const guidedModelScript = readFileSync(
|
||
fileURLToPath(new URL('../src/views/scripts/travelReimbursementGuidedFlowModel.js', import.meta.url)),
|
||
'utf8'
|
||
)
|
||
const sessionStateScript = readFileSync(
|
||
fileURLToPath(new URL('../src/views/scripts/useTravelReimbursementSessionState.js', import.meta.url)),
|
||
'utf8'
|
||
)
|
||
const submitComposerScript = readFileSync(
|
||
fileURLToPath(new URL('../src/views/scripts/useTravelReimbursementSubmitComposer.js', import.meta.url)),
|
||
'utf8'
|
||
)
|
||
const submitDraftPreflightScript = readFileSync(
|
||
fileURLToPath(new URL('../src/views/scripts/travelReimbursementSubmitDraftPreflight.js', import.meta.url)),
|
||
'utf8'
|
||
)
|
||
const messageHandlersScript = readFileSync(
|
||
fileURLToPath(new URL('../src/views/scripts/useTravelReimbursementCreateViewMessageHandlers.js', import.meta.url)),
|
||
'utf8'
|
||
)
|
||
|
||
test('assistant session modes expose independent quick actions', () => {
|
||
assert.deepEqual(
|
||
ASSISTANT_SESSION_MODE_OPTIONS.map((item) => item.label),
|
||
['小财管家', '申请助手', '报销助手', '审核助手', '财务知识助手', '预算编制助手']
|
||
)
|
||
assert.deepEqual(
|
||
EXPENSE_WELCOME_QUICK_ACTIONS.map((item) => item.label),
|
||
['快速发起报销', '查询单据状态', '差旅计算器']
|
||
)
|
||
assert.deepEqual(
|
||
EXPENSE_WELCOME_QUICK_ACTIONS.map((item) => item.action),
|
||
[
|
||
GUIDED_ACTION_START_REIMBURSEMENT,
|
||
GUIDED_ACTION_START_STATUS_QUERY,
|
||
GUIDED_ACTION_OPEN_TRAVEL_CALCULATOR
|
||
]
|
||
)
|
||
assert.ok(EXPENSE_WELCOME_QUICK_ACTIONS.every((item) => !item.prompt))
|
||
assert.equal(buildWelcomeQuickActions(SESSION_TYPE_EXPENSE).length, 3)
|
||
assert.deepEqual(
|
||
buildWelcomeQuickActions(SESSION_TYPE_APPLICATION).map((item) => item.label),
|
||
APPLICATION_WELCOME_QUICK_ACTIONS.map((item) => item.label)
|
||
)
|
||
assert.equal(buildWelcomeQuickActions(SESSION_TYPE_APPLICATION)[0].action, GUIDED_ACTION_START_APPLICATION)
|
||
assert.ok(!buildWelcomeQuickActions(SESSION_TYPE_APPLICATION)[0].prompt)
|
||
assert.ok(
|
||
buildWelcomeQuickActions(SESSION_TYPE_APPLICATION).every((item) => item.action !== GUIDED_ACTION_OPEN_TRAVEL_CALCULATOR)
|
||
)
|
||
assert.ok(
|
||
buildWelcomeQuickActions(SESSION_TYPE_APPROVAL).every((item) => item.action !== GUIDED_ACTION_OPEN_TRAVEL_CALCULATOR)
|
||
)
|
||
assert.match(guidedFlowScript, /GUIDED_ACTION_START_APPLICATION/)
|
||
assert.match(guidedFlowScript, /buildApplicationTemplatePreview/)
|
||
assert.deepEqual(
|
||
buildWelcomeQuickActions(SESSION_TYPE_APPROVAL).map((item) => item.label),
|
||
APPROVAL_WELCOME_QUICK_ACTIONS.map((item) => item.label)
|
||
)
|
||
assert.notDeepEqual(
|
||
buildWelcomeQuickActions(SESSION_TYPE_KNOWLEDGE).map((item) => item.label),
|
||
EXPENSE_WELCOME_QUICK_ACTIONS.map((item) => item.label),
|
||
'knowledge hot questions should stay independent'
|
||
)
|
||
})
|
||
|
||
test('assistant session scope guard keeps business boundaries isolated', () => {
|
||
const expenseInApplication = resolveAssistantScopeGuard('我想报销的士票', SESSION_TYPE_APPLICATION)
|
||
assert.equal(expenseInApplication.targetSessionType, SESSION_TYPE_EXPENSE)
|
||
assert.match(expenseInApplication.text, /申请助手/)
|
||
assert.match(expenseInApplication.text, /报销助手/)
|
||
assert.equal(expenseInApplication.suggestedActions[0].action_type, ASSISTANT_SCOPE_ACTION_SWITCH)
|
||
assert.equal(expenseInApplication.suggestedActions[0].payload.session_type, SESSION_TYPE_EXPENSE)
|
||
assert.equal(expenseInApplication.suggestedActions[0].payload.carry_text, '我想报销的士票')
|
||
|
||
assert.equal(resolveAssistantScopeGuard('我想发起一笔费用申请', SESSION_TYPE_APPLICATION), null)
|
||
assert.equal(
|
||
resolveAssistantScopeGuard('去北京出差3天,支撑国网仿生产环境部署', SESSION_TYPE_EXPENSE).targetSessionType,
|
||
SESSION_TYPE_APPLICATION
|
||
)
|
||
assert.equal(
|
||
resolveAssistantScopeGuard('去国网出差3天,协助仿生产环境部署', SESSION_TYPE_EXPENSE).targetSessionType,
|
||
SESSION_TYPE_APPLICATION
|
||
)
|
||
assert.equal(
|
||
resolveAssistantScopeGuard('下周去上海支撑客户系统上线,预计3天', SESSION_TYPE_EXPENSE).targetSessionType,
|
||
SESSION_TYPE_APPLICATION
|
||
)
|
||
assert.equal(
|
||
resolveAssistantScopeGuard('安排去深圳客户现场验收项目', SESSION_TYPE_EXPENSE).targetSessionType,
|
||
SESSION_TYPE_APPLICATION
|
||
)
|
||
assert.equal(
|
||
resolveAssistantScopeGuard('我要报销去北京出差的费用', SESSION_TYPE_APPLICATION).targetSessionType,
|
||
SESSION_TYPE_EXPENSE
|
||
)
|
||
assert.equal(
|
||
resolveAssistantScopeGuard('帮我查询待我审核的单据', SESSION_TYPE_EXPENSE).targetSessionType,
|
||
SESSION_TYPE_APPROVAL
|
||
)
|
||
assert.equal(
|
||
resolveAssistantScopeGuard('差旅住宿标准是多少', SESSION_TYPE_EXPENSE).targetSessionType,
|
||
SESSION_TYPE_KNOWLEDGE
|
||
)
|
||
assert.equal(
|
||
resolveAssistantScopeGuard('报销标准是多少', SESSION_TYPE_EXPENSE).targetSessionType,
|
||
SESSION_TYPE_KNOWLEDGE
|
||
)
|
||
assert.equal(
|
||
resolveAssistantScopeGuard('解释这张单据酒店超标风险', SESSION_TYPE_EXPENSE, { hasActiveReviewPayload: true }),
|
||
null
|
||
)
|
||
})
|
||
|
||
test('guided reimbursement asks type first and walks travel fields in order', () => {
|
||
const typeActions = buildGuidedExpenseTypeActions()
|
||
assert.deepEqual(
|
||
typeActions.map((action) => action.label),
|
||
['差旅费', '交通费', '住宿费', '业务招待费', '办公用品费', '其他费用']
|
||
)
|
||
assert.ok(typeActions.every((action) => action.action_type === GUIDED_ACTION_SELECT_EXPENSE_TYPE))
|
||
|
||
let state = createGuidedReimbursementState()
|
||
assert.equal(state.mode, GUIDED_FLOW_MODE_REIMBURSEMENT)
|
||
assert.equal(state.stepKey, 'expense_type')
|
||
|
||
state = selectGuidedExpenseType(state, 'travel')
|
||
assert.equal(state.stepKey, 'reason')
|
||
assert.match(buildGuidedStepPromptText(state), /第 1 步:事由/)
|
||
|
||
state = applyGuidedReimbursementAnswer(state, '去上海支持上海电力部署项目')
|
||
assert.equal(state.stepKey, 'location')
|
||
state = applyGuidedReimbursementAnswer(state, '上海')
|
||
assert.equal(state.stepKey, 'time_range')
|
||
state = applyGuidedReimbursementAnswer(state, '2026-05-20 至 2026-05-23,出差 3 天')
|
||
assert.equal(state.stepKey, 'amount')
|
||
state = applyGuidedReimbursementAnswer(state, '待核算')
|
||
assert.equal(state.stepKey, 'attachments')
|
||
state = applyGuidedReimbursementAnswer(state, '稍后上传')
|
||
|
||
assert.ok(isGuidedReimbursementReadyForReview(state))
|
||
assert.match(buildGuidedReimbursementSummaryText(state), /已完成“差旅费”的引导填写/)
|
||
assert.deepEqual(
|
||
buildGuidedReviewConfirmationActions().map((action) => action.action_type),
|
||
[GUIDED_ACTION_CONFIRM_REIMBURSEMENT_REVIEW]
|
||
)
|
||
|
||
const submitOptions = buildGuidedReviewSubmitOptions(state)
|
||
assert.equal(submitOptions.systemGenerated, true)
|
||
assert.equal(submitOptions.extraContext.expense_scene_selection.expense_type, 'travel')
|
||
assert.equal(submitOptions.extraContext.review_form_values.expense_type, '差旅费')
|
||
assert.match(submitOptions.rawText, /事由:去上海支持上海电力部署项目/)
|
||
assert.match(submitOptions.rawText, /出差时间\/天数:2026-05-20 至 2026-05-23,出差 3 天/)
|
||
})
|
||
|
||
test('guided reimbursement requires application selection for travel and entertainment', () => {
|
||
assert.equal(requiresApplicationBeforeReimbursement('travel'), true)
|
||
assert.equal(requiresApplicationBeforeReimbursement('meal'), true)
|
||
assert.equal(requiresApplicationBeforeReimbursement('transport'), false)
|
||
|
||
const claimsPayload = {
|
||
items: [
|
||
{
|
||
id: 'app-travel',
|
||
claim_no: 'AP-202605-001',
|
||
employee_name: '张小青',
|
||
expense_type: 'travel_application',
|
||
reason: '去上海支持项目部署',
|
||
location: '上海',
|
||
amount: 1800,
|
||
occurred_at: '2026-05-20T08:00:00Z',
|
||
status: 'approved',
|
||
created_at: '2026-06-02T00:58:00Z',
|
||
risk_flags_json: [{
|
||
source: 'application_detail',
|
||
application_detail: {
|
||
application_business_time: '2026-05-20 至 2026-05-23',
|
||
days: '4 天',
|
||
transport_mode: '火车',
|
||
lodging_daily_cap: '600元/天',
|
||
subsidy_daily_cap: '120元/天',
|
||
transport_policy: '按真实票据复核',
|
||
policy_estimate: '住宿 2,400元 + 补贴 480元'
|
||
}
|
||
}]
|
||
},
|
||
{
|
||
id: 'app-meal',
|
||
claim_no: 'AP-202605-002',
|
||
employee_name: '张小青',
|
||
expense_type: 'expense_application',
|
||
reason: '客户招待沟通项目',
|
||
location: '武汉',
|
||
amount: 600,
|
||
status: 'approved',
|
||
created_at: '2026-05-21T08:00:00Z'
|
||
},
|
||
{
|
||
id: 'app-draft',
|
||
claim_no: 'AP-202605-003',
|
||
employee_name: '张小青',
|
||
expense_type: 'travel_application',
|
||
reason: '草稿出差申请',
|
||
status: 'draft'
|
||
},
|
||
{
|
||
id: 'app-submitted',
|
||
claim_no: 'AP-202605-005',
|
||
employee_name: '张小青',
|
||
expense_type: 'travel_application',
|
||
reason: '审批中的出差申请',
|
||
status: 'submitted'
|
||
},
|
||
{
|
||
id: 'app-archived-stale-key',
|
||
claim_no: 'AP-202605-007',
|
||
employee_name: '张小青',
|
||
expense_type: 'travel_application',
|
||
reason: '已归档申请单',
|
||
status: 'archived',
|
||
approvalKey: 'completed'
|
||
},
|
||
{
|
||
id: 'app-linked',
|
||
claim_no: 'AP-202605-006',
|
||
employee_name: '张小青',
|
||
expense_type: 'travel_application',
|
||
reason: '已生成报销草稿的出差申请',
|
||
status: 'approved'
|
||
},
|
||
{
|
||
id: 're-linked-draft',
|
||
claim_no: 'RE-202605-006',
|
||
employee_name: '张小青',
|
||
expense_type: 'travel',
|
||
reason: '已关联申请单的报销草稿',
|
||
status: 'draft',
|
||
risk_flags_json: [{
|
||
source: 'application_link',
|
||
application_claim_id: 'app-linked',
|
||
application_claim_no: 'AP-202605-006'
|
||
}]
|
||
},
|
||
{
|
||
id: 'app-other-user',
|
||
claim_no: 'AP-202605-004',
|
||
employee_name: '李四',
|
||
expense_type: 'travel_application',
|
||
reason: '其他员工出差申请',
|
||
status: 'approved'
|
||
}
|
||
]
|
||
}
|
||
|
||
const currentUser = { name: '张小青', username: 'xiaoqing.zhang' }
|
||
const travelApplications = filterRequiredApplicationCandidates(claimsPayload, 'travel', currentUser)
|
||
assert.deepEqual(travelApplications.map((item) => item.claim_no), ['AP-202605-001'])
|
||
assert.match(buildRequiredApplicationSelectionText('travel', travelApplications), /需要先关联对应的申请单/)
|
||
assert.match(buildRequiredApplicationMissingText('meal'), /不能继续这类报销流程/)
|
||
|
||
const mealApplications = filterRequiredApplicationCandidates(claimsPayload, 'meal', currentUser)
|
||
assert.deepEqual(mealApplications.map((item) => item.claim_no), ['AP-202605-002'])
|
||
|
||
const actions = buildRequiredApplicationActions(travelApplications, GUIDED_ACTION_SELECT_REQUIRED_APPLICATION)
|
||
assert.equal(actions[0].action_type, GUIDED_ACTION_SELECT_REQUIRED_APPLICATION)
|
||
assert.equal(actions[0].payload.application_claim_no, 'AP-202605-001')
|
||
|
||
const associationActions = buildReimbursementAssociationActions(travelApplications, '我要报销')
|
||
assert.equal(associationActions[0].action_type, 'select_required_application')
|
||
assert.equal(associationActions[0].payload.application_claim_no, 'AP-202605-001')
|
||
assert.equal(associationActions.at(-1).action_type, SKIP_REQUIRED_APPLICATION_LINK_ACTION)
|
||
assert.match(buildReimbursementAssociationSelectionText(travelApplications), /单独新建报销单/)
|
||
assert.match(buildReimbursementAssociationSelectionText(travelApplications), /ai-document-card-list/)
|
||
assert.match(buildReimbursementAssociationSelectionText(travelApplications), /ai-document-card--application/)
|
||
assert.match(buildReimbursementAssociationMissingText(), /单独新建报销单/)
|
||
const associationQueryPayload = buildReimbursementAssociationQueryPayload(travelApplications)
|
||
assert.equal(associationQueryPayload.selectionMode, 'reimbursement_application_association')
|
||
assert.equal(associationQueryPayload.records[0].claimNo, 'AP-202605-001')
|
||
const completedThinking = buildReimbursementAssociationThinkingEvents('completed', { candidateCount: 1 })
|
||
assert.equal(completedThinking[0].title, '判断用户意图')
|
||
assert.equal(completedThinking.at(-1).status, 'completed')
|
||
const associationSubmitOptions = buildReimbursementAssociationSubmitOptions(
|
||
associationActions[0].payload,
|
||
'我要报销'
|
||
)
|
||
assert.equal(associationSubmitOptions.skipDraftAssociationPrompt, true)
|
||
assert.equal(associationSubmitOptions.extraContext.expense_scene_selection.application_claim_no, 'AP-202605-001')
|
||
assert.equal(associationSubmitOptions.extraContext.review_form_values.application_business_time, '2026-05-20 至 2026-05-23')
|
||
|
||
let state = waitForGuidedApplicationSelection(createGuidedReimbursementState(), 'travel', travelApplications)
|
||
assert.equal(state.stepKey, 'application_selection')
|
||
assert.equal(state.applicationCandidates[0].claim_no, 'AP-202605-001')
|
||
|
||
state = selectGuidedRequiredApplication(state, actions[0].payload)
|
||
assert.equal(state.stepKey, 'summary')
|
||
assert.equal(isGuidedReimbursementReadyForReview(state), true)
|
||
assert.equal(state.values.application_claim_no, 'AP-202605-001')
|
||
assert.equal(state.values.application_business_time, '2026-05-20 至 2026-05-23')
|
||
assert.equal(state.values.application_days, '4 天')
|
||
assert.equal(state.values.application_transport_mode, '火车')
|
||
assert.equal(state.values.application_lodging_daily_cap, '600元/天')
|
||
assert.equal(state.values.application_subsidy_daily_cap, '120元/天')
|
||
const summaryText = buildGuidedReimbursementSummaryText(state)
|
||
assert.match(summaryText, /关联申请单:AP-202605-001/)
|
||
assert.match(summaryText, /草稿详情中上传对应票据/)
|
||
assert.doesNotMatch(summaryText, /事由:待补充/)
|
||
|
||
const submitOptions = buildGuidedReviewSubmitOptions(state)
|
||
assert.equal(submitOptions.extraContext.review_action, 'save_draft')
|
||
assert.equal(submitOptions.extraContext.review_form_values.application_claim_no, 'AP-202605-001')
|
||
assert.equal(submitOptions.extraContext.review_form_values.reason, '去上海支持项目部署')
|
||
assert.equal(submitOptions.extraContext.review_form_values.location, '上海')
|
||
assert.equal(submitOptions.extraContext.review_form_values.amount, '')
|
||
assert.equal(submitOptions.extraContext.review_form_values.application_amount, '1800')
|
||
assert.equal(submitOptions.extraContext.review_form_values.application_business_time, '2026-05-20 至 2026-05-23')
|
||
assert.equal(submitOptions.extraContext.review_form_values.application_days, '4 天')
|
||
assert.equal(submitOptions.extraContext.review_form_values.transport_mode, '火车')
|
||
assert.equal(submitOptions.extraContext.review_form_values.application_transport_mode, '火车')
|
||
assert.equal(submitOptions.extraContext.review_form_values.reimbursement_type, undefined)
|
||
assert.equal(submitOptions.extraContext.review_form_values.reason_value, undefined)
|
||
assert.equal(submitOptions.extraContext.review_form_values.business_time, undefined)
|
||
assert.equal(submitOptions.extraContext.review_form_values.business_location, undefined)
|
||
assert.equal(submitOptions.extraContext.review_form_values.application_lodging_daily_cap, '600元/天')
|
||
assert.equal(submitOptions.extraContext.review_form_values.application_subsidy_daily_cap, '120元/天')
|
||
assert.equal(submitOptions.extraContext.expense_scene_selection.application_claim_no, 'AP-202605-001')
|
||
assert.match(submitOptions.rawText, /关联申请单:AP-202605-001/)
|
||
assert.doesNotMatch(submitOptions.rawText, /事由:待补充/)
|
||
})
|
||
|
||
test('guided reimbursement interrupts suspicious questions before expensive flow', () => {
|
||
const state = selectGuidedExpenseType(createGuidedReimbursementState(), 'transport')
|
||
assert.equal(shouldConfirmGuidedInterruption('送客户去机场', state), false)
|
||
assert.equal(shouldConfirmGuidedInterruption('帮我查询一下上周的报销状态?', state), true)
|
||
assert.deepEqual(
|
||
buildGuidedInterruptionActions().map((action) => action.action_type),
|
||
[GUIDED_ACTION_CONTINUE_FILLING, GUIDED_ACTION_PROCESS_INTERRUPTION]
|
||
)
|
||
})
|
||
|
||
test('status query guide collects a query mode before calling existing query flow', () => {
|
||
let state = createGuidedStatusQueryState()
|
||
assert.equal(state.mode, GUIDED_FLOW_MODE_STATUS_QUERY)
|
||
assert.equal(state.stepKey, 'query_mode')
|
||
assert.ok(buildGuidedQueryModeActions().every((action) => action.action_type === GUIDED_ACTION_SELECT_QUERY_MODE))
|
||
|
||
state = selectGuidedQueryMode(state, 'status')
|
||
assert.equal(state.stepKey, 'status_value')
|
||
assert.ok(buildGuidedQueryStatusActions().every((action) => action.action_type === GUIDED_ACTION_SELECT_QUERY_STATUS))
|
||
assert.equal(buildGuidedStatusQueryText(state, '已归档'), '帮我查询已归档的报销单据,筛选最近的 5 条记录')
|
||
|
||
const keywordState = selectGuidedQueryMode(createGuidedStatusQueryState(), 'keyword')
|
||
assert.equal(keywordState.stepKey, 'query_value')
|
||
assert.equal(
|
||
buildGuidedStatusQueryText(keywordState, '上海电力'),
|
||
'帮我查询地点或事由包含“上海电力”的报销单据状态,筛选最近的 5 条记录'
|
||
)
|
||
})
|
||
|
||
test('guided flow state is serializable and restored through session state', () => {
|
||
const empty = createEmptyGuidedFlowState()
|
||
assert.deepEqual(normalizeGuidedFlowState({ mode: 'bad' }), empty)
|
||
assert.deepEqual(
|
||
normalizeGuidedFlowState({
|
||
mode: GUIDED_FLOW_MODE_REIMBURSEMENT,
|
||
stepKey: 'amount',
|
||
expenseType: 'travel',
|
||
values: {
|
||
amount: 200,
|
||
attachment_names: ['a.pdf', '', 'a.pdf']
|
||
},
|
||
pendingInterruptionText: '查询状态?'
|
||
}),
|
||
{
|
||
mode: GUIDED_FLOW_MODE_REIMBURSEMENT,
|
||
stepKey: 'amount',
|
||
expenseType: 'travel',
|
||
values: {
|
||
amount: '200',
|
||
attachment_names: ['a.pdf']
|
||
},
|
||
pendingInterruptionText: '查询状态?',
|
||
applicationCandidates: []
|
||
}
|
||
)
|
||
|
||
assert.match(sessionStateScript, /guidedFlowState:\s*normalizeGuidedFlowState\(state\.guidedFlowState\)/)
|
||
assert.match(sessionStateScript, /runtimeRefs\.guidedFlowState\?\.value/)
|
||
assert.match(sessionStateScript, /guidedFlowState,\s*\n\s*insightPanelCollapsed/)
|
||
assert.match(sessionStateScript, /function refreshWelcomeQuickActions/)
|
||
assert.match(sessionStateScript, /buildWelcomeQuickActions\(/)
|
||
assert.match(sessionStateScript, /resolveAccessibleSessionTypes\(\)\.reduce/)
|
||
assert.match(sessionStateScript, /props\.entrySource === 'application' \? SESSION_TYPE_APPLICATION : SESSION_TYPE_EXPENSE/)
|
||
assert.match(sessionStateScript, /const canRestorePersistedInitialState =[\s\S]*shouldPersistLocalSnapshot/)
|
||
})
|
||
|
||
test('guided flow is local until final confirmation or collected query handoff', () => {
|
||
assert.doesNotMatch(guidedFlowScript, /runOrchestrator/)
|
||
assert.doesNotMatch(guidedFlowScript, /startExpenseClaimDraftFlowStep/)
|
||
assert.match(guidedModelScript, /review_action:\s*['"]save_draft['"]/)
|
||
assert.match(guidedFlowScript, /fetchExpenseClaims/)
|
||
assert.match(guidedFlowScript, /REIMBURSEMENT_LIST_PREVIEW_PARAMS/)
|
||
assert.match(guidedFlowScript, /fetchExpenseClaims\(REIMBURSEMENT_LIST_PREVIEW_PARAMS\)/)
|
||
assert.match(submitDraftPreflightScript, /REIMBURSEMENT_LIST_PREVIEW_PARAMS/)
|
||
assert.match(submitDraftPreflightScript, /fetchExpenseClaims\(REIMBURSEMENT_LIST_PREVIEW_PARAMS\)/)
|
||
assert.doesNotMatch(guidedFlowScript, /fetchExpenseClaims\(\)/)
|
||
assert.doesNotMatch(submitDraftPreflightScript, /fetchExpenseClaims\(\)/)
|
||
assert.match(guidedFlowScript, /GUIDED_ACTION_SELECT_REQUIRED_APPLICATION/)
|
||
assert.match(guidedFlowScript, /isGuidedReimbursementReadyForReview\(guidedFlowState\.value\)[\s\S]*pushReimbursementSummary\(\)/)
|
||
assert.match(guidedFlowScript, /isGuidedReimbursementReadyForReview\(currentState\) && fileNames\.length[\s\S]*buildGuidedReviewSubmitOptions\(currentState, mergedFiles\)[\s\S]*skipDraftAssociationPrompt:\s*true[\s\S]*skipUserMessage:\s*true[\s\S]*submitExistingComposer\(submitOptions\)/)
|
||
assert.doesNotMatch(guidedFlowScript, /amount:\s*applicationAmount/)
|
||
assert.match(guidedFlowScript, /amount:\s*''/)
|
||
assert.match(guidedFlowScript, /if \(!applications\.length\) \{[\s\S]*guidedFlowState\.value = createEmptyGuidedFlowState\(\)[\s\S]*meta: \['缺少可关联申请单'\][\s\S]*\}\)/)
|
||
assert.doesNotMatch(guidedFlowScript, /meta: \['缺少可关联申请单'\],[\s\S]{0,120}suggestedActions: buildGuidedExpenseTypeActions\(\)/)
|
||
assert.match(guidedFlowScript, /handleSceneSelectionApplicationGate/)
|
||
assert.match(createViewScript, /handleSceneSelectionApplicationGate/)
|
||
assert.match(createViewScript, /submitComposerFromMessageHandlers/)
|
||
assert.match(messageHandlersScript, /if \(await handleGuidedComposerSubmit\(options\)\) return null[\s\S]*return submitComposerInternal\(options\)/)
|
||
assert.match(suggestedActionsScript, /ASSISTANT_SCOPE_ACTION_SWITCH/)
|
||
assert.match(suggestedActionsScript, /actionPayload\.carry_text/)
|
||
assert.match(suggestedActionsScript, /targetSessionType === SESSION_TYPE_EXPENSE[\s\S]*carryText === '我要报销'[\s\S]*pushExpenseAssociationGatePrompt\(carryText\)/)
|
||
assert.match(suggestedActionsScript, /actionType === SKIP_REQUIRED_APPLICATION_LINK_ACTION[\s\S]*pushExpenseSceneSelectionPrompt/)
|
||
assert.match(suggestedActionsScript, /actionType === 'confirm_expense_intent'[\s\S]*pushExpenseAssociationGatePrompt\(originalMessage\)/)
|
||
assert.match(suggestedActionsScript, /pushReimbursementAssociationPromptMessage\(\{[\s\S]*skipDraftCheck: Boolean\(options\.skipDraftCheck\)/)
|
||
assert.match(submitComposerScript, /waitForExpenseSceneSelection[\s\S]*pushReimbursementAssociationPromptMessage\(\{[\s\S]*rawText/)
|
||
assert.match(submitComposerScript, /fetchExpenseClaims[\s\S]*currentUser/)
|
||
assert.doesNotMatch(submitComposerScript, /if \(waitForExpenseSceneSelection\) \{[\s\S]{0,260}buildExpenseSceneSelectionMessage/)
|
||
assert.match(submitComposerScript, /resolveAssistantScopeGuard/)
|
||
assert.match(submitComposerScript, /skipScopeGuard/)
|
||
assert.match(guidedFlowScript, /submitExistingComposer\(submitOptions\)/)
|
||
assert.match(guidedFlowScript, /submitExistingComposer\(\{[\s\S]*pendingText:\s*'正在查询单据状态\.\.\.'/)
|
||
})
|