Files
X-Financial/web/tests/travel-reimbursement-guided-flow.test.mjs
Codex 8b952c9a26 refactor(travel): split reimbursement create workflow
完整修改内容:

- 拆分 TravelReimbursementCreateView:提取审核面板纯模型、消息操作、建议动作处理、生命周期 watcher/UI 映射、小财管家运行时、续办流程和运行时文本模型,减少主组件继续堆叠业务分支。
- 调整申请预览链路:新增本地申请意图 gate,完善复杂差旅申请的大模型复核判断、交通方式缺失/候选识别、规则中心交通费用预估合并和申请冲突处理。
- 优化小财管家流程:抽出 steward typewriter 分段策略,避免 Markdown 表格逐字闪烁;补齐跨助手 carry、字段补齐续办、文本确认提交和行程规划推荐动作。
- 调整消息与样式:移除申请预览日期 chip 样式,收敛申请卡片/报销草稿消息的展示与复制、朗读、反馈入口逻辑。
- 更新测试:将源码锚点迁移到新模块,覆盖申请预览、提交确认、小财管家续办、引导流和审核抽屉相关断言。

验证:

- node --check web/src/views/scripts/TravelReimbursementCreateView.js 及新增拆分模块
- npm --prefix web run build
- node --test web/tests/expense-application-fast-preview.test.mjs web/tests/expense-application-submit-rich-confirm.test.mjs web/tests/travel-reimbursement-guided-flow.test.mjs

说明:

- 后端/规则/容器配置/Audit 页面等工作区已有改动未纳入本提交。
- 容器内后端定向 pytest 曾运行 timeout 180s /tmp/x-financial-server-venv/bin/pytest -q <相关后端测试>,180 秒超时且超时前已有失败标记,未作为通过项记录。
- TravelReimbursementCreateView 当前仍超过 800 行,后续仍需继续拆分;本提交先把新增职责模块控制在 800 行内,阻止主类/主模块继续膨胀。
2026-06-13 14:53:23 +00:00

462 lines
21 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 {
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'
)
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')
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, /if \(await handleGuidedComposerSubmit\(options\)\) \{[\s\S]*return null[\s\S]*\}[\s\S]*return submitComposerInternal\(options\)/)
assert.match(suggestedActionsScript, /ASSISTANT_SCOPE_ACTION_SWITCH/)
assert.match(suggestedActionsScript, /actionPayload\.carry_text/)
assert.match(submitComposerScript, /resolveAssistantScopeGuard/)
assert.match(submitComposerScript, /skipScopeGuard/)
assert.match(guidedFlowScript, /submitExistingComposer\(submitOptions\)/)
assert.match(guidedFlowScript, /submitExistingComposer\(\{[\s\S]*pendingText:\s*'正在查询单据状态\.\.\.'/)
})