Files
X-Financial/web/tests/travel-reimbursement-guided-flow.test.mjs
caoxiaozhu 575f093c74 feat: 新增风险规则生成引擎与知识图谱可视化
后端新增风险规则自动生成和模板执行服务,支持从规则资产
批量生成并持久化风险规则文件;知识库入库日志增强图谱
查询和本地 RAG 回退,前端审计页面增加风险规则模型和流
程图组件,知识入库面板拆分为图谱可视化子组件,报销创
建页面增加引导式流程模型,更新知识库索引数据。
2026-05-23 19:54:42 +08:00

190 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 { readFileSync } from 'node:fs'
import test from 'node:test'
import { fileURLToPath } from 'node:url'
import {
EXPENSE_WELCOME_QUICK_ACTIONS,
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_PROCESS_INTERRUPTION,
GUIDED_ACTION_SELECT_EXPENSE_TYPE,
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,
selectGuidedExpenseType,
selectGuidedQueryMode,
shouldConfirmGuidedInterruption
} from '../src/views/scripts/travelReimbursementGuidedFlowModel.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 sessionStateScript = readFileSync(
fileURLToPath(new URL('../src/views/scripts/useTravelReimbursementSessionState.js', import.meta.url)),
'utf8'
)
test('welcome quick actions are reduced to three guided local actions', () => {
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.notDeepEqual(
buildWelcomeQuickActions(SESSION_TYPE_KNOWLEDGE).map((item) => item.label),
EXPENSE_WELCOME_QUICK_ACTIONS.map((item) => item.label),
'knowledge hot questions should stay independent'
)
})
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 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: '查询状态?'
}
)
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\(/)
})
test('guided flow is local until final confirmation or collected query handoff', () => {
assert.doesNotMatch(guidedFlowScript, /runOrchestrator/)
assert.doesNotMatch(guidedFlowScript, /startExpenseClaimDraftFlowStep/)
assert.doesNotMatch(guidedFlowScript, /review_action:\s*['"]save_draft['"]/)
assert.match(createViewScript, /if \(await handleGuidedComposerSubmit\(options\)\) \{[\s\S]*return null[\s\S]*\}[\s\S]*return submitComposerInternal\(options\)/)
assert.match(guidedFlowScript, /submitExistingComposer\(submitOptions\)/)
assert.match(guidedFlowScript, /submitExistingComposer\(\{[\s\S]*pendingText:\s*'正在查询单据状态\.\.\.'/)
})