Files
X-Financial/web/tests/travel-reimbursement-guided-flow.test.mjs
caoxiaozhu ba444a514f feat(web): 报销单新增关联申请单门控与草稿检测流程
- 新增 travelReimbursementAssociationGateModel,查询可关联申请单/草稿报销单并生成跳过/选择/单独新建动作,区分差旅费与业务招待费类型
- travelReimbursementApplicationLinkModel 补充 buildLinkedApplicationReferenceIndex/buildRequiredApplicationActions 等关联构建逻辑
- useTravelReimbursementSuggestedActions 接入 select_required_application/skip 系列动作,'我要报销'入口改为先走关联门控
- useWorkbenchAiActionRouter 新增 SKIP_REQUIRED_APPLICATION_LINK/SKIP_REIMBURSEMENT_DRAFT_CHECK 动作分发
- useWorkbenchAiExpenseFlow 暴露 startAiReimbursementAssociationGate,stewardPlanModel 待处理流程适配
- 新增 workbench-ai-action-router、workbench-ai-reimbursement-association-gate 测试并更新 guided-flow、steward-plan 测试
2026-06-22 15:55:59 +08:00

505 lines
24 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 {
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 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, /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*'正在查询单据状态\.\.\.'/)
})