2026-05-26 09:15:14 +08:00
|
|
|
|
import assert from 'node:assert/strict'
|
|
|
|
|
|
import { readFileSync } from 'node:fs'
|
|
|
|
|
|
import test from 'node:test'
|
|
|
|
|
|
import { fileURLToPath } from 'node:url'
|
2026-05-30 15:46:51 +08:00
|
|
|
|
import { ref } from 'vue'
|
2026-05-26 09:15:14 +08:00
|
|
|
|
|
|
|
|
|
|
import {
|
2026-06-02 14:01:51 +08:00
|
|
|
|
applyApplicationBusinessTimeContext,
|
2026-05-26 09:15:14 +08:00
|
|
|
|
buildApplicationPreviewFooterMessage,
|
|
|
|
|
|
buildApplicationPreviewRows,
|
|
|
|
|
|
buildApplicationPreviewSubmitText,
|
|
|
|
|
|
buildApplicationTemplatePreview,
|
|
|
|
|
|
applyApplicationPolicyEstimateResult,
|
|
|
|
|
|
buildApplicationPolicyEstimateRequest,
|
|
|
|
|
|
buildLocalApplicationPreview,
|
|
|
|
|
|
buildLocalApplicationPreviewMessage,
|
|
|
|
|
|
buildModelRefinedApplicationPreview,
|
2026-06-06 17:19:07 +08:00
|
|
|
|
applicationDateRangesOverlap,
|
2026-05-26 09:15:14 +08:00
|
|
|
|
normalizeApplicationPreview,
|
2026-06-06 17:19:07 +08:00
|
|
|
|
normalizeTransportModeOption,
|
|
|
|
|
|
resolveApplicationDateRange,
|
2026-06-02 14:01:51 +08:00
|
|
|
|
resolveApplicationTimeLabel,
|
2026-06-13 14:52:26 +00:00
|
|
|
|
shouldRequireApplicationModelReview,
|
2026-05-26 09:15:14 +08:00
|
|
|
|
shouldUseLocalApplicationPreview
|
|
|
|
|
|
} from '../src/utils/expenseApplicationPreview.js'
|
2026-06-01 17:07:14 +08:00
|
|
|
|
import {
|
|
|
|
|
|
buildMockApplicationTransportEstimate,
|
|
|
|
|
|
resolveMockApplicationTransportWaitMs,
|
|
|
|
|
|
buildSystemApplicationEstimate
|
|
|
|
|
|
} from '../src/utils/expenseApplicationEstimate.js'
|
2026-06-03 09:25:23 +08:00
|
|
|
|
import {
|
|
|
|
|
|
TRAVEL_PLANNING_ACTION_GENERATE,
|
|
|
|
|
|
TRAVEL_PLANNING_ACTION_SKIP,
|
|
|
|
|
|
buildTravelPlanningNudgeMessage,
|
|
|
|
|
|
buildTravelPlanningRecommendation,
|
|
|
|
|
|
buildTravelPlanningSuggestedActions
|
|
|
|
|
|
} from '../src/utils/travelApplicationPlanning.js'
|
2026-05-27 10:32:08 +08:00
|
|
|
|
import { renderMarkdown } from '../src/utils/markdown.js'
|
2026-05-27 14:35:17 +08:00
|
|
|
|
import {
|
|
|
|
|
|
createMessage as createConversationMessage,
|
|
|
|
|
|
hasMeaningfulSessionMessages
|
|
|
|
|
|
} from '../src/views/scripts/travelReimbursementConversationModel.js'
|
2026-06-06 17:19:07 +08:00
|
|
|
|
import {
|
|
|
|
|
|
buildStewardSuggestedActions,
|
|
|
|
|
|
filterStewardBlockingMissingFields
|
|
|
|
|
|
} from '../src/views/scripts/stewardPlanModel.js'
|
|
|
|
|
|
import {
|
|
|
|
|
|
buildStewardFieldCompletionContinuation,
|
|
|
|
|
|
buildStewardFieldCompletionRawText
|
|
|
|
|
|
} from '../src/views/scripts/stewardFieldCompletionModel.js'
|
|
|
|
|
|
import {
|
|
|
|
|
|
shouldUseBudgetCompileReport
|
|
|
|
|
|
} from '../src/views/scripts/budgetAssistantReportModel.js'
|
2026-06-13 14:52:26 +00:00
|
|
|
|
import { resolveStewardTypewriterNextIndex } from '../src/views/scripts/stewardTypewriter.js'
|
|
|
|
|
|
import {
|
2026-06-18 22:12:24 +08:00
|
|
|
|
ASSISTANT_SCOPE_ACTION_SWITCH,
|
2026-06-13 14:52:26 +00:00
|
|
|
|
ASSISTANT_SCOPE_SESSION_APPLICATION,
|
|
|
|
|
|
ASSISTANT_SCOPE_SESSION_EXPENSE,
|
|
|
|
|
|
ASSISTANT_SCOPE_SESSION_STEWARD,
|
2026-06-18 22:12:24 +08:00
|
|
|
|
buildUnsupportedBusinessScopeConversation,
|
2026-06-13 14:52:26 +00:00
|
|
|
|
inferAssistantScopeTarget,
|
|
|
|
|
|
resolveAssistantScopeGuard
|
|
|
|
|
|
} from '../src/utils/assistantSessionScope.js'
|
2026-05-30 15:46:51 +08:00
|
|
|
|
import { useTravelReimbursementFlow } from '../src/views/scripts/useTravelReimbursementFlow.js'
|
2026-06-01 17:07:14 +08:00
|
|
|
|
import { useApplicationPreviewEditor } from '../src/views/scripts/useApplicationPreviewEditor.js'
|
2026-05-26 09:15:14 +08:00
|
|
|
|
|
2026-06-22 11:58:53 +08:00
|
|
|
|
const submitComposerScript = [
|
|
|
|
|
|
'../src/views/scripts/travelReimbursementSubmitConstants.js',
|
|
|
|
|
|
'../src/views/scripts/travelReimbursementSubmitApplicationConflicts.js',
|
|
|
|
|
|
'../src/views/scripts/travelReimbursementSubmitApplicationPreview.js',
|
|
|
|
|
|
'../src/views/scripts/travelReimbursementSubmitLocalPreviewFlow.js',
|
|
|
|
|
|
'../src/views/scripts/travelReimbursementSubmitStewardDelegation.js',
|
|
|
|
|
|
'../src/views/scripts/travelReimbursementSubmitAttachmentFlow.js',
|
|
|
|
|
|
'../src/views/scripts/travelReimbursementSubmitDraftPreflight.js',
|
|
|
|
|
|
'../src/views/scripts/travelReimbursementSubmitRecognitionFlow.js',
|
|
|
|
|
|
'../src/views/scripts/travelReimbursementSubmitResponseModel.js',
|
|
|
|
|
|
'../src/views/scripts/useTravelReimbursementSubmitComposer.js'
|
|
|
|
|
|
].map((path) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')).join('\n')
|
2026-06-06 17:19:07 +08:00
|
|
|
|
const stewardServiceScript = readFileSync(
|
|
|
|
|
|
fileURLToPath(new URL('../src/services/steward.js', import.meta.url)),
|
|
|
|
|
|
'utf8'
|
|
|
|
|
|
)
|
2026-05-26 09:15:14 +08:00
|
|
|
|
const createViewScript = readFileSync(
|
|
|
|
|
|
fileURLToPath(new URL('../src/views/scripts/TravelReimbursementCreateView.js', import.meta.url)),
|
|
|
|
|
|
'utf8'
|
|
|
|
|
|
)
|
2026-06-13 14:52:26 +00:00
|
|
|
|
const messageActionsScript = readFileSync(
|
|
|
|
|
|
fileURLToPath(new URL('../src/views/scripts/useTravelReimbursementMessageActions.js', import.meta.url)),
|
|
|
|
|
|
'utf8'
|
|
|
|
|
|
)
|
|
|
|
|
|
const suggestedActionsScript = readFileSync(
|
|
|
|
|
|
fileURLToPath(new URL('../src/views/scripts/useTravelReimbursementSuggestedActions.js', import.meta.url)),
|
|
|
|
|
|
'utf8'
|
|
|
|
|
|
)
|
|
|
|
|
|
const stewardRuntimeScript = readFileSync(
|
|
|
|
|
|
fileURLToPath(new URL('../src/views/scripts/useTravelReimbursementStewardRuntime.js', import.meta.url)),
|
|
|
|
|
|
'utf8'
|
|
|
|
|
|
)
|
|
|
|
|
|
const stewardRuntimeTextModelScript = readFileSync(
|
|
|
|
|
|
fileURLToPath(new URL('../src/views/scripts/travelReimbursementStewardRuntimeTextModel.js', import.meta.url)),
|
|
|
|
|
|
'utf8'
|
|
|
|
|
|
)
|
|
|
|
|
|
const stewardFollowupFlowScript = readFileSync(
|
|
|
|
|
|
fileURLToPath(new URL('../src/views/scripts/travelReimbursementStewardFollowupFlow.js', import.meta.url)),
|
|
|
|
|
|
'utf8'
|
|
|
|
|
|
)
|
2026-06-06 17:19:07 +08:00
|
|
|
|
const stewardPlanFlowScript = readFileSync(
|
|
|
|
|
|
fileURLToPath(new URL('../src/views/scripts/useStewardPlanFlow.js', import.meta.url)),
|
|
|
|
|
|
'utf8'
|
|
|
|
|
|
)
|
|
|
|
|
|
const stewardFieldCompletionScript = readFileSync(
|
|
|
|
|
|
fileURLToPath(new URL('../src/views/scripts/stewardFieldCompletionModel.js', import.meta.url)),
|
|
|
|
|
|
'utf8'
|
|
|
|
|
|
)
|
2026-05-26 09:15:14 +08:00
|
|
|
|
const createViewTemplate = readFileSync(
|
|
|
|
|
|
fileURLToPath(new URL('../src/views/TravelReimbursementCreateView.vue', import.meta.url)),
|
|
|
|
|
|
'utf8'
|
|
|
|
|
|
)
|
2026-05-27 10:32:08 +08:00
|
|
|
|
const messageItemTemplate = readFileSync(
|
|
|
|
|
|
fileURLToPath(new URL('../src/components/travel/TravelReimbursementMessageItem.vue', import.meta.url)),
|
|
|
|
|
|
'utf8'
|
|
|
|
|
|
)
|
|
|
|
|
|
const messageItemStyles = readFileSync(
|
|
|
|
|
|
fileURLToPath(new URL('../src/assets/styles/components/travel-reimbursement-message-item.css', import.meta.url)),
|
|
|
|
|
|
'utf8'
|
|
|
|
|
|
)
|
2026-05-30 15:46:51 +08:00
|
|
|
|
const applicationMessageStyles = readFileSync(
|
|
|
|
|
|
fileURLToPath(new URL('../src/assets/styles/components/travel-reimbursement-message-application.css', import.meta.url)),
|
|
|
|
|
|
'utf8'
|
|
|
|
|
|
)
|
2026-05-26 09:15:14 +08:00
|
|
|
|
const conversationModelScript = readFileSync(
|
|
|
|
|
|
fileURLToPath(new URL('../src/views/scripts/travelReimbursementConversationModel.js', import.meta.url)),
|
|
|
|
|
|
'utf8'
|
|
|
|
|
|
)
|
|
|
|
|
|
const previewEditorScript = readFileSync(
|
|
|
|
|
|
fileURLToPath(new URL('../src/views/scripts/useApplicationPreviewEditor.js', import.meta.url)),
|
|
|
|
|
|
'utf8'
|
|
|
|
|
|
)
|
2026-05-30 15:46:51 +08:00
|
|
|
|
const flowScript = readFileSync(
|
|
|
|
|
|
fileURLToPath(new URL('../src/views/scripts/useTravelReimbursementFlow.js', import.meta.url)),
|
|
|
|
|
|
'utf8'
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
function createFlowHarness() {
|
|
|
|
|
|
return useTravelReimbursementFlow({
|
|
|
|
|
|
activeSessionType: ref('application'),
|
|
|
|
|
|
reviewDrawerMode: ref(''),
|
|
|
|
|
|
insightPanelCollapsed: ref(true),
|
|
|
|
|
|
isKnowledgeSession: ref(false),
|
|
|
|
|
|
fetchAgentRunDetail: async () => null,
|
|
|
|
|
|
buildLocalIntentPreview: () => '本地意图预览',
|
|
|
|
|
|
buildLocalExtractionProgressMessages: () => ['正在抽取信息'],
|
|
|
|
|
|
summarizeSemanticIntentDetail: () => '模型已完成意图识别',
|
|
|
|
|
|
summarizeSemanticParseDetail: () => '模型已完成信息抽取',
|
|
|
|
|
|
SCENARIO_LABELS: {},
|
|
|
|
|
|
INTENT_LABELS: {},
|
|
|
|
|
|
EXPENSE_TYPE_LABELS: {},
|
|
|
|
|
|
FLOW_STEP_FALLBACKS: {
|
|
|
|
|
|
intent: { title: '意图识别', tool: 'SemanticRouter', runningText: '正在识别业务意图...', completedText: '已识别业务意图' },
|
|
|
|
|
|
extraction: { title: '信息抽取', tool: 'SemanticExtractor', runningText: '正在抽取关键信息...', completedText: '已抽取关键信息' },
|
|
|
|
|
|
'application-submit-success': { title: '申请单提交成功', tool: 'ApplicationSubmit', runningText: '正在提交申请单...', completedText: '申请单提交成功' }
|
|
|
|
|
|
},
|
|
|
|
|
|
REVIEW_DRAWER_MODE_FLOW: 'flow',
|
|
|
|
|
|
REVIEW_DRAWER_MODE_REVIEW: 'review',
|
|
|
|
|
|
FLOW_STEP_STATUS_PENDING: 'pending',
|
|
|
|
|
|
FLOW_STEP_STATUS_RUNNING: 'running',
|
|
|
|
|
|
FLOW_STEP_STATUS_COMPLETED: 'completed',
|
|
|
|
|
|
FLOW_STEP_STATUS_FAILED: 'failed'
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
2026-05-26 09:15:14 +08:00
|
|
|
|
|
|
|
|
|
|
test('application intent uses local preview instead of immediate orchestrator call', () => {
|
2026-06-13 14:52:26 +00:00
|
|
|
|
const prompt = '申请 2026-05-20 至 2026-05-23 去上海支撑上海电力部署项目,出差4天,高铁,预计金额2358元'
|
2026-05-26 09:15:14 +08:00
|
|
|
|
assert.equal(
|
|
|
|
|
|
shouldUseLocalApplicationPreview(prompt, {
|
|
|
|
|
|
sessionType: 'application',
|
|
|
|
|
|
attachmentCount: 0,
|
|
|
|
|
|
reviewAction: '',
|
|
|
|
|
|
systemGenerated: false
|
|
|
|
|
|
}),
|
|
|
|
|
|
true
|
|
|
|
|
|
)
|
|
|
|
|
|
assert.equal(
|
|
|
|
|
|
shouldUseLocalApplicationPreview('帮我查询申请状态', {
|
|
|
|
|
|
sessionType: 'application',
|
|
|
|
|
|
attachmentCount: 0,
|
|
|
|
|
|
reviewAction: '',
|
|
|
|
|
|
systemGenerated: false
|
|
|
|
|
|
}),
|
|
|
|
|
|
false
|
|
|
|
|
|
)
|
2026-06-13 14:52:26 +00:00
|
|
|
|
assert.equal(
|
|
|
|
|
|
shouldUseLocalApplicationPreview('小财管家\n23:04\n这是费用申请核对结果,请核对:', {
|
|
|
|
|
|
sessionType: 'application',
|
|
|
|
|
|
attachmentCount: 0,
|
|
|
|
|
|
reviewAction: '',
|
|
|
|
|
|
systemGenerated: false
|
|
|
|
|
|
}),
|
|
|
|
|
|
false
|
|
|
|
|
|
)
|
|
|
|
|
|
assert.equal(
|
|
|
|
|
|
shouldUseLocalApplicationPreview('我要申请', {
|
|
|
|
|
|
sessionType: 'application',
|
|
|
|
|
|
attachmentCount: 0,
|
|
|
|
|
|
reviewAction: '',
|
|
|
|
|
|
systemGenerated: false
|
|
|
|
|
|
}),
|
|
|
|
|
|
false
|
|
|
|
|
|
)
|
|
|
|
|
|
assert.equal(
|
|
|
|
|
|
shouldUseLocalApplicationPreview('去上海出差,支撑国网仿生产环境部署', {
|
|
|
|
|
|
sessionType: 'application',
|
|
|
|
|
|
attachmentCount: 0,
|
|
|
|
|
|
reviewAction: '',
|
|
|
|
|
|
systemGenerated: false
|
|
|
|
|
|
}),
|
|
|
|
|
|
true
|
|
|
|
|
|
)
|
2026-05-26 09:15:14 +08:00
|
|
|
|
|
2026-06-01 17:07:14 +08:00
|
|
|
|
const preview = buildLocalApplicationPreview(prompt, {
|
|
|
|
|
|
name: '李文静',
|
|
|
|
|
|
departmentName: '财务部',
|
|
|
|
|
|
position: '财务分析师',
|
|
|
|
|
|
managerName: '王强',
|
|
|
|
|
|
grade: 'P5'
|
|
|
|
|
|
})
|
2026-05-26 09:15:14 +08:00
|
|
|
|
assert.equal(preview.fields.applicationType, '差旅费用申请')
|
|
|
|
|
|
assert.equal(preview.fields.time, '2026-05-20 至 2026-05-23')
|
|
|
|
|
|
assert.equal(preview.fields.location, '上海')
|
2026-06-13 14:52:26 +00:00
|
|
|
|
assert.equal(preview.fields.days, '4天')
|
2026-05-26 09:15:14 +08:00
|
|
|
|
assert.equal(preview.fields.transportMode, '火车')
|
|
|
|
|
|
assert.equal(preview.fields.amount, '2358元')
|
2026-06-01 17:07:14 +08:00
|
|
|
|
assert.equal(preview.fields.applicant, '李文静')
|
2026-05-26 09:15:14 +08:00
|
|
|
|
assert.equal(preview.fields.grade, 'P5')
|
2026-06-01 17:07:14 +08:00
|
|
|
|
assert.equal(preview.fields.department, '财务部')
|
|
|
|
|
|
assert.equal(preview.fields.position, '财务分析师')
|
|
|
|
|
|
assert.equal(preview.fields.managerName, '王强')
|
2026-05-26 09:15:14 +08:00
|
|
|
|
assert.equal(preview.readyToSubmit, true)
|
|
|
|
|
|
assert.doesNotMatch(buildLocalApplicationPreviewMessage(preview), /#application-submit/)
|
|
|
|
|
|
assert.match(buildApplicationPreviewFooterMessage(preview), /#application-submit/)
|
|
|
|
|
|
assert.match(buildLocalApplicationPreviewMessage(preview), /点击对应行即可直接编辑/)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-06-18 22:12:24 +08:00
|
|
|
|
test('unsupported business guidance opens in assistant conversation form', () => {
|
|
|
|
|
|
const conversation = buildUnsupportedBusinessScopeConversation('你好')
|
|
|
|
|
|
|
|
|
|
|
|
assert.equal(conversation.state_json.session_type, ASSISTANT_SCOPE_SESSION_STEWARD)
|
|
|
|
|
|
assert.equal(conversation.messages.length, 1)
|
|
|
|
|
|
assert.equal(conversation.messages[0].role, 'assistant')
|
|
|
|
|
|
assert.match(conversation.messages[0].content, /小财管家暂时不处理「你好」/)
|
|
|
|
|
|
assert.equal(conversation.messages[0].assistantName, '小财管家')
|
|
|
|
|
|
assert.match(conversation.messages[0].content, /### 当前可继续的场景/)
|
|
|
|
|
|
assert.equal(
|
|
|
|
|
|
conversation.messages[0].message_json.orchestrator_payload.result.suggested_actions.length,
|
|
|
|
|
|
4
|
|
|
|
|
|
)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-06-13 14:52:26 +00:00
|
|
|
|
test('assistant scope guard blocks unsupported non-financial intent', () => {
|
2026-06-18 22:12:24 +08:00
|
|
|
|
const greetingGuard = resolveAssistantScopeGuard('你好', ASSISTANT_SCOPE_SESSION_APPLICATION)
|
2026-06-13 14:52:26 +00:00
|
|
|
|
const guard = resolveAssistantScopeGuard('帮我写一首诗,主题是春天', ASSISTANT_SCOPE_SESSION_APPLICATION)
|
|
|
|
|
|
|
2026-06-18 22:12:24 +08:00
|
|
|
|
assert.equal(greetingGuard.blocked, true)
|
|
|
|
|
|
assert.equal(greetingGuard.targetSessionType, '')
|
|
|
|
|
|
assert.equal(greetingGuard.suggestedActions.length, 4)
|
|
|
|
|
|
assert.deepEqual(
|
|
|
|
|
|
greetingGuard.suggestedActions.map((item) => item.action_type),
|
|
|
|
|
|
Array.from({ length: 4 }, () => ASSISTANT_SCOPE_ACTION_SWITCH)
|
|
|
|
|
|
)
|
|
|
|
|
|
assert.match(greetingGuard.text, /小财管家暂时不处理「你好」/)
|
2026-06-24 10:42:50 +08:00
|
|
|
|
assert.match(greetingGuard.text, /您可以直接点下面的场景继续/)
|
2026-06-18 22:12:24 +08:00
|
|
|
|
assert.equal(guard.suggestedActions.length, 4)
|
2026-06-13 14:52:26 +00:00
|
|
|
|
assert.equal(guard.blocked, true)
|
|
|
|
|
|
assert.equal(guard.targetSessionType, '')
|
|
|
|
|
|
assert.match(guard.text, /此意图系统不支持/)
|
|
|
|
|
|
assert.match(guard.text, /当前系统支持的业务范围/)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-06-18 22:12:24 +08:00
|
|
|
|
|
2026-06-13 14:52:26 +00:00
|
|
|
|
test('assistant scope guard routes related business intent instead of blocking', () => {
|
|
|
|
|
|
const guard = resolveAssistantScopeGuard('帮我查一下报销单状态', ASSISTANT_SCOPE_SESSION_APPLICATION)
|
|
|
|
|
|
|
|
|
|
|
|
assert.equal(guard.blocked, undefined)
|
|
|
|
|
|
assert.equal(guard.targetSessionType, ASSISTANT_SCOPE_SESSION_EXPENSE)
|
|
|
|
|
|
assert.match(guard.text, /报销助手/)
|
|
|
|
|
|
assert.equal(guard.suggestedActions[0].payload.session_type, ASSISTANT_SCOPE_SESSION_EXPENSE)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
test('assistant scope guard keeps current supported application intent and steward finance queries', () => {
|
|
|
|
|
|
assert.equal(
|
|
|
|
|
|
resolveAssistantScopeGuard('申请下周去上海出差,支撑服务器部署', ASSISTANT_SCOPE_SESSION_APPLICATION),
|
|
|
|
|
|
null
|
|
|
|
|
|
)
|
|
|
|
|
|
assert.equal(inferAssistantScopeTarget('查询一下预算余额'), ASSISTANT_SCOPE_SESSION_STEWARD)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-06-03 09:25:23 +08:00
|
|
|
|
test('travel application submit can continue with conversational planning recommendation', () => {
|
|
|
|
|
|
const preview = normalizeApplicationPreview({
|
|
|
|
|
|
fields: {
|
|
|
|
|
|
applicationType: '差旅费用申请',
|
|
|
|
|
|
time: '2026-02-20 至 2026-02-23',
|
|
|
|
|
|
location: '上海市',
|
|
|
|
|
|
reason: '支撑国网仿生产环境建设',
|
|
|
|
|
|
days: '4天',
|
|
|
|
|
|
transportMode: '火车'
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
const draftPayload = { claim_no: 'AP-202606030001-ABCDE123' }
|
|
|
|
|
|
const nudge = buildTravelPlanningNudgeMessage(preview, draftPayload)
|
|
|
|
|
|
const actions = buildTravelPlanningSuggestedActions(preview, draftPayload)
|
|
|
|
|
|
const recommendation = buildTravelPlanningRecommendation(preview, draftPayload)
|
|
|
|
|
|
|
|
|
|
|
|
assert.match(nudge, /上海市差旅申请已经提交/)
|
|
|
|
|
|
assert.match(nudge, /2026-02-20 至 2026-02-23/)
|
|
|
|
|
|
assert.deepEqual(actions.map((item) => item.action_type), [
|
|
|
|
|
|
TRAVEL_PLANNING_ACTION_GENERATE,
|
|
|
|
|
|
TRAVEL_PLANNING_ACTION_SKIP
|
|
|
|
|
|
])
|
|
|
|
|
|
assert.match(recommendation, /轻量行程规划/)
|
|
|
|
|
|
assert.match(recommendation, /优先看上午到中午抵达 上海市 的火车班次/)
|
|
|
|
|
|
assert.match(recommendation, /客户现场周边/)
|
|
|
|
|
|
assert.match(recommendation, /AP-202606030001-ABCDE123/)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-05-26 09:15:14 +08:00
|
|
|
|
test('application preview renders ordered editable rows and submit text uses edited values', () => {
|
|
|
|
|
|
const preview = buildLocalApplicationPreview('申请 2026-05-25 至 2026-05-28 去新疆,伊犁出差,服务美团业务部署,火车,预计费用1800元', {
|
|
|
|
|
|
name: '李文静',
|
2026-06-01 17:07:14 +08:00
|
|
|
|
departmentName: '财务部',
|
|
|
|
|
|
position: '财务分析师',
|
|
|
|
|
|
managerName: '王强',
|
2026-05-26 09:15:14 +08:00
|
|
|
|
grade: 'P5'
|
|
|
|
|
|
})
|
|
|
|
|
|
assert.equal(preview.fields.location, '新疆,伊犁')
|
|
|
|
|
|
assert.equal(preview.fields.reason, '服务美团业务部署')
|
|
|
|
|
|
const editedPreview = normalizeApplicationPreview({
|
|
|
|
|
|
...preview,
|
|
|
|
|
|
fields: {
|
|
|
|
|
|
...preview.fields,
|
|
|
|
|
|
reason: '客户现场项目支持',
|
|
|
|
|
|
amount: '1900元'
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const rows = buildApplicationPreviewRows(editedPreview)
|
|
|
|
|
|
assert.deepEqual(
|
|
|
|
|
|
rows.map((row) => row.label),
|
2026-06-02 16:22:59 +08:00
|
|
|
|
['申请类型', '姓名', '职级', '部门', '岗位', '直属领导', '出发时间', '返回时间', '地点', '事由', '天数', '出行方式', '住宿上限/天', '补贴标准/天', '交通费用口径', '规则测算参考', '系统预估费用']
|
2026-05-26 09:15:14 +08:00
|
|
|
|
)
|
2026-06-02 16:22:59 +08:00
|
|
|
|
assert.match(buildApplicationPreviewSubmitText(editedPreview), /出发时间:2026-05-25/)
|
|
|
|
|
|
assert.match(buildApplicationPreviewSubmitText(editedPreview), /返回时间:2026-05-28/)
|
2026-06-02 14:01:51 +08:00
|
|
|
|
assert.doesNotMatch(buildApplicationPreviewSubmitText(editedPreview), /发生时间:/)
|
2026-05-26 09:15:14 +08:00
|
|
|
|
assert.equal(rows.find((row) => row.key === 'amount')?.value, '1900元')
|
|
|
|
|
|
assert.equal(rows.find((row) => row.key === 'amount')?.highlight, true)
|
2026-06-01 17:07:14 +08:00
|
|
|
|
assert.equal(rows.find((row) => row.key === 'amount')?.editable, false)
|
|
|
|
|
|
assert.equal(rows.find((row) => row.key === 'applicant')?.editable, false)
|
2026-05-27 10:32:08 +08:00
|
|
|
|
assert.equal(rows.find((row) => row.key === 'grade')?.editable, false)
|
2026-06-01 17:07:14 +08:00
|
|
|
|
assert.equal(rows.find((row) => row.key === 'department')?.editable, false)
|
|
|
|
|
|
assert.equal(rows.find((row) => row.key === 'position')?.editable, false)
|
|
|
|
|
|
assert.equal(rows.find((row) => row.key === 'managerName')?.editable, false)
|
2026-05-26 09:15:14 +08:00
|
|
|
|
assert.equal(rows.find((row) => row.key === 'lodgingDailyCap')?.editable, false)
|
2026-06-01 17:07:14 +08:00
|
|
|
|
assert.match(buildApplicationPreviewSubmitText(editedPreview), /姓名:李文静/)
|
|
|
|
|
|
assert.match(buildApplicationPreviewSubmitText(editedPreview), /部门:财务部/)
|
|
|
|
|
|
assert.match(buildApplicationPreviewSubmitText(editedPreview), /岗位:财务分析师/)
|
|
|
|
|
|
assert.match(buildApplicationPreviewSubmitText(editedPreview), /直属领导:王强/)
|
2026-05-26 09:15:14 +08:00
|
|
|
|
assert.match(buildApplicationPreviewSubmitText(editedPreview), /事由:客户现场项目支持/)
|
2026-06-01 17:07:14 +08:00
|
|
|
|
assert.match(buildApplicationPreviewSubmitText(editedPreview), /系统预估费用:1900元/)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
test('application estimate builds deterministic mock transport amount and total', () => {
|
|
|
|
|
|
const trainEstimate = buildMockApplicationTransportEstimate({ transportMode: '高铁', location: '上海' })
|
|
|
|
|
|
const datedTrainEstimate = buildMockApplicationTransportEstimate({
|
|
|
|
|
|
transportMode: '高铁',
|
|
|
|
|
|
location: '上海',
|
|
|
|
|
|
time: '2026-05-25 至 2026-05-28'
|
|
|
|
|
|
})
|
|
|
|
|
|
const flightEstimate = buildMockApplicationTransportEstimate({ transportMode: '机票', location: '新疆,伊犁' })
|
|
|
|
|
|
const shipEstimate = buildMockApplicationTransportEstimate({ transportMode: '船票', location: '厦门' })
|
|
|
|
|
|
const totalEstimate = buildSystemApplicationEstimate({
|
|
|
|
|
|
transportMode: '火车',
|
|
|
|
|
|
location: '上海',
|
|
|
|
|
|
lodgingAmount: 1800,
|
|
|
|
|
|
allowanceAmount: 360
|
|
|
|
|
|
})
|
|
|
|
|
|
const datedTotalEstimate = buildSystemApplicationEstimate({
|
|
|
|
|
|
transportMode: '火车',
|
|
|
|
|
|
location: '上海',
|
|
|
|
|
|
time: '2026-05-25 至 2026-05-28',
|
|
|
|
|
|
lodgingAmount: 1800,
|
|
|
|
|
|
allowanceAmount: 360
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
assert.equal(trainEstimate.amountDisplay, '1,040')
|
|
|
|
|
|
assert.equal(datedTrainEstimate.queryDate, '2026-05-25')
|
|
|
|
|
|
assert.equal(datedTrainEstimate.amountDisplay, '1,100')
|
2026-06-13 14:52:26 +00:00
|
|
|
|
assert.equal(datedTrainEstimate.source, 'fallback_transport_budget_estimate_v1')
|
2026-06-02 16:22:59 +08:00
|
|
|
|
assert.equal(datedTrainEstimate.basisText, '预估交通费用 1,100元')
|
2026-06-01 17:07:14 +08:00
|
|
|
|
assert.ok(datedTrainEstimate.simulatedLatencyMs >= 360)
|
|
|
|
|
|
assert.ok(datedTrainEstimate.simulatedLatencyMs <= 779)
|
|
|
|
|
|
assert.equal(resolveMockApplicationTransportWaitMs(datedTrainEstimate), 320)
|
2026-06-13 14:52:26 +00:00
|
|
|
|
assert.equal(flightEstimate.amountDisplay, '3,200')
|
2026-06-01 17:07:14 +08:00
|
|
|
|
assert.equal(shipEstimate.amountDisplay, '1,040')
|
|
|
|
|
|
assert.equal(totalEstimate.transportAmountDisplay, '1,040')
|
|
|
|
|
|
assert.equal(totalEstimate.totalAmountDisplay, '3,200')
|
|
|
|
|
|
assert.equal(datedTotalEstimate.transportAmountDisplay, '1,100')
|
|
|
|
|
|
assert.equal(datedTotalEstimate.totalAmountDisplay, '3,260')
|
2026-05-26 09:15:14 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
2026-06-02 14:01:51 +08:00
|
|
|
|
test('application preview uses selected date range and business-specific time label', () => {
|
|
|
|
|
|
const preview = applyApplicationBusinessTimeContext(
|
|
|
|
|
|
buildLocalApplicationPreview(
|
|
|
|
|
|
'去上海出差4天,支撑国网仿生产环境部署,飞机',
|
|
|
|
|
|
{
|
|
|
|
|
|
name: '曹笑竹',
|
|
|
|
|
|
departmentName: '技术部',
|
|
|
|
|
|
position: '财务智能化产品经理',
|
|
|
|
|
|
managerName: '向万红',
|
|
|
|
|
|
grade: 'P5'
|
|
|
|
|
|
},
|
|
|
|
|
|
{ today: '2026-06-02' }
|
|
|
|
|
|
),
|
|
|
|
|
|
{
|
|
|
|
|
|
mode: 'range',
|
|
|
|
|
|
start_date: '2026-02-20',
|
|
|
|
|
|
end_date: '2026-02-23',
|
|
|
|
|
|
business_time: '2026-02-20 至 2026-02-23'
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
const rows = buildApplicationPreviewRows(preview)
|
|
|
|
|
|
const submitText = buildApplicationPreviewSubmitText(preview)
|
|
|
|
|
|
|
2026-06-02 16:22:59 +08:00
|
|
|
|
assert.equal(resolveApplicationTimeLabel(preview.fields.applicationType), '出发时间')
|
2026-06-02 14:01:51 +08:00
|
|
|
|
assert.equal(preview.fields.time, '2026-02-20 至 2026-02-23')
|
|
|
|
|
|
assert.equal(preview.fields.days, '4天')
|
|
|
|
|
|
assert.equal(preview.fields.reason, '支撑国网仿生产环境部署')
|
2026-06-02 16:22:59 +08:00
|
|
|
|
assert.equal(rows.find((row) => row.key === 'time')?.label, '出发时间')
|
|
|
|
|
|
assert.equal(rows.find((row) => row.key === 'time')?.value, '2026-02-20')
|
|
|
|
|
|
assert.equal(rows.find((row) => row.key === 'time_return')?.label, '返回时间')
|
|
|
|
|
|
assert.equal(rows.find((row) => row.key === 'time_return')?.value, '2026-02-23')
|
2026-06-22 15:56:06 +08:00
|
|
|
|
assert.equal(rows.find((row) => row.key === 'time_return')?.editable, true)
|
2026-06-02 16:22:59 +08:00
|
|
|
|
assert.match(submitText, /出发时间:2026-02-20/)
|
|
|
|
|
|
assert.match(submitText, /返回时间:2026-02-23/)
|
2026-06-02 14:01:51 +08:00
|
|
|
|
assert.match(submitText, /事由:支撑国网仿生产环境部署/)
|
|
|
|
|
|
assert.doesNotMatch(submitText, /发生时间:/)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-06-13 14:52:26 +00:00
|
|
|
|
test('application preview parses same-month shorthand date range', () => {
|
|
|
|
|
|
const preview = buildLocalApplicationPreview(
|
|
|
|
|
|
'我要申请2月20日-23日去上海出差,辅助国网仿生产项目部署',
|
|
|
|
|
|
{
|
|
|
|
|
|
name: '曹笑竹',
|
|
|
|
|
|
departmentName: '技术部',
|
|
|
|
|
|
position: '财务智能化产品经理',
|
|
|
|
|
|
managerName: '向万红',
|
|
|
|
|
|
grade: 'P5'
|
|
|
|
|
|
},
|
|
|
|
|
|
{ today: '2026-06-09' }
|
|
|
|
|
|
)
|
|
|
|
|
|
const rows = buildApplicationPreviewRows(preview)
|
|
|
|
|
|
|
|
|
|
|
|
assert.equal(preview.fields.time, '2026-02-20 至 2026-02-23')
|
|
|
|
|
|
assert.equal(preview.fields.days, '4天')
|
|
|
|
|
|
assert.equal(rows.find((row) => row.key === 'time')?.value, '2026-02-20')
|
|
|
|
|
|
assert.equal(rows.find((row) => row.key === 'time_return')?.value, '2026-02-23')
|
|
|
|
|
|
assert.equal(preview.fields.location, '上海')
|
|
|
|
|
|
assert.equal(preview.fields.reason, '辅助国网仿生产项目部署')
|
|
|
|
|
|
assert.doesNotMatch(preview.fields.reason, /小财管家继续执行/)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-06-24 10:42:50 +08:00
|
|
|
|
test('application preview splits compact destination and business purpose', () => {
|
|
|
|
|
|
const preview = buildLocalApplicationPreview(
|
|
|
|
|
|
'2026-02-20 至 2026-02-23,去上海辅助国网仿生产服务器部署,火车',
|
|
|
|
|
|
{
|
|
|
|
|
|
name: '曹笑竹',
|
|
|
|
|
|
departmentName: '技术部',
|
|
|
|
|
|
position: '财务智能化产品经理',
|
|
|
|
|
|
managerName: '向万红',
|
|
|
|
|
|
grade: 'P5'
|
|
|
|
|
|
},
|
|
|
|
|
|
{ today: '2026-06-09' }
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
assert.equal(preview.fields.time, '2026-02-20 至 2026-02-23')
|
|
|
|
|
|
assert.equal(preview.fields.days, '4天')
|
|
|
|
|
|
assert.equal(preview.fields.location, '上海')
|
|
|
|
|
|
assert.equal(preview.fields.reason, '辅助国网仿生产服务器部署')
|
|
|
|
|
|
assert.equal(preview.fields.transportMode, '火车')
|
|
|
|
|
|
assert.equal(preview.readyToSubmit, true)
|
|
|
|
|
|
assert.deepEqual(preview.validationIssues, [])
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-06-13 14:52:26 +00:00
|
|
|
|
test('application preview blocks submit when date range conflicts with explicit days', () => {
|
|
|
|
|
|
const preview = buildLocalApplicationPreview(
|
|
|
|
|
|
'申请2月20-23日去上海出差3天,辅助国网仿生产服务器部署,火车',
|
|
|
|
|
|
{
|
|
|
|
|
|
name: '曹笑竹',
|
|
|
|
|
|
departmentName: '技术部',
|
|
|
|
|
|
position: '财务智能化产品经理',
|
|
|
|
|
|
managerName: '向万红',
|
|
|
|
|
|
grade: 'P5'
|
|
|
|
|
|
},
|
|
|
|
|
|
{ today: '2026-06-09' }
|
|
|
|
|
|
)
|
|
|
|
|
|
const normalized = normalizeApplicationPreview(preview)
|
|
|
|
|
|
const footer = buildApplicationPreviewFooterMessage(normalized)
|
|
|
|
|
|
|
|
|
|
|
|
assert.equal(normalized.fields.time, '2026-02-20 至 2026-02-23')
|
|
|
|
|
|
assert.equal(normalized.fields.days, '3天')
|
|
|
|
|
|
assert.equal(normalized.readyToSubmit, false)
|
|
|
|
|
|
assert.equal(normalized.validationIssues[0].code, 'time_days_conflict')
|
|
|
|
|
|
assert.match(footer, /按自然日为 4 天/)
|
|
|
|
|
|
assert.match(footer, /填写的是 3 天/)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
test('application preview blocks submit when location candidates conflict', () => {
|
|
|
|
|
|
const preview = buildLocalApplicationPreview(
|
|
|
|
|
|
'申请2月20-23日去北京出差4天,地点:上海,辅助国网仿生产服务器部署,火车',
|
|
|
|
|
|
{
|
|
|
|
|
|
name: '曹笑竹',
|
|
|
|
|
|
departmentName: '技术部',
|
|
|
|
|
|
position: '财务智能化产品经理',
|
|
|
|
|
|
managerName: '向万红',
|
|
|
|
|
|
grade: 'P5'
|
|
|
|
|
|
},
|
|
|
|
|
|
{ today: '2026-06-09' }
|
|
|
|
|
|
)
|
|
|
|
|
|
const footer = buildApplicationPreviewFooterMessage(preview)
|
|
|
|
|
|
|
|
|
|
|
|
assert.equal(preview.readyToSubmit, false)
|
|
|
|
|
|
assert.equal(preview.validationIssues[0].code, 'location_candidates_conflict')
|
|
|
|
|
|
assert.match(footer, /同时出现多个地点/)
|
|
|
|
|
|
assert.match(footer, /北京/)
|
|
|
|
|
|
assert.match(footer, /上海/)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
test('application preview does not treat application type labels as locations', () => {
|
|
|
|
|
|
const preview = normalizeApplicationPreview({
|
|
|
|
|
|
sourceText: [
|
|
|
|
|
|
'费用申请出差',
|
|
|
|
|
|
'任务摘要:交通方式和出差预算待补充',
|
|
|
|
|
|
'申请类型:差旅费用申请',
|
|
|
|
|
|
'地点:上海',
|
|
|
|
|
|
'申请2月20日-23日火车去上海出差,服务国网仿生产服务器部署'
|
|
|
|
|
|
].join('\n'),
|
|
|
|
|
|
fields: {
|
|
|
|
|
|
applicationType: '差旅费用申请',
|
|
|
|
|
|
time: '2026-02-20 至 2026-02-23',
|
|
|
|
|
|
location: '上海',
|
|
|
|
|
|
reason: '服务国网仿生产服务器部署',
|
|
|
|
|
|
days: '4天',
|
|
|
|
|
|
transportMode: '火车',
|
|
|
|
|
|
amount: '2120元',
|
|
|
|
|
|
grade: 'P5',
|
|
|
|
|
|
applicant: '曹笑竹',
|
|
|
|
|
|
department: '技术部',
|
|
|
|
|
|
position: '产品经理',
|
|
|
|
|
|
managerName: '向万红'
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
assert.equal(preview.readyToSubmit, true)
|
|
|
|
|
|
assert.deepEqual(preview.validationIssues, [])
|
|
|
|
|
|
assert.doesNotMatch(buildApplicationPreviewFooterMessage(preview), /多个地点|费用申请/)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
test('application preview trusts model-refined fields over noisy source candidates', () => {
|
|
|
|
|
|
const preview = normalizeApplicationPreview({
|
|
|
|
|
|
sourceText: [
|
|
|
|
|
|
'任务摘要:交通方式和出差预算待补充',
|
|
|
|
|
|
'申请2月20日-23日火车去上海出差,服务国网仿生产服务器部署'
|
|
|
|
|
|
].join('\n'),
|
|
|
|
|
|
modelRefined: true,
|
|
|
|
|
|
modelReviewStatus: 'completed',
|
|
|
|
|
|
parseStrategy: 'llm_primary',
|
|
|
|
|
|
fields: {
|
|
|
|
|
|
applicationType: '差旅费用申请',
|
|
|
|
|
|
time: '2026-02-20 至 2026-02-23',
|
|
|
|
|
|
location: '上海',
|
|
|
|
|
|
reason: '服务国网仿生产服务器部署',
|
|
|
|
|
|
days: '4天',
|
|
|
|
|
|
transportMode: '火车',
|
|
|
|
|
|
amount: '2120元',
|
|
|
|
|
|
grade: 'P5',
|
|
|
|
|
|
applicant: '曹笑竹',
|
|
|
|
|
|
department: '技术部',
|
|
|
|
|
|
position: '产品经理',
|
|
|
|
|
|
managerName: '向万红'
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
assert.equal(preview.readyToSubmit, true)
|
|
|
|
|
|
assert.deepEqual(preview.validationIssues, [])
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-06-24 10:42:50 +08:00
|
|
|
|
test('application preview normalizes model-refined location mixed with business content', () => {
|
|
|
|
|
|
const rawText = '申请2月20日-23日火车出差,事由:辅助国网仿生产服务器部署'
|
|
|
|
|
|
const preview = buildModelRefinedApplicationPreview(
|
|
|
|
|
|
buildLocalApplicationPreview(rawText, { name: '曹笑竹', grade: 'P5' }, { today: '2026-06-09' }),
|
|
|
|
|
|
{
|
|
|
|
|
|
parse_strategy: 'llm_primary',
|
|
|
|
|
|
entities: [
|
|
|
|
|
|
{ type: 'expense_type', value: '差旅费', normalized_value: 'travel' },
|
|
|
|
|
|
{ type: 'location', value: '上海辅助国网仿生产服务器', normalized_value: '上海辅助国网仿生产服务器' },
|
|
|
|
|
|
{ type: 'reason', value: '辅助国网仿生产服务器部署', normalized_value: '辅助国网仿生产服务器部署' },
|
|
|
|
|
|
{ type: 'transport_mode', value: '火车', normalized_value: '火车' },
|
|
|
|
|
|
{ type: 'policy_total_amount', value: '2120元', normalized_value: '2120' }
|
|
|
|
|
|
],
|
|
|
|
|
|
time_range: {
|
|
|
|
|
|
start_date: '2026-02-20',
|
|
|
|
|
|
end_date: '2026-02-23'
|
|
|
|
|
|
},
|
|
|
|
|
|
missing_slots: []
|
|
|
|
|
|
},
|
|
|
|
|
|
rawText,
|
|
|
|
|
|
{ name: '曹笑竹', grade: 'P5' }
|
|
|
|
|
|
)
|
|
|
|
|
|
const estimateRequest = buildApplicationPolicyEstimateRequest(preview, { grade: 'P5', location: '武汉' })
|
|
|
|
|
|
const footer = buildApplicationPreviewFooterMessage(preview)
|
|
|
|
|
|
|
|
|
|
|
|
assert.equal(preview.fields.location, '上海')
|
|
|
|
|
|
assert.equal(preview.fields.reason, '辅助国网仿生产服务器部署')
|
|
|
|
|
|
assert.equal(preview.readyToSubmit, true)
|
|
|
|
|
|
assert.deepEqual(preview.validationIssues, [])
|
|
|
|
|
|
assert.match(footer, /#application-submit/)
|
|
|
|
|
|
assert.equal(estimateRequest.canCalculate, true)
|
|
|
|
|
|
assert.equal(estimateRequest.payload.location, '上海')
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-06-13 14:52:26 +00:00
|
|
|
|
test('application preview blocks submit when transport candidates conflict', () => {
|
|
|
|
|
|
const preview = buildLocalApplicationPreview(
|
|
|
|
|
|
'申请2月20-23日去上海出差4天,辅助国网仿生产服务器部署,出行方式:飞机,坐火车',
|
|
|
|
|
|
{
|
|
|
|
|
|
name: '曹笑竹',
|
|
|
|
|
|
departmentName: '技术部',
|
|
|
|
|
|
position: '财务智能化产品经理',
|
|
|
|
|
|
managerName: '向万红',
|
|
|
|
|
|
grade: 'P5'
|
|
|
|
|
|
},
|
|
|
|
|
|
{ today: '2026-06-09' }
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
assert.equal(preview.readyToSubmit, false)
|
|
|
|
|
|
assert.equal(preview.validationIssues[0].code, 'transport_candidates_conflict')
|
|
|
|
|
|
assert.match(buildApplicationPreviewFooterMessage(preview), /同时出现多个出行方式/)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
test('application preview normalizes compact amount candidates', () => {
|
|
|
|
|
|
const preview = buildLocalApplicationPreview(
|
|
|
|
|
|
'申请2月20-23日去上海出差4天,辅助国网仿生产服务器部署,火车,预计费用1.8k',
|
|
|
|
|
|
{
|
|
|
|
|
|
name: '曹笑竹',
|
|
|
|
|
|
departmentName: '技术部',
|
|
|
|
|
|
position: '财务智能化产品经理',
|
|
|
|
|
|
managerName: '向万红',
|
|
|
|
|
|
grade: 'P5'
|
|
|
|
|
|
},
|
|
|
|
|
|
{ today: '2026-06-09' }
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
assert.equal(preview.fields.amount, '1800元')
|
|
|
|
|
|
assert.equal(preview.readyToSubmit, true)
|
|
|
|
|
|
assert.deepEqual(preview.validationIssues, [])
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-06-02 16:22:59 +08:00
|
|
|
|
test('application preview keeps labeled reason in structured travel form', () => {
|
|
|
|
|
|
const preview = buildLocalApplicationPreview([
|
|
|
|
|
|
'发生时间:2026-02-20 至 2026-02-23',
|
|
|
|
|
|
'地点:上海',
|
|
|
|
|
|
'事由:支撑国网仿生产环境建设',
|
|
|
|
|
|
'天数:4天'
|
|
|
|
|
|
].join('\n'), {
|
|
|
|
|
|
name: '曹笑竹',
|
|
|
|
|
|
grade: 'P5'
|
|
|
|
|
|
})
|
|
|
|
|
|
const rows = buildApplicationPreviewRows(preview)
|
|
|
|
|
|
|
|
|
|
|
|
assert.equal(preview.fields.applicationType, '差旅费用申请')
|
|
|
|
|
|
assert.equal(preview.fields.time, '2026-02-20 至 2026-02-23')
|
|
|
|
|
|
assert.equal(preview.fields.location, '上海')
|
|
|
|
|
|
assert.equal(preview.fields.reason, '支撑国网仿生产环境建设')
|
|
|
|
|
|
assert.equal(preview.fields.days, '4天')
|
|
|
|
|
|
assert.equal(rows.find((row) => row.key === 'reason')?.value, '支撑国网仿生产环境建设')
|
|
|
|
|
|
assert.equal(rows.find((row) => row.key === 'reason')?.missing, false)
|
|
|
|
|
|
assert.equal(rows.find((row) => row.key === 'time')?.label, '出发时间')
|
|
|
|
|
|
assert.equal(rows.find((row) => row.key === 'time_return')?.label, '返回时间')
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-05-26 09:15:14 +08:00
|
|
|
|
test('application preview cleans empty time labels and keeps only business reason', () => {
|
|
|
|
|
|
const preview = buildLocalApplicationPreview('发生时间:,去九江出差3天,服务美团业务部署,预计费用1800元,火车', {
|
|
|
|
|
|
name: '李文静',
|
|
|
|
|
|
grade: 'P5'
|
2026-05-30 15:46:51 +08:00
|
|
|
|
}, {
|
|
|
|
|
|
today: '2026-05-29'
|
2026-05-26 09:15:14 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
2026-05-30 15:46:51 +08:00
|
|
|
|
assert.equal(preview.fields.time, '2026-05-29 至 2026-05-31')
|
2026-05-26 09:15:14 +08:00
|
|
|
|
assert.equal(preview.fields.location, '九江')
|
|
|
|
|
|
assert.equal(preview.fields.days, '3天')
|
|
|
|
|
|
assert.equal(preview.fields.reason, '服务美团业务部署')
|
|
|
|
|
|
assert.equal(preview.fields.transportMode, '火车')
|
|
|
|
|
|
assert.doesNotMatch(preview.fields.reason, /发生时间|去九江|出差3天/)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
test('application preview can be refined by ontology model extraction', () => {
|
|
|
|
|
|
const rawText = '发生时间:,去九江出差3天,服务美团业务部署,预计费用1800元,火车'
|
2026-05-30 15:46:51 +08:00
|
|
|
|
const localPreview = buildLocalApplicationPreview(rawText, { name: '李文静', grade: 'P5' }, { today: '2026-05-29' })
|
2026-05-26 09:15:14 +08:00
|
|
|
|
const refinedPreview = buildModelRefinedApplicationPreview(
|
|
|
|
|
|
localPreview,
|
|
|
|
|
|
{
|
|
|
|
|
|
parse_strategy: 'llm_primary',
|
|
|
|
|
|
entities: [
|
|
|
|
|
|
{ type: 'expense_type', value: '差旅费', normalized_value: 'travel' },
|
|
|
|
|
|
{ type: 'location', value: '九江', normalized_value: '九江' },
|
|
|
|
|
|
{ type: 'reason', value: '服务美团业务部署', normalized_value: '服务美团业务部署' },
|
|
|
|
|
|
{ type: 'transport_mode', value: '火车', normalized_value: '火车' },
|
|
|
|
|
|
{ type: 'amount', value: '1800元', normalized_value: '1800' }
|
|
|
|
|
|
],
|
|
|
|
|
|
time_range: {},
|
|
|
|
|
|
missing_slots: []
|
|
|
|
|
|
},
|
|
|
|
|
|
rawText,
|
|
|
|
|
|
{ name: '李文静', grade: 'P5' }
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
assert.equal(refinedPreview.modelRefined, true)
|
|
|
|
|
|
assert.equal(refinedPreview.parseStrategy, 'llm_primary')
|
|
|
|
|
|
assert.equal(refinedPreview.modelReviewStatus, 'completed')
|
|
|
|
|
|
assert.equal(refinedPreview.fields.applicationType, '差旅费用申请')
|
2026-05-30 15:46:51 +08:00
|
|
|
|
assert.equal(refinedPreview.fields.time, '2026-05-29 至 2026-05-31')
|
2026-05-26 09:15:14 +08:00
|
|
|
|
assert.equal(refinedPreview.fields.reason, '服务美团业务部署')
|
|
|
|
|
|
assert.equal(refinedPreview.fields.transportMode, '火车')
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-06-13 14:52:26 +00:00
|
|
|
|
test('application preview preserves ontology amount roles for travel estimates', () => {
|
|
|
|
|
|
const rawText = '申请2月20日-23日火车去上海出差,服务国网仿生产服务器部署'
|
|
|
|
|
|
const localPreview = buildLocalApplicationPreview(rawText, { name: '曹笑竹', grade: 'P5' }, { today: '2026-06-13' })
|
|
|
|
|
|
const refinedPreview = buildModelRefinedApplicationPreview(
|
|
|
|
|
|
localPreview,
|
|
|
|
|
|
{
|
|
|
|
|
|
parse_strategy: 'llm_primary',
|
|
|
|
|
|
entities: [
|
|
|
|
|
|
{ type: 'expense_type', value: '差旅费', normalized_value: 'travel' },
|
|
|
|
|
|
{ type: 'location', value: '上海', normalized_value: '上海' },
|
|
|
|
|
|
{ type: 'reason', value: '服务国网仿生产服务器部署', normalized_value: '服务国网仿生产服务器部署' },
|
|
|
|
|
|
{ type: 'transport_mode', value: '火车', normalized_value: '火车' },
|
|
|
|
|
|
{ type: 'transport_estimated_amount', value: '720元', normalized_value: '720' },
|
|
|
|
|
|
{ type: 'hotel_amount', value: '1000元', normalized_value: '1000' },
|
|
|
|
|
|
{ type: 'allowance_amount', value: '400元', normalized_value: '400' },
|
|
|
|
|
|
{ type: 'policy_total_amount', value: '2120元', normalized_value: '2120' }
|
|
|
|
|
|
],
|
|
|
|
|
|
time_range: {
|
|
|
|
|
|
start_date: '2026-02-20',
|
|
|
|
|
|
end_date: '2026-02-23'
|
|
|
|
|
|
},
|
|
|
|
|
|
missing_slots: []
|
|
|
|
|
|
},
|
|
|
|
|
|
rawText,
|
|
|
|
|
|
{ name: '曹笑竹', grade: 'P5' }
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
assert.equal(refinedPreview.fields.amount, '2120元')
|
|
|
|
|
|
assert.equal(refinedPreview.fields.transportEstimatedAmount, '720元')
|
|
|
|
|
|
assert.equal(refinedPreview.fields.hotelAmount, '1000元')
|
|
|
|
|
|
assert.equal(refinedPreview.fields.allowanceAmount, '400元')
|
|
|
|
|
|
assert.equal(refinedPreview.fields.policyTotalAmount, '2120元')
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
test('application preview ignores model reason polluted by application type', () => {
|
|
|
|
|
|
const rawText = '我申请2月20日至23日去上海出差,辅助国网方法生产服务器上线部署,'
|
|
|
|
|
|
const localPreview = buildLocalApplicationPreview(rawText, {
|
|
|
|
|
|
name: '曹笑竹',
|
|
|
|
|
|
grade: 'P5'
|
|
|
|
|
|
}, {
|
|
|
|
|
|
today: '2026-06-13'
|
|
|
|
|
|
})
|
|
|
|
|
|
const refinedPreview = buildModelRefinedApplicationPreview(
|
|
|
|
|
|
localPreview,
|
|
|
|
|
|
{
|
|
|
|
|
|
parse_strategy: 'llm_primary',
|
|
|
|
|
|
entities: [
|
|
|
|
|
|
{ type: 'expense_type', value: '差旅费', normalized_value: 'travel' },
|
|
|
|
|
|
{ type: 'location', value: '上海', normalized_value: '上海' },
|
|
|
|
|
|
{ type: 'reason', value: '类型:差旅费用申请', normalized_value: '类型:差旅费用申请' }
|
|
|
|
|
|
],
|
|
|
|
|
|
missing_slots: []
|
|
|
|
|
|
},
|
|
|
|
|
|
rawText,
|
|
|
|
|
|
{ name: '曹笑竹', grade: 'P5' }
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
assert.equal(localPreview.fields.reason, '辅助国网方法生产服务器上线部署')
|
|
|
|
|
|
assert.equal(refinedPreview.fields.reason, '辅助国网方法生产服务器上线部署')
|
|
|
|
|
|
assert.doesNotMatch(refinedPreview.fields.reason, /类型|差旅费用申请/)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
test('application preview strips internal steward instruction from reason', () => {
|
|
|
|
|
|
const preview = buildLocalApplicationPreview(
|
|
|
|
|
|
'申请2月20-23日去上海出差,事由:辅助国网仿生产服务器部署请直接生成申请单核对结果,信息足够时生成申请单,但在入库或提交审批前仍需让我确认',
|
|
|
|
|
|
{ name: '曹笑竹', grade: 'P5' },
|
|
|
|
|
|
{ today: '2026-06-09' }
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
assert.equal(preview.fields.reason, '辅助国网仿生产服务器部署')
|
|
|
|
|
|
assert.doesNotMatch(preview.fields.reason, /请直接生成|入库|提交审批/)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
test('application preview requires explicit transport mode before submit', () => {
|
2026-06-03 16:36:02 +08:00
|
|
|
|
const rawText = '\u7533\u8bf7 2026-05-25 \u81f3 2026-05-27 \u53bb\u4e0a\u6d77\u51fa\u5dee3\u5929\uff0c\u670d\u52a1\u9879\u76ee\u90e8\u7f72\uff0c\u9884\u8ba1\u8d39\u75281800\u5143'
|
|
|
|
|
|
const localPreview = buildLocalApplicationPreview(rawText, {
|
|
|
|
|
|
name: '\u674e\u6587\u9759',
|
|
|
|
|
|
grade: 'P5'
|
|
|
|
|
|
})
|
|
|
|
|
|
const refinedPreview = buildModelRefinedApplicationPreview(
|
|
|
|
|
|
localPreview,
|
|
|
|
|
|
{
|
|
|
|
|
|
parse_strategy: 'llm_primary',
|
|
|
|
|
|
entities: [
|
|
|
|
|
|
{ type: 'expense_type', value: '\u5dee\u65c5\u8d39', normalized_value: 'travel' },
|
|
|
|
|
|
{ type: 'location', value: '\u4e0a\u6d77', normalized_value: '\u4e0a\u6d77' },
|
|
|
|
|
|
{ type: 'reason', value: '\u670d\u52a1\u9879\u76ee\u90e8\u7f72', normalized_value: '\u670d\u52a1\u9879\u76ee\u90e8\u7f72' },
|
|
|
|
|
|
{ type: 'transport_mode', value: '\u706b\u8f66', normalized_value: '\u706b\u8f66' },
|
|
|
|
|
|
{ type: 'amount', value: '1800\u5143', normalized_value: '1800' }
|
|
|
|
|
|
],
|
|
|
|
|
|
time_range: {
|
|
|
|
|
|
start: '2026-05-25',
|
|
|
|
|
|
end: '2026-05-27'
|
|
|
|
|
|
},
|
|
|
|
|
|
missing_slots: []
|
|
|
|
|
|
},
|
|
|
|
|
|
rawText,
|
|
|
|
|
|
{ name: '\u674e\u6587\u9759', grade: 'P5' }
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
assert.equal(localPreview.fields.transportMode, '')
|
|
|
|
|
|
assert.equal(refinedPreview.fields.transportMode, '')
|
2026-06-13 14:52:26 +00:00
|
|
|
|
assert.equal(refinedPreview.missingFields.includes('\u51fa\u884c\u65b9\u5f0f'), true)
|
2026-06-03 16:36:02 +08:00
|
|
|
|
assert.equal(refinedPreview.readyToSubmit, false)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-06-13 14:52:26 +00:00
|
|
|
|
test('application preview does not treat transport prompt options as selected mode', () => {
|
|
|
|
|
|
const preview = buildLocalApplicationPreview(
|
|
|
|
|
|
'当前还需要补充:出行方式。请先补充出行方式,可以选择火车、飞机或轮船。',
|
|
|
|
|
|
{ name: '李文静', grade: 'P5' }
|
|
|
|
|
|
)
|
|
|
|
|
|
const mixedPreview = buildLocalApplicationPreview(
|
|
|
|
|
|
'任务摘要:交通方式和出差预算待补充\n申请2月20日-23日火车去上海出差',
|
|
|
|
|
|
{ name: '李文静', grade: 'P5' },
|
|
|
|
|
|
{ today: '2026-06-09' }
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
assert.equal(preview.fields.transportMode, '')
|
|
|
|
|
|
assert.equal(mixedPreview.fields.transportMode, '火车')
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-05-30 15:46:51 +08:00
|
|
|
|
test('application preview precomputes a date range from today when only days are provided', () => {
|
|
|
|
|
|
const preview = buildLocalApplicationPreview(
|
|
|
|
|
|
'去北京出差3天,支撑国网仿生产环境部署,飞机,预计费用12000元',
|
|
|
|
|
|
{ name: '李文静', grade: 'P5' },
|
|
|
|
|
|
{ today: '2026-05-29' }
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
assert.equal(preview.fields.time, '2026-05-29 至 2026-05-31')
|
|
|
|
|
|
assert.equal(preview.fields.days, '3天')
|
|
|
|
|
|
assert.equal(preview.readyToSubmit, true)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-05-26 09:15:14 +08:00
|
|
|
|
test('application preview keeps rule fallback distinct from model reviewed result', () => {
|
2026-06-13 14:52:26 +00:00
|
|
|
|
const rawText = '申请 2026-05-20 至 2026-05-23 去上海支撑服务器部署,出差4天,火车,预计费用1800元'
|
2026-05-26 09:15:14 +08:00
|
|
|
|
const localPreview = buildLocalApplicationPreview(rawText, { name: '李文静', grade: 'P5' })
|
|
|
|
|
|
const fallbackPreview = buildModelRefinedApplicationPreview(
|
|
|
|
|
|
localPreview,
|
|
|
|
|
|
{
|
|
|
|
|
|
parse_strategy: 'rule_fallback',
|
|
|
|
|
|
entities: [
|
|
|
|
|
|
{ type: 'expense_type', value: '差旅费', normalized_value: 'travel' },
|
|
|
|
|
|
{ type: 'location', value: '上海', normalized_value: '上海' },
|
|
|
|
|
|
{ type: 'amount', value: '1800元', normalized_value: '1800' }
|
|
|
|
|
|
],
|
|
|
|
|
|
time_range: {
|
|
|
|
|
|
start: '2026-05-20',
|
|
|
|
|
|
end: '2026-05-23'
|
|
|
|
|
|
},
|
|
|
|
|
|
missing_slots: []
|
|
|
|
|
|
},
|
|
|
|
|
|
rawText,
|
|
|
|
|
|
{ name: '李文静', grade: 'P5' }
|
|
|
|
|
|
)
|
|
|
|
|
|
const message = buildLocalApplicationPreviewMessage(fallbackPreview)
|
|
|
|
|
|
const footer = buildApplicationPreviewFooterMessage(fallbackPreview)
|
|
|
|
|
|
|
|
|
|
|
|
assert.equal(fallbackPreview.modelReviewStatus, 'fallback')
|
|
|
|
|
|
assert.match(message, /规则兜底/)
|
2026-05-27 12:27:17 +08:00
|
|
|
|
assert.match(footer, /请确认上述的信息是否填写正确/)
|
|
|
|
|
|
assert.match(footer, /#application-submit/)
|
2026-05-26 09:15:14 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
test('application preview with missing budget stays in chat and asks for补充信息', () => {
|
|
|
|
|
|
const preview = buildLocalApplicationPreview('我想申请去北京出差,高铁,但是不知道预算', {
|
|
|
|
|
|
name: '李文静',
|
|
|
|
|
|
grade: 'P5'
|
|
|
|
|
|
})
|
|
|
|
|
|
assert.equal(preview.fields.amount, '待测算')
|
|
|
|
|
|
assert.equal(preview.readyToSubmit, false)
|
|
|
|
|
|
assert.match(buildLocalApplicationPreviewMessage(preview), /下方表格/)
|
|
|
|
|
|
assert.doesNotMatch(buildLocalApplicationPreviewMessage(preview), /当前还需要补充/)
|
|
|
|
|
|
assert.match(buildApplicationPreviewFooterMessage(preview), /当前还需要补充/)
|
|
|
|
|
|
assert.doesNotMatch(buildLocalApplicationPreviewMessage(preview), /#application-submit/)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
test('application quick start renders a template without model review', () => {
|
|
|
|
|
|
const preview = buildApplicationTemplatePreview({
|
|
|
|
|
|
name: '李文静',
|
|
|
|
|
|
departmentName: '财务部',
|
2026-06-01 17:07:14 +08:00
|
|
|
|
position: '财务分析师',
|
|
|
|
|
|
managerName: '王强',
|
2026-05-26 09:15:14 +08:00
|
|
|
|
grade: 'P5'
|
|
|
|
|
|
})
|
|
|
|
|
|
const message = buildLocalApplicationPreviewMessage(preview)
|
|
|
|
|
|
|
|
|
|
|
|
assert.equal(preview.modelReviewStatus, 'template')
|
|
|
|
|
|
assert.equal(preview.fields.applicationType, '费用申请')
|
|
|
|
|
|
assert.equal(preview.fields.applicant, '李文静')
|
|
|
|
|
|
assert.equal(preview.fields.department, '财务部')
|
2026-06-01 17:07:14 +08:00
|
|
|
|
assert.equal(preview.fields.position, '财务分析师')
|
|
|
|
|
|
assert.equal(preview.fields.managerName, '王强')
|
2026-05-26 09:15:14 +08:00
|
|
|
|
assert.equal(preview.fields.grade, 'P5')
|
2026-05-27 10:32:08 +08:00
|
|
|
|
assert.equal(buildApplicationPreviewRows(preview).find((row) => row.key === 'grade')?.editable, false)
|
2026-05-26 09:15:14 +08:00
|
|
|
|
assert.match(message, /不调用大模型/)
|
|
|
|
|
|
assert.match(message, /点击对应行直接填写/)
|
|
|
|
|
|
assert.doesNotMatch(message, /#application-submit/)
|
|
|
|
|
|
assert.match(buildApplicationPreviewFooterMessage(preview), /当前还需要补充/)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-05-27 14:35:17 +08:00
|
|
|
|
test('application quick start template counts as deletable session content', () => {
|
|
|
|
|
|
const welcomeMessage = createConversationMessage('assistant', '欢迎语', [], {
|
|
|
|
|
|
isWelcome: true,
|
|
|
|
|
|
welcomeQuickActions: [{ label: '快速发起申请', action: 'start_guided_application' }]
|
|
|
|
|
|
})
|
|
|
|
|
|
const templateMessage = createConversationMessage('assistant', '申请模板', [], {
|
|
|
|
|
|
applicationPreview: buildApplicationTemplatePreview({
|
|
|
|
|
|
name: '测试员工',
|
|
|
|
|
|
departmentName: '财务部',
|
|
|
|
|
|
grade: 'P5'
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
assert.equal(hasMeaningfulSessionMessages([welcomeMessage]), false)
|
|
|
|
|
|
assert.equal(hasMeaningfulSessionMessages([welcomeMessage, templateMessage]), true)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-05-26 09:15:14 +08:00
|
|
|
|
test('application session shows intent flow, persists preview, and supports inline table edit', () => {
|
|
|
|
|
|
assert.match(submitComposerScript, /shouldUseLocalApplicationPreview/)
|
|
|
|
|
|
assert.match(submitComposerScript, /buildLocalApplicationPreviewMessage/)
|
|
|
|
|
|
assert.match(submitComposerScript, /buildApplicationPreviewWithModelReview/)
|
|
|
|
|
|
assert.match(submitComposerScript, /fetchOntologyParse/)
|
|
|
|
|
|
assert.match(submitComposerScript, /calculateTravelReimbursement/)
|
|
|
|
|
|
assert.match(submitComposerScript, /buildApplicationPolicyEstimateRequest/)
|
|
|
|
|
|
assert.match(submitComposerScript, /模型复核中/)
|
|
|
|
|
|
assert.match(submitComposerScript, /startFlowStep\('intent'/)
|
|
|
|
|
|
assert.match(submitComposerScript, /startFlowStep\('application-review-preview'/)
|
|
|
|
|
|
assert.match(submitComposerScript, /completeFlowStep\('intent'/)
|
2026-06-06 17:19:07 +08:00
|
|
|
|
assert.match(submitComposerScript, /function resetStewardDelegatedInsightState\(\) \{[\s\S]*insightPanelCollapsed\.value = true/)
|
2026-05-26 09:15:14 +08:00
|
|
|
|
assert.doesNotMatch(submitComposerScript, /void refineApplicationPreviewWithModel/)
|
|
|
|
|
|
assert.match(submitComposerScript, /return null[\s\S]*const hasUnsavedReviewDraft/)
|
|
|
|
|
|
assert.ok(
|
|
|
|
|
|
submitComposerScript.indexOf('shouldUseLocalApplicationPreview') <
|
|
|
|
|
|
submitComposerScript.indexOf('const payload = await runOrchestrator')
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
assert.match(createViewScript, /const isApplicationSession = computed/)
|
|
|
|
|
|
assert.match(createViewScript, /insightPanelCollapsed,/)
|
|
|
|
|
|
assert.doesNotMatch(createViewScript, /if \(isApplicationSession\.value\) \{\s*return false\s*\}/)
|
2026-05-27 14:35:17 +08:00
|
|
|
|
assert.match(createViewScript, /activeFlowSteps\.value\.length > 0/)
|
2026-05-26 09:15:14 +08:00
|
|
|
|
assert.match(createViewScript, /useApplicationPreviewEditor/)
|
2026-06-13 14:52:26 +00:00
|
|
|
|
assert.match(messageActionsScript, /message-bubble-application-preview/)
|
|
|
|
|
|
assert.match(messageActionsScript, /buildApplicationPreviewFooterMessage/)
|
|
|
|
|
|
assert.match(messageActionsScript, /function buildApplicationPreviewFooterText\(message\)/)
|
|
|
|
|
|
assert.match(stewardRuntimeScript, /buildApplicationPreviewSubmitText/)
|
|
|
|
|
|
assert.match(stewardRuntimeScript, /user_input_text: applicationSubmitText/)
|
2026-05-26 09:15:14 +08:00
|
|
|
|
assert.match(conversationModelScript, /applicationPreview: null/)
|
|
|
|
|
|
assert.match(conversationModelScript, /applicationPreview: message\.applicationPreview \|\| null/)
|
2026-05-27 14:35:17 +08:00
|
|
|
|
assert.match(conversationModelScript, /\|\| message\.applicationPreview/)
|
|
|
|
|
|
assert.match(createViewScript, /hasMeaningfulSessionMessages\(messages\.value\)/)
|
2026-05-26 09:15:14 +08:00
|
|
|
|
|
2026-05-27 10:32:08 +08:00
|
|
|
|
assert.match(messageItemTemplate, /class="application-preview-table"/)
|
|
|
|
|
|
assert.match(messageItemTemplate, /class="application-preview-footer application-preview-footer-missing"/)
|
|
|
|
|
|
assert.match(messageItemTemplate, /application-preview-missing-chip/)
|
|
|
|
|
|
assert.match(messageItemTemplate, /当前还需要补充:/)
|
|
|
|
|
|
assert.match(messageItemTemplate, /补齐后我再帮您提交申请。/)
|
|
|
|
|
|
assert.match(messageItemTemplate, /class="application-preview-footer message-answer-content message-answer-markdown"/)
|
|
|
|
|
|
assert.match(messageItemTemplate, /v-html="ui\.renderMarkdown\(ui\.buildApplicationPreviewFooterText\(message\)\)"/)
|
2026-05-30 15:46:51 +08:00
|
|
|
|
assert.doesNotMatch(messageItemTemplate, /class="application-date-editor-layer"/)
|
|
|
|
|
|
assert.doesNotMatch(messageItemTemplate, /ui\.commitApplicationPreviewDateEditor\(message\)/)
|
2026-06-13 14:52:26 +00:00
|
|
|
|
assert.doesNotMatch(messageItemTemplate, /application-preview-date-chip/)
|
2026-05-30 15:46:51 +08:00
|
|
|
|
assert.match(messageItemTemplate, /申请单据已生成/)
|
2026-06-02 14:01:51 +08:00
|
|
|
|
assert.match(messageItemTemplate, /ui\.shouldShowDraftSavedCard\(message\)/)
|
|
|
|
|
|
assert.match(messageItemTemplate, /报销草稿已生成/)
|
2026-06-06 17:19:07 +08:00
|
|
|
|
assert.match(messageItemTemplate, /报销草稿待保存/)
|
2026-06-02 14:01:51 +08:00
|
|
|
|
assert.match(messageItemTemplate, /ui\.resolveReimbursementDraftClaimNo\(message\.draftPayload\)/)
|
2026-06-06 17:19:07 +08:00
|
|
|
|
assert.match(messageItemTemplate, /v-if="ui\.canOpenDraftDetail\(message\)"/)
|
2026-06-02 14:01:51 +08:00
|
|
|
|
assert.match(messageItemTemplate, /class="reimbursement-draft-link"/)
|
|
|
|
|
|
assert.match(messageItemTemplate, /查看详情/)
|
2026-06-06 17:19:07 +08:00
|
|
|
|
assert.match(messageItemTemplate, /class="reimbursement-draft-pending-detail"/)
|
|
|
|
|
|
assert.match(messageItemTemplate, /保存后可查看详情/)
|
2026-06-13 14:52:26 +00:00
|
|
|
|
assert.match(messageActionsScript, /function canOpenDraftDetail\(message\)/)
|
2026-06-06 17:19:07 +08:00
|
|
|
|
assert.match(createViewScript, /canOpenDraftDetail,/)
|
2026-06-13 14:52:26 +00:00
|
|
|
|
assert.match(messageActionsScript, /保存后生成/)
|
2026-06-02 14:01:51 +08:00
|
|
|
|
assert.doesNotMatch(messageItemTemplate, /ui\.buildReimbursementDraftSummaryItems\(message\.draftPayload\)/)
|
|
|
|
|
|
assert.doesNotMatch(messageItemTemplate, /可以继续上传票据,我会归集到这张草稿。/)
|
|
|
|
|
|
assert.ok(
|
|
|
|
|
|
messageItemTemplate.indexOf('class="draft-preview application-draft-preview"')
|
|
|
|
|
|
< messageItemTemplate.indexOf('class="message-detail-block review-message-block"')
|
|
|
|
|
|
)
|
2026-05-30 15:46:51 +08:00
|
|
|
|
assert.match(messageItemTemplate, /application-draft-head/)
|
|
|
|
|
|
assert.match(messageItemTemplate, /mdi mdi-file-document-check-outline/)
|
2026-06-02 14:01:51 +08:00
|
|
|
|
assert.match(messageItemTemplate, /mdi mdi-file-document-edit-outline/)
|
2026-05-30 15:46:51 +08:00
|
|
|
|
assert.match(messageItemTemplate, /'is-primary': item\.label === '单号'/)
|
|
|
|
|
|
assert.match(messageItemTemplate, /完整审批链、附件和明细可在单据详情中[\s\S]*application-draft-detail-link[\s\S]*>查看<\/button>/)
|
|
|
|
|
|
assert.doesNotMatch(messageItemTemplate, /application-draft-detail-btn/)
|
|
|
|
|
|
assert.match(messageItemTemplate, /ui\.openApplicationDraftDetail\(message\)/)
|
2026-06-06 17:19:07 +08:00
|
|
|
|
assert.doesNotMatch(messageItemTemplate, /<OperationFeedbackInlineCard/)
|
|
|
|
|
|
assert.match(messageItemTemplate, /class="message-action-toolbar"/)
|
|
|
|
|
|
assert.match(messageItemTemplate, /ui\.shouldShowAssistantMessageActions\(message\)/)
|
|
|
|
|
|
assert.match(messageItemTemplate, /ui\.copyAssistantMessage\(message\)/)
|
|
|
|
|
|
assert.match(messageItemTemplate, /ui\.speakAssistantMessage\(message\)/)
|
|
|
|
|
|
assert.match(messageItemTemplate, /ui\.submitOperationFeedbackForMessage\(message, \{ rating: 5, reason: 'thumbs_up' \}\)/)
|
|
|
|
|
|
assert.match(messageItemTemplate, /ui\.submitOperationFeedbackForMessage\(message, \{ rating: 1, reason: 'thumbs_down' \}\)/)
|
|
|
|
|
|
assert.match(messageItemTemplate, /mdi mdi-content-copy/)
|
|
|
|
|
|
assert.match(messageItemTemplate, /mdi mdi-volume-high/)
|
2026-06-02 14:01:51 +08:00
|
|
|
|
assert.match(submitComposerScript, /employee_grade:\s*user\.grade \|\| user\.employeeGrade \|\| user\.employee_grade/)
|
|
|
|
|
|
assert.match(submitComposerScript, /employeeGrade:\s*user\.grade \|\| user\.employeeGrade \|\| user\.employee_grade/)
|
2026-05-26 09:15:14 +08:00
|
|
|
|
assert.match(createViewTemplate, /'has-insight': hasInsightPanelContent && showInsightPanel/)
|
2026-05-27 10:32:08 +08:00
|
|
|
|
assert.match(messageItemTemplate, /v-model="ui\.applicationPreviewEditor\.draftValue"/)
|
2026-05-30 15:46:51 +08:00
|
|
|
|
assert.doesNotMatch(messageItemTemplate, /v-model="ui\.applicationPreviewEditor\.singleDate"/)
|
2026-05-27 10:32:08 +08:00
|
|
|
|
assert.match(messageItemTemplate, /application-preview-select/)
|
|
|
|
|
|
assert.match(messageItemTemplate, /resolveApplicationPreviewEditorOptions/)
|
|
|
|
|
|
assert.match(messageItemTemplate, /row\.editable && !ui\.isApplicationPreviewEditing\(message, row\.key\).*ui\.openApplicationPreviewEditor\(message, row\.key, row\.value\)"/)
|
|
|
|
|
|
assert.match(messageItemTemplate, /@keydown\.enter\.prevent="row\.editable && !ui\.isApplicationPreviewEditing\(message, row\.key\).*ui\.openApplicationPreviewEditor\(message, row\.key, row\.value\)"/)
|
|
|
|
|
|
assert.match(messageItemTemplate, /@keydown\.stop="ui\.handleApplicationPreviewEditorKeydown\(\$event, message\)"/)
|
|
|
|
|
|
assert.match(messageItemTemplate, /mdi mdi-pencil-outline/)
|
|
|
|
|
|
assert.match(messageItemTemplate, /@click\.stop="ui\.openApplicationPreviewEditor\(message, row\.key, row\.value\)"/)
|
|
|
|
|
|
assert.match(messageItemTemplate, /openApplicationPreviewEditor/)
|
|
|
|
|
|
assert.match(messageItemTemplate, /commitApplicationPreviewEditor/)
|
|
|
|
|
|
assert.match(createViewScript, /resolveApplicationPreviewMissingFields/)
|
2026-05-30 15:46:51 +08:00
|
|
|
|
assert.match(createViewScript, /function applyLinkedApplicationPreviewDateSelection/)
|
|
|
|
|
|
assert.match(createViewScript, /onComposerDateSelection: applyLinkedApplicationPreviewDateSelection/)
|
|
|
|
|
|
assert.match(createViewScript, /function openApplicationPreviewEditorFromUi/)
|
|
|
|
|
|
assert.match(createViewScript, /syncComposerDateFromApplicationEditor/)
|
2026-06-13 14:52:26 +00:00
|
|
|
|
assert.match(messageActionsScript, /function shouldShowAssistantMessageActions/)
|
|
|
|
|
|
assert.match(messageActionsScript, /function buildMessageOperationFeedbackContext/)
|
|
|
|
|
|
assert.match(messageActionsScript, /function isMessageFeedbackSelected/)
|
|
|
|
|
|
assert.match(messageActionsScript, /function submitOperationFeedbackForMessage/)
|
|
|
|
|
|
assert.match(stewardRuntimeScript, /const stewardSubmitContinuation = message\?\.stewardContinuation \|\| null/)
|
|
|
|
|
|
assert.match(stewardRuntimeScript, /stewardContinuation:\s*stewardSubmitContinuation/)
|
2026-05-30 15:46:51 +08:00
|
|
|
|
assert.match(createViewTemplate, /handleComposerDateInputChange\('single'\)/)
|
|
|
|
|
|
assert.match(createViewTemplate, /handleComposerDateInputChange\('range-start'\)/)
|
|
|
|
|
|
assert.match(createViewTemplate, /handleComposerDateInputChange\('range-end'\)/)
|
|
|
|
|
|
assert.doesNotMatch(createViewTemplate, /@click="applyComposerDateSelection"/)
|
2026-05-26 09:15:14 +08:00
|
|
|
|
|
|
|
|
|
|
assert.match(previewEditorScript, /normalizeApplicationPreview/)
|
|
|
|
|
|
assert.match(previewEditorScript, /APPLICATION_TRANSPORT_MODE_OPTIONS/)
|
2026-05-30 15:46:51 +08:00
|
|
|
|
assert.match(previewEditorScript, /getTodayDateValue/)
|
2026-05-26 09:15:14 +08:00
|
|
|
|
assert.match(previewEditorScript, /buildLocalApplicationPreviewMessage/)
|
|
|
|
|
|
assert.match(previewEditorScript, /targetRow\.editable === false/)
|
|
|
|
|
|
assert.match(previewEditorScript, /\[editor\.fieldKey\]: nextValue/)
|
2026-05-30 15:46:51 +08:00
|
|
|
|
assert.match(previewEditorScript, /fieldKey === 'time'\) return 'date'/)
|
|
|
|
|
|
assert.match(previewEditorScript, /commitApplicationPreviewDateEditor/)
|
2026-05-27 10:32:08 +08:00
|
|
|
|
|
2026-05-30 15:46:51 +08:00
|
|
|
|
assert.match(messageItemStyles, /@import "\.\/travel-reimbursement-message-application\.css";/)
|
2026-05-27 10:32:08 +08:00
|
|
|
|
assert.match(messageItemStyles, /\.application-preview-row\.missing \{[\s\S]*--theme-primary-rgb/)
|
|
|
|
|
|
assert.match(messageItemStyles, /\.application-preview-table \{[\s\S]*border: 1px solid #d7e4f2;[\s\S]*background: #ffffff;/)
|
|
|
|
|
|
assert.match(messageItemStyles, /\.application-preview-row \{[\s\S]*grid-template-columns: 108px minmax\(0, 1fr\);/)
|
|
|
|
|
|
assert.match(messageItemStyles, /\.application-preview-text \{[\s\S]*overflow-wrap: anywhere;/)
|
|
|
|
|
|
assert.match(messageItemStyles, /\.application-preview-select \{[\s\S]*width: 100%;/)
|
2026-05-27 12:27:17 +08:00
|
|
|
|
assert.match(messageItemStyles, /\.application-preview-footer \{[\s\S]*margin-top: 48px;/)
|
|
|
|
|
|
assert.match(messageItemStyles, /\.message-answer-markdown :deep\(\.markdown-action-link\) \{[\s\S]*text-decoration: underline;/)
|
2026-05-27 10:32:08 +08:00
|
|
|
|
assert.match(messageItemStyles, /\.application-preview-footer-missing \{[\s\S]*margin-top: 48px;[\s\S]*background: transparent;/)
|
|
|
|
|
|
assert.match(messageItemStyles, /\.application-preview-missing-chip \{[\s\S]*background: rgba\(var\(--theme-primary-rgb/)
|
2026-05-30 15:46:51 +08:00
|
|
|
|
assert.doesNotMatch(applicationMessageStyles, /\.application-date-editor-layer/)
|
|
|
|
|
|
assert.match(applicationMessageStyles, /\.application-draft-preview \.application-draft-head \{[\s\S]*grid-template-columns: 36px minmax\(0, 1fr\) auto;/)
|
2026-06-01 17:07:14 +08:00
|
|
|
|
assert.match(applicationMessageStyles, /\.application-draft-brief \{[\s\S]*gap: 1px;[\s\S]*border: 1px solid #d7e4f2;[\s\S]*background: #d7e4f2;/)
|
|
|
|
|
|
assert.match(applicationMessageStyles, /\.application-draft-brief-item \{[\s\S]*border: 0;[\s\S]*background: #ffffff;/)
|
|
|
|
|
|
assert.doesNotMatch(applicationMessageStyles, /\.application-draft-brief-item:nth-child\(even\)/)
|
2026-05-30 15:46:51 +08:00
|
|
|
|
assert.match(applicationMessageStyles, /\.application-draft-brief-item\.is-primary \{[\s\S]*grid-column: 1 \/ -1;/)
|
2026-06-02 14:01:51 +08:00
|
|
|
|
assert.match(applicationMessageStyles, /\.application-draft-preview\.reimbursement-draft-preview \{[\s\S]*max-width: 520px;/)
|
|
|
|
|
|
assert.match(applicationMessageStyles, /\.reimbursement-draft-card \{[\s\S]*grid-template-columns: 30px minmax\(0, 1fr\);/)
|
|
|
|
|
|
assert.match(applicationMessageStyles, /\.reimbursement-draft-link \{[\s\S]*text-decoration: underline;/)
|
2026-05-30 15:46:51 +08:00
|
|
|
|
|
|
|
|
|
|
assert.match(flowScript, /application-submit-success/)
|
|
|
|
|
|
assert.match(flowScript, /function shouldHideToolCall/)
|
|
|
|
|
|
assert.match(flowScript, /semantic_ontology/)
|
|
|
|
|
|
assert.match(flowScript, /return null/)
|
|
|
|
|
|
assert.match(flowScript, /申请单提交成功/)
|
2026-06-02 14:01:51 +08:00
|
|
|
|
assert.match(submitComposerScript, /const isApplicationSubmitOperation = feedbackOperationType === 'submit_application'/)
|
2026-06-06 17:19:07 +08:00
|
|
|
|
assert.match(submitComposerScript, /if \(!stewardDelegated && isApplicationSubmitOperation\) \{[\s\S]*startFlowStep\('application-submit-success'/)
|
|
|
|
|
|
assert.match(submitComposerScript, /else if \(!stewardDelegated && rawText && !reviewAction\) \{[\s\S]*startFlowStep\('intent'/)
|
|
|
|
|
|
assert.match(submitComposerScript, /if \(!isApplicationSubmitOperation && !stewardDelegated\) \{[\s\S]*startExpenseClaimDraftFlowStep/)
|
2026-05-30 15:46:51 +08:00
|
|
|
|
assert.match(flowScript, /function resolveDurationFromFields/)
|
|
|
|
|
|
assert.match(flowScript, /function resolveStartedTimestamp/)
|
|
|
|
|
|
assert.match(flowScript, /function resolveFinishedTimestamp/)
|
|
|
|
|
|
assert.match(flowScript, /syntheticTiming/)
|
|
|
|
|
|
assert.match(flowScript, /refreshCompleted/)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-06-13 14:52:26 +00:00
|
|
|
|
test('steward application missing transport blocks preview table', () => {
|
2026-06-06 17:19:07 +08:00
|
|
|
|
assert.match(submitComposerScript, /function shouldPauseStewardApplicationPreview/)
|
|
|
|
|
|
assert.match(submitComposerScript, /function sanitizeStewardDelegatedTaskSummary/)
|
|
|
|
|
|
assert.match(submitComposerScript, /交通方式和\(\?:预算\|预计\)\?金额待补充/)
|
|
|
|
|
|
assert.match(submitComposerScript, /出差费用预算/)
|
|
|
|
|
|
assert.match(submitComposerScript, /预估\|预计\|预算\)\?费用/)
|
|
|
|
|
|
assert.match(submitComposerScript, /applicationPreview:\s*pauseForMissingFields \? null : applicationPreview/)
|
2026-06-13 14:52:26 +00:00
|
|
|
|
assert.match(submitComposerScript, /我已经识别出这一步要先处理申请单,但现在还不能生成可提交的申请核对表/)
|
2026-06-06 17:19:07 +08:00
|
|
|
|
assert.match(submitComposerScript, /applicationPreview:\s*normalized/)
|
2026-06-24 10:42:50 +08:00
|
|
|
|
assert.doesNotMatch(submitComposerScript, /请先告诉我您打算怎么出行:\*\*火车、飞机或轮船\*\*/)
|
2026-06-13 14:52:26 +00:00
|
|
|
|
|
|
|
|
|
|
assert.match(suggestedActionsScript, /payload\.applicationPreview/)
|
|
|
|
|
|
assert.match(suggestedActionsScript, /function continueStewardApplicationFieldCompletion/)
|
|
|
|
|
|
assert.match(suggestedActionsScript, /submitComposerInternal\(\{[\s\S]*stewardContinuation: continuation/)
|
|
|
|
|
|
assert.match(suggestedActionsScript, /skipUserMessage:\s*true/)
|
|
|
|
|
|
assert.match(suggestedActionsScript, /targetMessage\.applicationPreview = normalizeApplicationPreview\(sourcePreview\)/)
|
|
|
|
|
|
assert.match(suggestedActionsScript, /openApplicationPreviewEditor\(targetMessage, fieldKey/)
|
|
|
|
|
|
assert.match(suggestedActionsScript, /commitApplicationPreviewEditor\(targetMessage\)/)
|
2026-06-06 17:19:07 +08:00
|
|
|
|
assert.match(stewardFieldCompletionScript, /transportMode:\s*'transport_mode'/)
|
2026-06-13 14:52:26 +00:00
|
|
|
|
assert.match(stewardFieldCompletionScript, /基础规则交通费用预估表/)
|
2026-06-06 17:19:07 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
test('steward field completion reruns application preview instead of directly rendering table', () => {
|
|
|
|
|
|
const continuation = {
|
|
|
|
|
|
planId: 'steward-plan-transport-gap',
|
|
|
|
|
|
currentTaskId: 'task-application-beijing',
|
|
|
|
|
|
currentTask: {
|
|
|
|
|
|
task_id: 'task-application-beijing',
|
|
|
|
|
|
task_type: 'expense_application',
|
|
|
|
|
|
summary: '明天前往北京出差3天,支撑国网仿生产部署',
|
|
|
|
|
|
ontology_fields: {
|
|
|
|
|
|
time_range: '2026-06-05 至 2026-06-07',
|
|
|
|
|
|
location: '北京',
|
|
|
|
|
|
reason: '支撑国网仿生产部署'
|
|
|
|
|
|
},
|
|
|
|
|
|
missing_fields: ['transport_mode']
|
|
|
|
|
|
},
|
|
|
|
|
|
remainingTasks: []
|
|
|
|
|
|
}
|
|
|
|
|
|
const preview = normalizeApplicationPreview({
|
|
|
|
|
|
fields: {
|
|
|
|
|
|
applicationType: '差旅费用申请',
|
|
|
|
|
|
time: '2026-06-05 至 2026-06-07',
|
|
|
|
|
|
location: '北京',
|
|
|
|
|
|
reason: '支撑国网仿生产部署',
|
|
|
|
|
|
days: '3天',
|
|
|
|
|
|
transportMode: ''
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const nextContinuation = buildStewardFieldCompletionContinuation(continuation, 'transportMode', '火车')
|
|
|
|
|
|
assert.equal(nextContinuation.currentTask.ontology_fields.transport_mode, '火车')
|
|
|
|
|
|
assert.deepEqual(nextContinuation.currentTask.missing_fields, [])
|
|
|
|
|
|
|
|
|
|
|
|
const carryText = buildStewardFieldCompletionRawText({
|
|
|
|
|
|
preview,
|
|
|
|
|
|
fieldKey: 'transportMode',
|
|
|
|
|
|
fieldLabel: '出行方式',
|
|
|
|
|
|
value: '火车',
|
|
|
|
|
|
continuation: nextContinuation
|
|
|
|
|
|
})
|
|
|
|
|
|
assert.match(carryText, /用户已补充:出行方式:火车/)
|
|
|
|
|
|
assert.match(carryText, /地点:北京/)
|
|
|
|
|
|
assert.match(carryText, /天数:3天/)
|
2026-06-13 14:52:26 +00:00
|
|
|
|
assert.match(carryText, /请先根据已补齐字段按基础规则交通费用预估表/)
|
2026-06-06 17:19:07 +08:00
|
|
|
|
|
|
|
|
|
|
const rebuiltPreview = buildLocalApplicationPreview(carryText, { name: '曹笑竹', grade: 'P5' })
|
|
|
|
|
|
assert.equal(rebuiltPreview.fields.location, '北京')
|
|
|
|
|
|
assert.equal(rebuiltPreview.fields.transportMode, '火车')
|
|
|
|
|
|
assert.equal(rebuiltPreview.fields.days, '3天')
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
test('budget compile report does not steal steward delegated application rerun', () => {
|
|
|
|
|
|
const staleBudgetContext = {
|
|
|
|
|
|
budgetNo: 'BUD-2026-TECH',
|
|
|
|
|
|
mode: 'edit',
|
|
|
|
|
|
categoryRows: []
|
|
|
|
|
|
}
|
|
|
|
|
|
const stewardApplicationText = [
|
|
|
|
|
|
'小财管家继续执行申请单字段补齐。',
|
|
|
|
|
|
'用户已补充:出行方式:火车。',
|
|
|
|
|
|
'地点:北京',
|
|
|
|
|
|
'天数:3天',
|
2026-06-13 14:52:26 +00:00
|
|
|
|
'处理要求:请先根据已补齐字段按基础规则交通费用预估表测算费用口径,完成系统预估金额测算,再生成申请单核对表。'
|
2026-06-06 17:19:07 +08:00
|
|
|
|
].join('\n')
|
|
|
|
|
|
|
|
|
|
|
|
assert.equal(shouldUseBudgetCompileReport(stewardApplicationText, {
|
|
|
|
|
|
sessionType: 'application',
|
|
|
|
|
|
entrySource: 'workbench',
|
|
|
|
|
|
budgetContext: staleBudgetContext
|
|
|
|
|
|
}), false)
|
|
|
|
|
|
assert.equal(shouldUseBudgetCompileReport('帮我生成 2026 年 Q3 预算编制建议', {
|
|
|
|
|
|
sessionType: 'budget',
|
|
|
|
|
|
entrySource: 'budget',
|
|
|
|
|
|
budgetContext: staleBudgetContext
|
|
|
|
|
|
}), true)
|
|
|
|
|
|
assert.match(submitComposerScript, /if \(!stewardDelegated && shouldUseBudgetCompileReport/)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
test('text confirmation submits pending application preview before replanning steward task', () => {
|
|
|
|
|
|
assert.match(stewardServiceScript, /fetchStewardRuntimeDecision/)
|
|
|
|
|
|
assert.match(stewardServiceScript, /\/steward\/runtime-decisions/)
|
2026-06-13 14:52:26 +00:00
|
|
|
|
assert.match(stewardRuntimeScript, /function buildStewardRuntimeState/)
|
|
|
|
|
|
assert.match(stewardRuntimeScript, /function buildStewardRuntimeFastPathDecision/)
|
|
|
|
|
|
assert.match(stewardRuntimeScript, /function shouldUseStewardRuntimeLlmDecision/)
|
|
|
|
|
|
assert.match(stewardRuntimeScript, /function findPendingSlotSuggestedActionContextByInput/)
|
|
|
|
|
|
assert.match(stewardRuntimeTextModelScript, /function shouldPlanNewStewardTasksLocally/)
|
|
|
|
|
|
assert.match(stewardRuntimeTextModelScript, /function resolveStewardRuntimeTransportAlias/)
|
|
|
|
|
|
assert.match(stewardRuntimeScript, /const actionTransportAlias = resolveStewardRuntimeTransportAlias/)
|
|
|
|
|
|
assert.match(stewardRuntimeScript, /actionTransportAlias === transportAlias/)
|
|
|
|
|
|
assert.match(stewardRuntimeScript, /next_action:\s*'continue_next_task'/)
|
|
|
|
|
|
assert.match(stewardRuntimeScript, /next_action:\s*'submit_current_application'/)
|
|
|
|
|
|
assert.match(stewardRuntimeScript, /next_action:\s*'fill_current_slot'/)
|
|
|
|
|
|
assert.match(stewardRuntimeScript, /next_action:\s*'plan_new_tasks'/)
|
|
|
|
|
|
assert.match(stewardRuntimeScript, /suppressUserEcho:\s*userMessageAlreadyAdded/)
|
|
|
|
|
|
assert.match(suggestedActionsScript, /if \(!action\?\.suppressUserEcho\) \{[\s\S]*messages\.value\.push\(createMessage\('user', userText\)\)/)
|
|
|
|
|
|
assert.match(suggestedActionsScript, /skipApplicationModelReview:\s*true/)
|
|
|
|
|
|
assert.match(suggestedActionsScript, /skipApplicationModelReview:\s*targetSessionType === SESSION_TYPE_APPLICATION/)
|
|
|
|
|
|
assert.match(suggestedActionsScript, /skipStewardSlotDecision:\s*targetSessionType === SESSION_TYPE_APPLICATION/)
|
2026-06-06 17:19:07 +08:00
|
|
|
|
assert.match(submitComposerScript, /skipModelReview:\s*Boolean\(stewardDelegated && options\.skipApplicationModelReview\)/)
|
2026-06-13 14:52:26 +00:00
|
|
|
|
assert.match(submitComposerScript, /const requireModelReview = shouldRequireApplicationModelReview\(rawText\)/)
|
|
|
|
|
|
assert.match(submitComposerScript, /if \(options\.skipModelReview && !requireModelReview\) \{[\s\S]*结构化快路径/)
|
2026-06-06 17:19:07 +08:00
|
|
|
|
assert.match(submitComposerScript, /const localPauseForMissingFields = shouldPauseStewardApplicationPreview\(applicationPreview\)/)
|
|
|
|
|
|
assert.match(submitComposerScript, /const shouldFetchSlotDecision = localPauseForMissingFields && !options\.skipStewardSlotDecision/)
|
|
|
|
|
|
assert.match(submitComposerScript, /const slotDecision = shouldFetchSlotDecision[\s\S]*fetchStewardApplicationSlotDecision/)
|
|
|
|
|
|
assert.match(submitComposerScript, /const pendingSuggestedActions = Array\.isArray\(finalExtras\.suggestedActions\)/)
|
|
|
|
|
|
assert.match(submitComposerScript, /message\.suggestedActions = pendingSuggestedActions[\s\S]*message\.stewardPlan = buildStewardDelegatedPlan\(continuation, \[\.\.\.typedEvents\], 'typing'\)/)
|
2026-06-13 14:52:26 +00:00
|
|
|
|
assert.match(stewardRuntimeScript, /async function handleStewardRuntimeDecision/)
|
|
|
|
|
|
assert.match(stewardRuntimeScript, /const runtimeState = buildStewardRuntimeState\(\)/)
|
|
|
|
|
|
assert.match(stewardRuntimeScript, /if \(!hasActiveStewardRuntimeDecisionContext\(runtimeState\)\) \{[\s\S]*return false/)
|
|
|
|
|
|
assert.match(stewardRuntimeScript, /function pushStewardRuntimeUserMessage\(userText = ''\)/)
|
|
|
|
|
|
assert.match(stewardRuntimeScript, /const userMessageAlreadyAdded = options\.skipUserMessage[\s\S]*pushStewardRuntimeUserMessage\(rawText\)/)
|
|
|
|
|
|
assert.match(stewardRuntimeScript, /const fastDecision = buildStewardRuntimeFastPathDecision\(rawText, runtimeState\)[\s\S]*submitStewardPlan\(\{[\s\S]*skipUserMessage: userMessageAlreadyAdded \|\| options\.skipUserMessage/)
|
|
|
|
|
|
assert.match(stewardRuntimeScript, /executeStewardRuntimeDecision\(fastDecision, rawText, \{ userMessageAlreadyAdded \}\)[\s\S]*if \(!shouldUseStewardRuntimeLlmDecision\(rawText, runtimeState\)\)/)
|
|
|
|
|
|
assert.match(stewardRuntimeScript, /if \(!shouldUseStewardRuntimeLlmDecision\(rawText, runtimeState\)\)[\s\S]*fetchStewardRuntimeDecision/)
|
|
|
|
|
|
assert.match(stewardRuntimeScript, /executeStewardRuntimeDecision\(decision, rawText, \{ userMessageAlreadyAdded \}\)/)
|
|
|
|
|
|
assert.match(stewardRuntimeScript, /skipUserMessage: userMessageAlreadyAdded \|\| options\.skipUserMessage/)
|
|
|
|
|
|
assert.match(stewardRuntimeScript, /fetchStewardRuntimeDecision\(\{[\s\S]*runtime_state: runtimeState/)
|
2026-06-06 17:19:07 +08:00
|
|
|
|
assert.match(createViewScript, /if \(await handleStewardRuntimeDecision\(options\)\) \{[\s\S]*return null/)
|
2026-06-13 14:52:26 +00:00
|
|
|
|
assert.match(stewardRuntimeTextModelScript, /function isApplicationSubmitConfirmationText/)
|
|
|
|
|
|
assert.match(stewardRuntimeTextModelScript, /APPLICATION_SUBMIT_CONFIRM_TEXT_PATTERN[\s\S]*确认提交[\s\S]*提交审批/)
|
|
|
|
|
|
assert.match(stewardRuntimeScript, /function findPendingApplicationSubmitMessage/)
|
|
|
|
|
|
assert.match(stewardRuntimeScript, /normalizedPreview\.readyToSubmit/)
|
|
|
|
|
|
assert.match(stewardRuntimeScript, /async function handleApplicationSubmitConfirmationText/)
|
|
|
|
|
|
assert.match(stewardRuntimeScript, /await confirmApplicationSubmit\(\{ userText: rawText \}\)/)
|
2026-06-06 17:19:07 +08:00
|
|
|
|
assert.match(createViewScript, /if \(await handleApplicationSubmitConfirmationText\(options\)\) \{[\s\S]*return null[\s\S]*\}[\s\S]*if \(isStewardSession\.value && !options\.skipStewardPlan/)
|
2026-06-13 14:52:26 +00:00
|
|
|
|
assert.match(stewardRuntimeScript, /message\.applicationSubmitConfirmed = true/)
|
|
|
|
|
|
assert.match(stewardRuntimeScript, /message\.applicationSubmitConfirmed[\s\S]*continue/)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
test('application submit result does not render reimbursement review followup', () => {
|
|
|
|
|
|
assert.match(submitComposerScript, /function shouldExposeReviewPayloadForMessage\(payload, options = \{\}\)/)
|
|
|
|
|
|
assert.match(submitComposerScript, /options\.isApplicationSubmitOperation \|\| isApplicationDraftPayload\(result\.draft_payload\)/)
|
|
|
|
|
|
assert.match(submitComposerScript, /function buildPresentationPayload\(payload, \{ exposeReviewPayload = true \} = \{\}\)/)
|
|
|
|
|
|
assert.match(submitComposerScript, /review_payload:\s*null/)
|
|
|
|
|
|
assert.match(submitComposerScript, /const exposeReviewPayload = shouldExposeReviewPayloadForMessage\(payload, \{ isApplicationSubmitOperation \}\)/)
|
|
|
|
|
|
assert.match(submitComposerScript, /const presentationPayload = buildPresentationPayload\(payload, \{ exposeReviewPayload \}\)/)
|
|
|
|
|
|
assert.match(submitComposerScript, /const resultReviewPayload = presentationResult\.review_payload \|\| null/)
|
|
|
|
|
|
assert.match(submitComposerScript, /suggestedActions:\s*resultSuggestedActions/)
|
|
|
|
|
|
assert.match(submitComposerScript, /reviewPayload:\s*resultReviewPayload/)
|
|
|
|
|
|
assert.match(submitComposerScript, /buildAgentInsight\(\s*presentationPayload,/)
|
2026-06-06 17:19:07 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
test('steward streaming uses chunked typewriter to reduce perceived latency', () => {
|
|
|
|
|
|
assert.match(stewardPlanFlowScript, /STEWARD_THINKING_TYPEWRITER_CHUNK_SIZE = 5/)
|
2026-06-22 11:58:53 +08:00
|
|
|
|
assert.match(stewardPlanFlowScript, /resolveStewardTypewriterNextIndex\(chars, index, STEWARD_THINKING_TYPEWRITER_CHUNK_SIZE\)/)
|
2026-06-06 17:19:07 +08:00
|
|
|
|
assert.match(submitComposerScript, /STEWARD_DELEGATED_THINKING_CHUNK_SIZE = 5/)
|
2026-06-22 11:58:53 +08:00
|
|
|
|
assert.match(submitComposerScript, /resolveStewardTypewriterNextIndex\(chars, index, STEWARD_DELEGATED_THINKING_CHUNK_SIZE\)/)
|
2026-06-13 14:52:26 +00:00
|
|
|
|
assert.match(stewardFollowupFlowScript, /STEWARD_FOLLOWUP_THINKING_CHUNK_SIZE = 5/)
|
2026-06-22 11:58:53 +08:00
|
|
|
|
assert.match(stewardFollowupFlowScript, /resolveStewardTypewriterNextIndex\(chars, index, STEWARD_FOLLOWUP_THINKING_CHUNK_SIZE\)/)
|
2026-06-13 14:52:26 +00:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
test('steward typewriter renders markdown table blocks at once', () => {
|
|
|
|
|
|
const tableText = '这是费用申请核对结果:\n| 字段 | 值 |\n| --- | --- |\n| 地点 | 上海 |\n下一段'
|
|
|
|
|
|
const tableChars = Array.from(tableText)
|
|
|
|
|
|
const tableIndex = tableText.indexOf('| 字段')
|
|
|
|
|
|
const nextParagraphIndex = tableText.indexOf('下一段')
|
|
|
|
|
|
const normalIndex = 0
|
|
|
|
|
|
|
|
|
|
|
|
assert.equal(resolveStewardTypewriterNextIndex(tableChars, normalIndex), 3)
|
|
|
|
|
|
assert.equal(resolveStewardTypewriterNextIndex(tableChars, tableIndex), nextParagraphIndex)
|
|
|
|
|
|
assert.equal(resolveStewardTypewriterNextIndex(tableChars, tableIndex - 1), nextParagraphIndex)
|
|
|
|
|
|
assert.equal(resolveStewardTypewriterNextIndex(Array.from('### 核对结果'), 0), 2)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
test('application preview table appears as a whole card instead of row-by-row animation', () => {
|
|
|
|
|
|
assert.doesNotMatch(
|
|
|
|
|
|
messageItemStyles,
|
|
|
|
|
|
/structured-card-reveal-enter-active\s+\.application-preview-row\s*\{[\s\S]*animation:/,
|
|
|
|
|
|
)
|
|
|
|
|
|
assert.doesNotMatch(
|
|
|
|
|
|
messageItemStyles,
|
|
|
|
|
|
/application-preview-row:nth-child\([^)]*\)\s*\{[\s\S]*animation-delay:/,
|
|
|
|
|
|
)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
test('complex travel application sentences require model review', () => {
|
|
|
|
|
|
assert.equal(
|
|
|
|
|
|
shouldRequireApplicationModelReview('申请2月20日-23日火车去上海出差,服务国网仿生产服务器部署'),
|
|
|
|
|
|
true
|
|
|
|
|
|
)
|
|
|
|
|
|
assert.equal(shouldRequireApplicationModelReview('我想发起一笔费用申请'), false)
|
2026-06-06 17:19:07 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
test('steward initial workbench entry shows recognition state before messages arrive', () => {
|
|
|
|
|
|
assert.match(createViewScript, /const hasStewardInitialAutoSubmitPayload = computed/)
|
|
|
|
|
|
assert.match(createViewScript, /const showStewardInitialRecognition = computed/)
|
|
|
|
|
|
assert.match(createViewScript, /!messages\.value\.length/)
|
|
|
|
|
|
assert.match(createViewScript, /workbenchVisible\.value \|\| submitting\.value/)
|
|
|
|
|
|
assert.match(createViewScript, /showStewardInitialRecognition/)
|
|
|
|
|
|
assert.match(createViewTemplate, /v-if="showStewardInitialRecognition"/)
|
|
|
|
|
|
assert.match(createViewTemplate, /class="steward-initial-recognition"/)
|
|
|
|
|
|
assert.match(createViewTemplate, /小财管家正在识别意图/)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
test('steward application carry text does not leak transport examples into extraction', () => {
|
|
|
|
|
|
const actions = buildStewardSuggestedActions({
|
|
|
|
|
|
plan_id: 'steward-plan-transport-gap',
|
|
|
|
|
|
plan_status: 'ready',
|
|
|
|
|
|
tasks: [
|
|
|
|
|
|
{
|
|
|
|
|
|
task_id: 'task-application-beijing',
|
|
|
|
|
|
task_type: 'expense_application',
|
|
|
|
|
|
title: '北京出差申请',
|
|
|
|
|
|
summary: '明天前往北京出差3天,支撑国网仿生产部署',
|
|
|
|
|
|
assigned_agent: 'application_assistant',
|
|
|
|
|
|
ontology_fields: {
|
|
|
|
|
|
expense_type: 'travel',
|
|
|
|
|
|
time_range: '2026-06-05 至 2026-06-07',
|
|
|
|
|
|
location: '北京',
|
|
|
|
|
|
reason: '支撑国网仿生产部署'
|
|
|
|
|
|
},
|
|
|
|
|
|
missing_fields: ['transport_mode', 'amount', 'attachments', 'employee_no']
|
|
|
|
|
|
}
|
|
|
|
|
|
],
|
|
|
|
|
|
confirmation_groups: [
|
|
|
|
|
|
{
|
|
|
|
|
|
action_type: 'confirm_create_application',
|
|
|
|
|
|
target_task_id: 'task-application-beijing'
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const carryText = actions[0]?.payload?.carry_text || ''
|
|
|
|
|
|
const currentTask = actions[0]?.payload?.steward_current_task || null
|
|
|
|
|
|
assert.match(carryText, /费用类型:差旅/)
|
|
|
|
|
|
assert.doesNotMatch(carryText, /费用类型:travel/)
|
|
|
|
|
|
assert.match(carryText, /还需要补充:出行方式/)
|
2026-06-13 14:52:26 +00:00
|
|
|
|
assert.doesNotMatch(carryText, /请先追问上述缺失信息/)
|
|
|
|
|
|
assert.doesNotMatch(carryText, /请直接生成申请单核对结果/)
|
|
|
|
|
|
assert.doesNotMatch(carryText, /入库或提交审批前/)
|
2026-06-06 17:19:07 +08:00
|
|
|
|
assert.doesNotMatch(carryText, /高铁|火车|飞机|轮船|自驾|出租车/)
|
|
|
|
|
|
assert.doesNotMatch(carryText, /预计金额|附件\/凭证|员工编号|金额/)
|
|
|
|
|
|
assert.equal(currentTask?.task_type, 'expense_application')
|
|
|
|
|
|
assert.deepEqual(currentTask?.missing_fields, ['transport_mode'])
|
|
|
|
|
|
assert.deepEqual(
|
|
|
|
|
|
filterStewardBlockingMissingFields(
|
|
|
|
|
|
['transport_type', 'amount', 'attachments', 'employee_no', 'department_name'],
|
|
|
|
|
|
'expense_application'
|
|
|
|
|
|
),
|
|
|
|
|
|
['transport_mode']
|
|
|
|
|
|
)
|
|
|
|
|
|
assert.deepEqual(
|
|
|
|
|
|
filterStewardBlockingMissingFields(['amount', 'attachments'], 'reimbursement'),
|
|
|
|
|
|
['amount', 'attachments']
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
const preview = buildLocalApplicationPreview(carryText, { name: '曹笑竹', grade: 'P5' })
|
|
|
|
|
|
assert.equal(preview.fields.transportMode, '')
|
|
|
|
|
|
assert.equal(preview.missingFields.includes('出行方式'), true)
|
|
|
|
|
|
|
|
|
|
|
|
assert.match(stewardServiceScript, /fetchStewardSlotDecision/)
|
|
|
|
|
|
assert.match(stewardServiceScript, /\/steward\/slot-decisions/)
|
|
|
|
|
|
assert.match(submitComposerScript, /fetchStewardApplicationSlotDecision/)
|
|
|
|
|
|
assert.match(submitComposerScript, /task_type:\s*'expense_application'/)
|
|
|
|
|
|
assert.match(submitComposerScript, /steward_continuation:\s*continuation/)
|
2026-06-13 14:52:26 +00:00
|
|
|
|
assert.match(suggestedActionsScript, /currentTask:\s*actionPayload\.steward_current_task/)
|
2026-06-06 17:19:07 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
test('steward application slot fallback ignores non-blocking application fields', () => {
|
|
|
|
|
|
assert.match(submitComposerScript, /APPLICATION_NON_BLOCKING_ONTOLOGY_FIELDS/)
|
|
|
|
|
|
assert.match(submitComposerScript, /'attachments'/)
|
|
|
|
|
|
assert.match(submitComposerScript, /'employee_no'/)
|
|
|
|
|
|
assert.match(submitComposerScript, /'amount'/)
|
|
|
|
|
|
assert.match(submitComposerScript, /function formatStewardDecisionUserText/)
|
|
|
|
|
|
assert.match(submitComposerScript, /formatStewardDecisionUserText\(decision\.question/)
|
|
|
|
|
|
assert.match(submitComposerScript, /formatStewardDecisionUserText\(decision\.rationale/)
|
|
|
|
|
|
assert.match(submitComposerScript, /normalizeTransportModeOption\(value \|\| label, ''\)/)
|
2026-06-13 14:52:26 +00:00
|
|
|
|
assert.match(suggestedActionsScript, /normalizeTransportModeOption\(value, ''\)/)
|
2026-06-06 17:19:07 +08:00
|
|
|
|
assert.equal(normalizeTransportModeOption('高铁', ''), '火车')
|
|
|
|
|
|
assert.equal(normalizeTransportModeOption('自驾', ''), '')
|
|
|
|
|
|
assert.match(submitComposerScript, /function resolveBlockingApplicationMissingFieldsForSteward/)
|
|
|
|
|
|
assert.match(submitComposerScript, /isBlockingApplicationOntologyField\(key\)/)
|
|
|
|
|
|
assert.match(submitComposerScript, /canonicalField && !isBlockingApplicationOntologyField\(canonicalField\)/)
|
|
|
|
|
|
assert.doesNotMatch(submitComposerScript, /附件\/凭证和员工编号为合规必需字段/)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-05-30 15:46:51 +08:00
|
|
|
|
test('flow panel durations use backend timing instead of local preview delay', () => {
|
|
|
|
|
|
const flow = createFlowHarness()
|
|
|
|
|
|
flow.resetFlowRun({ startedAt: Date.parse('2026-05-29T00:00:00.000Z') })
|
|
|
|
|
|
flow.startFlowStep('intent', '正在识别业务意图...')
|
|
|
|
|
|
flow.completeFlowStep('intent', '本地预览完成', 80)
|
|
|
|
|
|
flow.startFlowStep('extraction', '正在抽取关键信息...')
|
|
|
|
|
|
flow.completeFlowStep('extraction', '本地抽取完成', 90)
|
|
|
|
|
|
flow.startFlowStep('application-submit-success', {
|
|
|
|
|
|
title: '申请单提交成功',
|
|
|
|
|
|
tool: 'ApplicationSubmit',
|
|
|
|
|
|
detail: '正在提交申请单...'
|
|
|
|
|
|
})
|
|
|
|
|
|
flow.completeFlowStep('application-submit-success', '本地提交完成', 100)
|
|
|
|
|
|
|
|
|
|
|
|
flow.mergeFlowRunDetail({
|
|
|
|
|
|
started_at: '2026-05-29T00:00:00.000Z',
|
|
|
|
|
|
finished_at: '2026-05-29T00:00:05.000Z',
|
|
|
|
|
|
status: 'succeeded',
|
|
|
|
|
|
semantic_parse: {},
|
|
|
|
|
|
ontology_json: {},
|
|
|
|
|
|
tool_calls: [
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'submit-1',
|
|
|
|
|
|
run_id: 'run-1',
|
|
|
|
|
|
tool_type: 'application',
|
|
|
|
|
|
tool_name: 'application.submit',
|
|
|
|
|
|
request_json: {},
|
|
|
|
|
|
response_json: { status: 'submitted', draft_payload: { status: 'submitted' } },
|
|
|
|
|
|
status: 'succeeded',
|
|
|
|
|
|
duration_ms: 2360,
|
|
|
|
|
|
created_at: '2026-05-29T00:00:04.000Z'
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const durationByKey = Object.fromEntries(flow.flowSteps.value.map((step) => [step.key, step.durationMs]))
|
|
|
|
|
|
assert.equal(durationByKey.intent, 1400)
|
|
|
|
|
|
assert.equal(durationByKey.extraction, 2600)
|
|
|
|
|
|
assert.equal(durationByKey['application-submit-success'], 2360)
|
|
|
|
|
|
assert.equal(flow.flowTotalDurationText.value, '5.0s')
|
|
|
|
|
|
assert.equal(flow.formatFlowStepDuration({ status: 'completed', durationMs: 0 }), '--')
|
|
|
|
|
|
assert.equal(flow.formatFlowStepDuration({ status: 'completed', durationMs: null }), '--')
|
2026-05-27 10:32:08 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
2026-06-02 14:01:51 +08:00
|
|
|
|
test('application submit confirmation flow only shows submit success step', () => {
|
|
|
|
|
|
const flow = createFlowHarness()
|
|
|
|
|
|
flow.resetFlowRun({ startedAt: Date.parse('2026-05-29T00:00:00.000Z') })
|
|
|
|
|
|
flow.startFlowStep('application-submit-success', {
|
|
|
|
|
|
title: '申请单提交成功',
|
|
|
|
|
|
tool: 'ApplicationSubmit',
|
|
|
|
|
|
detail: '正在提交费用申请...'
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
flow.completeFlowResult({
|
|
|
|
|
|
status: 'succeeded',
|
|
|
|
|
|
result: {
|
|
|
|
|
|
answer: '申请单据已生成,并已进入审批流程。',
|
|
|
|
|
|
draft_payload: {
|
|
|
|
|
|
draft_type: 'expense_application',
|
|
|
|
|
|
status: 'submitted',
|
|
|
|
|
|
claim_no: 'AP-20260602010101-ABCDEFGH',
|
|
|
|
|
|
approval_stage: '直属领导审批'
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
assert.deepEqual(flow.flowSteps.value.map((step) => step.key), ['application-submit-success'])
|
|
|
|
|
|
assert.deepEqual(flow.visibleFlowSteps.value.map((step) => step.key), ['application-submit-success'])
|
|
|
|
|
|
const submitStep = flow.flowSteps.value[0]
|
|
|
|
|
|
assert.equal(submitStep.status, 'completed')
|
|
|
|
|
|
assert.match(submitStep.detail, /AP-20260602010101-ABCDEFGH/)
|
|
|
|
|
|
assert.doesNotMatch(flow.flowSteps.value.map((step) => step.key).join(','), /intent|extraction/)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
test('application duplicate confirmation flow marks submit step as blocked duplicate', () => {
|
|
|
|
|
|
const flow = createFlowHarness()
|
|
|
|
|
|
flow.resetFlowRun({ startedAt: Date.parse('2026-05-29T00:00:00.000Z') })
|
|
|
|
|
|
flow.startFlowStep('application-submit-success', {
|
|
|
|
|
|
title: '申请单提交成功',
|
|
|
|
|
|
tool: 'ApplicationSubmit',
|
|
|
|
|
|
detail: '正在提交费用申请...'
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
flow.completeFlowResult({
|
|
|
|
|
|
status: 'succeeded',
|
|
|
|
|
|
result: {
|
|
|
|
|
|
answer: [
|
2026-06-02 16:22:59 +08:00
|
|
|
|
'检测到同一申请人、同一申请类型、同一出发时间已存在申请单,系统没有重复创建。',
|
2026-06-02 14:01:51 +08:00
|
|
|
|
'已有申请单号:AP-20260602010101-ABCDEFGH',
|
|
|
|
|
|
'当前节点:直属领导审批'
|
|
|
|
|
|
].join('\n')
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
assert.deepEqual(flow.flowSteps.value.map((step) => step.key), ['application-submit-success'])
|
|
|
|
|
|
const submitStep = flow.flowSteps.value[0]
|
|
|
|
|
|
assert.equal(submitStep.status, 'completed')
|
|
|
|
|
|
assert.equal(submitStep.title, '重复申请已拦截')
|
|
|
|
|
|
assert.match(submitStep.detail, /AP-20260602010101-ABCDEFGH/)
|
|
|
|
|
|
assert.doesNotMatch(submitStep.detail, /提交成功/)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-05-27 10:32:08 +08:00
|
|
|
|
test('assistant markdown tables render with component-scoped table styling', () => {
|
|
|
|
|
|
const rendered = renderMarkdown([
|
|
|
|
|
|
'| 项目 | 标准口径 | 天数 | 小计 |',
|
|
|
|
|
|
'| --- | --- | ---: | ---: |',
|
|
|
|
|
|
'| 住宿费 | 武汉 / P5 标准:330.00 元/天 | 1 | 330.00 元 |',
|
|
|
|
|
|
'| 出差补贴 | 其他地区:伙食 55.00 元 + 基本 35.00 元 | 1 | 90.00 元 |'
|
|
|
|
|
|
].join('\n'))
|
|
|
|
|
|
|
|
|
|
|
|
assert.match(rendered, /<div class="markdown-table-wrap">/)
|
|
|
|
|
|
assert.match(rendered, /<table>/)
|
|
|
|
|
|
assert.match(rendered, /<th/)
|
|
|
|
|
|
assert.match(rendered, /<td/)
|
|
|
|
|
|
assert.match(messageItemStyles, /\.message-answer-markdown :deep\(\.markdown-table-wrap\) \{[\s\S]*overflow-x: auto;[\s\S]*border: 1px solid #dbe4ee;/)
|
2026-06-13 14:52:26 +00:00
|
|
|
|
assert.match(messageItemStyles, /\.message-answer-markdown :deep\(table\) \{[\s\S]*min-width: 560px;[\s\S]*table-layout: fixed;/)
|
|
|
|
|
|
assert.match(messageItemStyles, /\.message-answer-markdown :deep\(th\),[\s\S]*\.message-answer-markdown :deep\(td\) \{[\s\S]*padding: 8px 10px;[\s\S]*overflow-wrap: break-word;/)
|
|
|
|
|
|
assert.match(messageItemStyles, /\.message-answer-markdown :deep\(th:first-child\),[\s\S]*\.message-answer-markdown :deep\(td:first-child\) \{[\s\S]*width: 88px;[\s\S]*white-space: nowrap;[\s\S]*word-break: keep-all;/)
|
|
|
|
|
|
assert.match(messageItemStyles, /\.message-answer-markdown :deep\(th:last-child\),[\s\S]*\.message-answer-markdown :deep\(td:last-child\) \{[\s\S]*width: 112px;[\s\S]*text-align: right;[\s\S]*white-space: nowrap;[\s\S]*word-break: keep-all;/)
|
2026-05-26 09:15:14 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
2026-06-06 17:19:07 +08:00
|
|
|
|
test('assistant reimbursement recognition copy renders structured markdown sections', () => {
|
|
|
|
|
|
const rendered = renderMarkdown([
|
|
|
|
|
|
'识别到您希望报销一笔“业务招待费”费用:',
|
|
|
|
|
|
'',
|
|
|
|
|
|
'基础信息识别结果:',
|
|
|
|
|
|
'时间:2026-06-04',
|
|
|
|
|
|
'事由:小财管家继续执行剩余任务,请填写报销单:客户接待费用报销。',
|
|
|
|
|
|
'',
|
|
|
|
|
|
'报销测算参考:',
|
|
|
|
|
|
'先以用户填写金额或票据识别金额为基础,再结合费用类型、发生地点、业务事由和规则中心限额进行复核。'
|
|
|
|
|
|
].join('\n'))
|
|
|
|
|
|
|
|
|
|
|
|
assert.match(rendered, /<h3>基础信息识别结果<\/h3>/)
|
|
|
|
|
|
assert.match(rendered, /<li><strong>时间<\/strong>:2026-06-04<\/li>/)
|
|
|
|
|
|
assert.match(rendered, /<li><strong>事由<\/strong>:小财管家继续执行剩余任务/)
|
|
|
|
|
|
assert.match(rendered, /<h3>报销测算参考<\/h3>/)
|
|
|
|
|
|
assert.doesNotMatch(rendered, /基础信息识别结果:<\/h3>/)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
test('application date overlap blocks steward preview before duplicate application table', () => {
|
|
|
|
|
|
const existingRange = resolveApplicationDateRange('2026-06-05 至 2026-06-07')
|
|
|
|
|
|
const currentRange = resolveApplicationDateRange('2026-06-06 至 2026-06-08')
|
|
|
|
|
|
const disjointRange = resolveApplicationDateRange('2026-06-08 至 2026-06-10')
|
|
|
|
|
|
|
|
|
|
|
|
assert.equal(applicationDateRangesOverlap(currentRange, existingRange), true)
|
|
|
|
|
|
assert.equal(applicationDateRangesOverlap(disjointRange, existingRange), false)
|
|
|
|
|
|
assert.match(submitComposerScript, /function findOverlappingApplicationClaim\(applicationPreview, claimsPayload\)/)
|
|
|
|
|
|
assert.match(submitComposerScript, /function normalizeApplicationExpenseType\(value\)/)
|
|
|
|
|
|
assert.match(submitComposerScript, /currentExpenseType !== existingExpenseType/)
|
|
|
|
|
|
assert.match(submitComposerScript, /fetchExpenseClaims\(\{ page: 1, pageSize: 100 \}\)/)
|
|
|
|
|
|
assert.match(submitComposerScript, /buildApplicationDateConflictMessage\(applicationDateConflict\)/)
|
|
|
|
|
|
assert.match(submitComposerScript, /meta: \[STEWARD_ASSISTANT_NAME, '申请日期冲突'\]/)
|
|
|
|
|
|
assert.match(submitComposerScript, /applicationPreview: pauseForMissingFields \? null : applicationPreview/)
|
2026-06-13 14:52:26 +00:00
|
|
|
|
assert.match(suggestedActionsScript, /actionType === 'open_application_detail'/)
|
2026-06-06 17:19:07 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
2026-05-26 09:15:14 +08:00
|
|
|
|
test('application preview merges rule center travel estimate into highlighted rows', () => {
|
2026-06-13 14:52:26 +00:00
|
|
|
|
const preview = buildLocalApplicationPreview('申请 2026-05-25 至 2026-05-27 去上海出差3天,服务项目部署,火车,预计费用1800元', {
|
2026-05-26 09:15:14 +08:00
|
|
|
|
name: '李文静',
|
|
|
|
|
|
grade: 'P5'
|
|
|
|
|
|
})
|
|
|
|
|
|
const request = buildApplicationPolicyEstimateRequest(preview, { grade: 'P5' })
|
|
|
|
|
|
assert.equal(request.canCalculate, true)
|
2026-06-13 14:52:26 +00:00
|
|
|
|
assert.deepEqual(request.payload, {
|
|
|
|
|
|
days: 3,
|
|
|
|
|
|
location: '上海',
|
|
|
|
|
|
grade: 'P5',
|
|
|
|
|
|
transport_mode: '火车',
|
|
|
|
|
|
origin_location: null,
|
|
|
|
|
|
travel_date: '2026-05-25'
|
|
|
|
|
|
})
|
2026-05-26 09:15:14 +08:00
|
|
|
|
|
|
|
|
|
|
const estimatedPreview = applyApplicationPolicyEstimateResult(preview, {
|
|
|
|
|
|
days: 3,
|
|
|
|
|
|
location: '上海',
|
|
|
|
|
|
matched_city: '上海',
|
|
|
|
|
|
grade: 'P5',
|
|
|
|
|
|
hotel_rate: 600,
|
|
|
|
|
|
hotel_amount: 1800,
|
|
|
|
|
|
total_allowance_rate: 120,
|
|
|
|
|
|
allowance_amount: 360,
|
2026-06-13 14:52:26 +00:00
|
|
|
|
transport_mode: '火车',
|
|
|
|
|
|
transport_origin: '武汉',
|
|
|
|
|
|
transport_destination: '上海',
|
|
|
|
|
|
transport_estimated_amount: 720,
|
|
|
|
|
|
transport_estimate_basis: '武汉-上海火车往返二等座预估',
|
|
|
|
|
|
transport_estimate_source: 'basic_rule_transport_estimate',
|
|
|
|
|
|
transport_estimate_confidence: '基础规则',
|
|
|
|
|
|
total_amount: 2880,
|
2026-05-26 09:15:14 +08:00
|
|
|
|
rule_name: '公司差旅费报销规则',
|
|
|
|
|
|
rule_version: '2026版'
|
|
|
|
|
|
}, { grade: 'P5' })
|
|
|
|
|
|
|
|
|
|
|
|
assert.equal(estimatedPreview.fields.lodgingDailyCap, '600元/天')
|
|
|
|
|
|
assert.equal(estimatedPreview.fields.subsidyDailyCap, '120元/天')
|
2026-06-13 14:52:26 +00:00
|
|
|
|
assert.equal(estimatedPreview.fields.transportPolicy, '当前尚未接通实时票务价格查询 API,无法获取当前实际票价;先按《交通费用预估表》武汉-上海火车往返(二等座预估)暂估 720元用于申请阶段预算占用,最终报销以实际票据金额为准')
|
2026-06-02 16:22:59 +08:00
|
|
|
|
assert.doesNotMatch(estimatedPreview.fields.transportPolicy, /参考票价|查询耗时|2026-05-25|真实票据/)
|
2026-06-13 14:52:26 +00:00
|
|
|
|
assert.match(estimatedPreview.fields.policyEstimate, /交通 720元/)
|
|
|
|
|
|
assert.match(estimatedPreview.fields.policyEstimate, /2,880元/)
|
|
|
|
|
|
assert.equal(estimatedPreview.fields.transportEstimatedAmount, '720元')
|
|
|
|
|
|
assert.equal(estimatedPreview.fields.transportEstimateSource, 'basic_rule_transport_estimate')
|
|
|
|
|
|
assert.equal(estimatedPreview.fields.transportQueryLatencyMs, '')
|
|
|
|
|
|
assert.equal(estimatedPreview.fields.amount, '2,880元')
|
2026-05-26 09:15:14 +08:00
|
|
|
|
assert.equal(buildApplicationPreviewRows(estimatedPreview).find((row) => row.key === 'policyEstimate')?.highlight, true)
|
|
|
|
|
|
})
|
2026-06-01 17:07:14 +08:00
|
|
|
|
|
2026-06-20 10:17:37 +08:00
|
|
|
|
test('application preview calculates base policy estimate when transport mode is missing', () => {
|
2026-06-13 14:52:26 +00:00
|
|
|
|
const currentUser = { name: '李文静', grade: 'P5', location: '武汉' }
|
|
|
|
|
|
const preview = buildLocalApplicationPreview(
|
|
|
|
|
|
'我要申请2月20日-23日去上海出差,辅助国网仿生产项目部署',
|
|
|
|
|
|
currentUser,
|
|
|
|
|
|
{ today: '2026-06-09' }
|
|
|
|
|
|
)
|
|
|
|
|
|
const request = buildApplicationPolicyEstimateRequest(preview, currentUser)
|
2026-06-20 10:17:37 +08:00
|
|
|
|
assert.equal(request.canCalculate, true)
|
|
|
|
|
|
assert.deepEqual(request.payload, {
|
|
|
|
|
|
days: 4,
|
|
|
|
|
|
location: '上海',
|
|
|
|
|
|
grade: 'P5',
|
|
|
|
|
|
transport_mode: null,
|
|
|
|
|
|
origin_location: '武汉',
|
|
|
|
|
|
travel_date: '2026-02-20'
|
|
|
|
|
|
})
|
2026-06-13 14:52:26 +00:00
|
|
|
|
assert.equal(preview.missingFields.includes('出行方式'), true)
|
|
|
|
|
|
assert.equal(preview.readyToSubmit, false)
|
|
|
|
|
|
|
|
|
|
|
|
const staleEstimateResult = {
|
|
|
|
|
|
days: 4,
|
|
|
|
|
|
location: '上海',
|
|
|
|
|
|
matched_city: '上海',
|
|
|
|
|
|
grade: 'P5',
|
|
|
|
|
|
hotel_rate: 250,
|
|
|
|
|
|
hotel_amount: 1000,
|
|
|
|
|
|
total_allowance_rate: 100,
|
|
|
|
|
|
allowance_amount: 400,
|
|
|
|
|
|
transport_mode: '火车',
|
|
|
|
|
|
transport_origin: '武汉',
|
|
|
|
|
|
transport_destination: '上海',
|
|
|
|
|
|
transport_estimated_amount: 720,
|
|
|
|
|
|
transport_estimate_basis: '武汉-上海火车往返二等座预估',
|
|
|
|
|
|
transport_estimate_source: 'basic_rule_transport_estimate',
|
|
|
|
|
|
transport_estimate_confidence: '基础规则',
|
|
|
|
|
|
total_amount: 2120,
|
|
|
|
|
|
travel_date: '2026-02-20',
|
|
|
|
|
|
rule_name: '差旅住宿报销标准',
|
|
|
|
|
|
rule_version: 'v1.0.0'
|
|
|
|
|
|
}
|
|
|
|
|
|
const blockedEstimatePreview = applyApplicationPolicyEstimateResult(preview, {
|
|
|
|
|
|
...staleEstimateResult,
|
|
|
|
|
|
transport_mode: ''
|
|
|
|
|
|
}, currentUser)
|
|
|
|
|
|
const staleEstimatePreview = applyApplicationPolicyEstimateResult(preview, staleEstimateResult, currentUser)
|
|
|
|
|
|
|
|
|
|
|
|
assert.equal(blockedEstimatePreview.fields.transportMode, '')
|
|
|
|
|
|
assert.equal(blockedEstimatePreview.fields.transportEstimatedAmount, '')
|
2026-06-20 10:17:37 +08:00
|
|
|
|
assert.equal(blockedEstimatePreview.fields.lodgingDailyCap, '250元/天')
|
|
|
|
|
|
assert.equal(blockedEstimatePreview.fields.subsidyDailyCap, '100元/天')
|
|
|
|
|
|
assert.equal(blockedEstimatePreview.fields.policyEstimate, '交通待补充 + 住宿 1,000元 + 补贴 400元 = 1,400元(4天,不含交通)')
|
|
|
|
|
|
assert.equal(blockedEstimatePreview.fields.amount, '1,400元(不含交通)')
|
2026-06-13 14:52:26 +00:00
|
|
|
|
assert.equal(blockedEstimatePreview.missingFields.includes('出行方式'), true)
|
|
|
|
|
|
assert.equal(staleEstimatePreview.fields.reason, '辅助国网仿生产项目部署')
|
2026-06-20 10:17:37 +08:00
|
|
|
|
assert.equal(staleEstimatePreview.fields.transportMode, '')
|
|
|
|
|
|
assert.equal(staleEstimatePreview.missingFields.includes('出行方式'), true)
|
|
|
|
|
|
assert.equal(staleEstimatePreview.fields.transportPolicy, '选择火车、飞机或轮船后自动预估交通费用')
|
|
|
|
|
|
assert.equal(staleEstimatePreview.fields.policyEstimate, '交通待补充 + 住宿 1,000元 + 补贴 400元 = 1,400元(4天,不含交通)')
|
|
|
|
|
|
assert.equal(staleEstimatePreview.fields.amount, '1,400元(不含交通)')
|
2026-06-13 14:52:26 +00:00
|
|
|
|
})
|
|
|
|
|
|
|
2026-06-22 15:56:06 +08:00
|
|
|
|
test('application preview estimate infers days from completed date range', () => {
|
|
|
|
|
|
const currentUser = { name: '\u674e\u6587\u9759', grade: 'P5', location: '\u6b66\u6c49' }
|
|
|
|
|
|
const preview = normalizeApplicationPreview({
|
|
|
|
|
|
fields: {
|
|
|
|
|
|
applicationType: '\u5dee\u65c5\u8d39\u7528\u7533\u8bf7',
|
|
|
|
|
|
time: '2026-06-23 \u81f3 2026-06-25',
|
|
|
|
|
|
location: '\u5317\u4eac',
|
|
|
|
|
|
reason: '\u652f\u6491\u5ba2\u6237\u73b0\u573a\u5b9e\u65bd',
|
|
|
|
|
|
days: '',
|
|
|
|
|
|
transportMode: '',
|
|
|
|
|
|
grade: 'P5'
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
const request = buildApplicationPolicyEstimateRequest(preview, currentUser)
|
|
|
|
|
|
|
|
|
|
|
|
assert.equal(request.canCalculate, true)
|
|
|
|
|
|
assert.deepEqual(request.payload, {
|
|
|
|
|
|
days: 3,
|
|
|
|
|
|
location: '\u5317\u4eac',
|
|
|
|
|
|
grade: 'P5',
|
|
|
|
|
|
transport_mode: null,
|
|
|
|
|
|
origin_location: '\u6b66\u6c49',
|
|
|
|
|
|
travel_date: '2026-06-23'
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const estimatedPreview = applyApplicationPolicyEstimateResult(preview, {
|
|
|
|
|
|
days: 3,
|
|
|
|
|
|
location: '\u5317\u4eac',
|
|
|
|
|
|
matched_city: '\u5317\u4eac',
|
|
|
|
|
|
grade: 'P5',
|
|
|
|
|
|
hotel_rate: 450,
|
|
|
|
|
|
hotel_amount: 1350,
|
|
|
|
|
|
total_allowance_rate: 100,
|
|
|
|
|
|
allowance_amount: 300,
|
|
|
|
|
|
total_amount: 1650,
|
|
|
|
|
|
rule_name: '\u516c\u53f8\u5dee\u65c5\u8d39\u62a5\u9500\u89c4\u5219',
|
|
|
|
|
|
rule_version: 'v1.0.0'
|
|
|
|
|
|
}, currentUser)
|
|
|
|
|
|
|
|
|
|
|
|
assert.equal(estimatedPreview.fields.days, '3\u5929')
|
|
|
|
|
|
assert.equal(estimatedPreview.fields.lodgingDailyCap, '450\u5143/\u5929')
|
|
|
|
|
|
assert.equal(estimatedPreview.fields.subsidyDailyCap, '100\u5143/\u5929')
|
|
|
|
|
|
assert.equal(estimatedPreview.fields.amount, '1,650\u5143\uff08\u4e0d\u542b\u4ea4\u901a\uff09')
|
|
|
|
|
|
assert.match(estimatedPreview.fields.policyEstimate, /\u4ea4\u901a\u5f85\u8865\u5145/)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-06-01 17:07:14 +08:00
|
|
|
|
test('application preview editor refreshes transport estimate after mode change', async () => {
|
|
|
|
|
|
const preview = applyApplicationPolicyEstimateResult(
|
|
|
|
|
|
buildLocalApplicationPreview('申请 2026-05-25 至 2026-05-27 去上海出差3天,服务项目部署', {
|
|
|
|
|
|
name: '李文静',
|
|
|
|
|
|
grade: 'P5'
|
|
|
|
|
|
}),
|
|
|
|
|
|
{
|
|
|
|
|
|
days: 3,
|
|
|
|
|
|
location: '上海',
|
|
|
|
|
|
matched_city: '上海',
|
|
|
|
|
|
grade: 'P5',
|
|
|
|
|
|
hotel_rate: 600,
|
|
|
|
|
|
hotel_amount: 1800,
|
|
|
|
|
|
total_allowance_rate: 120,
|
|
|
|
|
|
allowance_amount: 360,
|
|
|
|
|
|
total_amount: 2160
|
|
|
|
|
|
},
|
|
|
|
|
|
{ grade: 'P5' }
|
|
|
|
|
|
)
|
|
|
|
|
|
const message = {
|
|
|
|
|
|
id: 'application-preview-editor-message',
|
|
|
|
|
|
applicationPreview: preview,
|
|
|
|
|
|
text: ''
|
|
|
|
|
|
}
|
|
|
|
|
|
let persistCount = 0
|
|
|
|
|
|
const toastMessages = []
|
|
|
|
|
|
const editor = useApplicationPreviewEditor({
|
|
|
|
|
|
persistSessionState: () => {
|
|
|
|
|
|
persistCount += 1
|
|
|
|
|
|
},
|
|
|
|
|
|
toast: (messageText) => {
|
|
|
|
|
|
toastMessages.push(messageText)
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
editor.openApplicationPreviewEditor(message, 'transportMode', '待补充')
|
|
|
|
|
|
editor.applicationPreviewEditor.value.draftValue = '飞机'
|
|
|
|
|
|
const committed = await editor.commitApplicationPreviewEditor(message)
|
|
|
|
|
|
|
|
|
|
|
|
assert.equal(committed, true)
|
|
|
|
|
|
assert.equal(message.applicationPreview.fields.transportMode, '飞机')
|
2026-06-13 14:52:26 +00:00
|
|
|
|
assert.equal(message.applicationPreview.fields.transportEstimatedAmount, '1,380元')
|
|
|
|
|
|
assert.equal(message.applicationPreview.fields.amount, '3,540元')
|
|
|
|
|
|
assert.equal(message.applicationPreview.fields.transportPolicy, '预估交通费用 1,380元')
|
2026-06-02 16:22:59 +08:00
|
|
|
|
assert.doesNotMatch(message.applicationPreview.fields.transportPolicy, /参考票价|查询耗时|2026-05-25|真实票据/)
|
2026-06-01 17:07:14 +08:00
|
|
|
|
assert.doesNotMatch(message.applicationPreview.fields.transportPolicy, /模拟/)
|
|
|
|
|
|
assert.ok(persistCount >= 2)
|
|
|
|
|
|
assert.equal(toastMessages.at(-1), '已更新出行方式和费用测算。')
|
|
|
|
|
|
})
|
2026-06-03 09:25:23 +08:00
|
|
|
|
|
|
|
|
|
|
test('application preview editor recalculates days and subsidy after date range change', async () => {
|
|
|
|
|
|
const preview = normalizeApplicationPreview({
|
|
|
|
|
|
fields: {
|
|
|
|
|
|
applicationType: '\u5dee\u65c5\u8d39\u7528\u7533\u8bf7',
|
|
|
|
|
|
time: '2026-05-25',
|
|
|
|
|
|
location: '\u4e0a\u6d77',
|
|
|
|
|
|
reason: '\u670d\u52a1\u9879\u76ee\u90e8\u7f72',
|
|
|
|
|
|
days: '1\u5929',
|
|
|
|
|
|
transportMode: '\u706b\u8f66',
|
|
|
|
|
|
amount: '',
|
|
|
|
|
|
grade: 'P5',
|
|
|
|
|
|
applicant: '\u674e\u6587\u9759',
|
|
|
|
|
|
department: '\u6280\u672f\u90e8',
|
|
|
|
|
|
position: '\u8d22\u52a1\u667a\u80fd\u5316\u4ea7\u54c1\u7ecf\u7406',
|
|
|
|
|
|
managerName: '\u5411\u4e07\u7ea2'
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
const message = {
|
|
|
|
|
|
id: 'application-preview-editor-date-message',
|
|
|
|
|
|
applicationPreview: preview,
|
|
|
|
|
|
text: ''
|
|
|
|
|
|
}
|
|
|
|
|
|
const requestedPayloads = []
|
|
|
|
|
|
const editor = useApplicationPreviewEditor({
|
|
|
|
|
|
persistSessionState: () => {},
|
|
|
|
|
|
toast: () => {},
|
|
|
|
|
|
currentUser: ref({ grade: 'P5' }),
|
|
|
|
|
|
calculateTravelReimbursement: async (payload) => {
|
|
|
|
|
|
requestedPayloads.push(payload)
|
|
|
|
|
|
return {
|
|
|
|
|
|
days: payload.days,
|
|
|
|
|
|
location: payload.location,
|
|
|
|
|
|
matched_city: payload.location,
|
|
|
|
|
|
grade: payload.grade,
|
|
|
|
|
|
hotel_rate: 450,
|
|
|
|
|
|
hotel_amount: 1800,
|
|
|
|
|
|
total_allowance_rate: 100,
|
|
|
|
|
|
allowance_amount: 400,
|
|
|
|
|
|
total_amount: 2200,
|
|
|
|
|
|
rule_name: '\u516c\u53f8\u5dee\u65c5\u8d39\u62a5\u9500\u89c4\u5219',
|
|
|
|
|
|
rule_version: 'v1.0.0'
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
editor.openApplicationPreviewEditor(message, 'time', message.applicationPreview.fields.time)
|
|
|
|
|
|
editor.setApplicationPreviewDateMode('range')
|
|
|
|
|
|
editor.applicationPreviewEditor.value.rangeStartDate = '2026-02-20'
|
|
|
|
|
|
editor.applicationPreviewEditor.value.rangeEndDate = '2026-02-23'
|
|
|
|
|
|
const committed = await editor.commitApplicationPreviewDateEditor(message)
|
|
|
|
|
|
|
|
|
|
|
|
assert.equal(committed, true)
|
2026-06-13 14:52:26 +00:00
|
|
|
|
assert.deepEqual(requestedPayloads.at(-1), {
|
|
|
|
|
|
days: 4,
|
|
|
|
|
|
location: '\u4e0a\u6d77',
|
|
|
|
|
|
grade: 'P5',
|
|
|
|
|
|
transport_mode: '\u706b\u8f66',
|
|
|
|
|
|
origin_location: null,
|
|
|
|
|
|
travel_date: '2026-02-20'
|
|
|
|
|
|
})
|
2026-06-03 09:25:23 +08:00
|
|
|
|
assert.equal(message.applicationPreview.fields.time, '2026-02-20 \u81f3 2026-02-23')
|
|
|
|
|
|
assert.equal(message.applicationPreview.fields.days, '4\u5929')
|
|
|
|
|
|
assert.equal(message.applicationPreview.fields.lodgingDailyCap, '450\u5143/\u5929')
|
|
|
|
|
|
assert.equal(message.applicationPreview.fields.subsidyDailyCap, '100\u5143/\u5929')
|
|
|
|
|
|
assert.match(message.applicationPreview.fields.policyEstimate, /\u8865\u8d34 400\u5143/)
|
|
|
|
|
|
})
|
2026-06-22 15:56:06 +08:00
|
|
|
|
|
|
|
|
|
|
test('application preview editor can edit return date from table row', async () => {
|
|
|
|
|
|
const preview = normalizeApplicationPreview({
|
|
|
|
|
|
fields: {
|
|
|
|
|
|
applicationType: '\u5dee\u65c5\u8d39\u7528\u7533\u8bf7',
|
|
|
|
|
|
time: '2026-02-20 \u81f3 2026-02-23',
|
|
|
|
|
|
location: '\u4e0a\u6d77',
|
|
|
|
|
|
reason: '\u670d\u52a1\u9879\u76ee\u90e8\u7f72',
|
|
|
|
|
|
days: '4\u5929',
|
|
|
|
|
|
transportMode: '\u706b\u8f66',
|
|
|
|
|
|
amount: '',
|
|
|
|
|
|
grade: 'P5',
|
|
|
|
|
|
applicant: '\u674e\u6587\u9759',
|
|
|
|
|
|
department: '\u6280\u672f\u90e8',
|
|
|
|
|
|
position: '\u8d22\u52a1\u667a\u80fd\u5316\u4ea7\u54c1\u7ecf\u7406',
|
|
|
|
|
|
managerName: '\u5411\u4e07\u7ea2'
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
const message = {
|
|
|
|
|
|
id: 'application-preview-editor-return-date-message',
|
|
|
|
|
|
applicationPreview: preview,
|
|
|
|
|
|
text: ''
|
|
|
|
|
|
}
|
|
|
|
|
|
const requestedPayloads = []
|
|
|
|
|
|
const editor = useApplicationPreviewEditor({
|
|
|
|
|
|
persistSessionState: () => {},
|
|
|
|
|
|
toast: () => {},
|
|
|
|
|
|
currentUser: ref({ grade: 'P5' }),
|
|
|
|
|
|
calculateTravelReimbursement: async (payload) => {
|
|
|
|
|
|
requestedPayloads.push(payload)
|
|
|
|
|
|
return {
|
|
|
|
|
|
days: payload.days,
|
|
|
|
|
|
location: payload.location,
|
|
|
|
|
|
matched_city: payload.location,
|
|
|
|
|
|
grade: payload.grade,
|
|
|
|
|
|
hotel_rate: 450,
|
|
|
|
|
|
hotel_amount: 2250,
|
|
|
|
|
|
total_allowance_rate: 100,
|
|
|
|
|
|
allowance_amount: 500,
|
|
|
|
|
|
total_amount: 2750,
|
|
|
|
|
|
rule_name: '\u516c\u53f8\u5dee\u65c5\u8d39\u62a5\u9500\u89c4\u5219',
|
|
|
|
|
|
rule_version: 'v1.0.0'
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
editor.openApplicationPreviewEditor(message, 'time_return', '2026-02-23')
|
|
|
|
|
|
assert.equal(editor.resolveApplicationPreviewEditorControl('time_return'), 'date')
|
|
|
|
|
|
assert.equal(editor.applicationPreviewEditor.value.dateMode, 'range')
|
|
|
|
|
|
assert.equal(editor.applicationPreviewEditor.value.rangeStartDate, '2026-02-20')
|
|
|
|
|
|
editor.applicationPreviewEditor.value.rangeEndDate = '2026-02-24'
|
|
|
|
|
|
const committed = await editor.commitApplicationPreviewDateEditor(message)
|
|
|
|
|
|
|
|
|
|
|
|
assert.equal(committed, true)
|
|
|
|
|
|
assert.deepEqual(requestedPayloads.at(-1), {
|
|
|
|
|
|
days: 5,
|
|
|
|
|
|
location: '\u4e0a\u6d77',
|
|
|
|
|
|
grade: 'P5',
|
|
|
|
|
|
transport_mode: '\u706b\u8f66',
|
|
|
|
|
|
origin_location: null,
|
|
|
|
|
|
travel_date: '2026-02-20'
|
|
|
|
|
|
})
|
|
|
|
|
|
assert.equal(message.applicationPreview.fields.time, '2026-02-20 \u81f3 2026-02-24')
|
|
|
|
|
|
assert.equal(message.applicationPreview.fields.days, '5\u5929')
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
test('application preview editor can edit return date from inline table input', async () => {
|
|
|
|
|
|
const preview = normalizeApplicationPreview({
|
|
|
|
|
|
fields: {
|
|
|
|
|
|
applicationType: '\u5dee\u65c5\u8d39\u7528\u7533\u8bf7',
|
|
|
|
|
|
time: '2026-02-20 \u81f3 2026-02-23',
|
|
|
|
|
|
location: '\u4e0a\u6d77',
|
|
|
|
|
|
reason: '\u670d\u52a1\u9879\u76ee\u90e8\u7f72',
|
|
|
|
|
|
days: '4\u5929',
|
|
|
|
|
|
transportMode: '\u706b\u8f66',
|
|
|
|
|
|
amount: '',
|
|
|
|
|
|
grade: 'P5',
|
|
|
|
|
|
applicant: '\u674e\u6587\u9759',
|
|
|
|
|
|
department: '\u6280\u672f\u90e8',
|
|
|
|
|
|
position: '\u8d22\u52a1\u667a\u80fd\u5316\u4ea7\u54c1\u7ecf\u7406',
|
|
|
|
|
|
managerName: '\u5411\u4e07\u7ea2'
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
const message = {
|
|
|
|
|
|
id: 'application-preview-editor-inline-return-date-message',
|
|
|
|
|
|
applicationPreview: preview,
|
|
|
|
|
|
text: ''
|
|
|
|
|
|
}
|
|
|
|
|
|
const requestedPayloads = []
|
|
|
|
|
|
const editor = useApplicationPreviewEditor({
|
|
|
|
|
|
persistSessionState: () => {},
|
|
|
|
|
|
toast: () => {},
|
|
|
|
|
|
currentUser: ref({ grade: 'P5' }),
|
|
|
|
|
|
calculateTravelReimbursement: async (payload) => {
|
|
|
|
|
|
requestedPayloads.push(payload)
|
|
|
|
|
|
return {
|
|
|
|
|
|
days: payload.days,
|
|
|
|
|
|
location: payload.location,
|
|
|
|
|
|
matched_city: payload.location,
|
|
|
|
|
|
grade: payload.grade,
|
|
|
|
|
|
hotel_rate: 450,
|
|
|
|
|
|
hotel_amount: 2250,
|
|
|
|
|
|
total_allowance_rate: 100,
|
|
|
|
|
|
allowance_amount: 500,
|
|
|
|
|
|
total_amount: 2750,
|
|
|
|
|
|
rule_name: '\u516c\u53f8\u5dee\u65c5\u8d39\u62a5\u9500\u89c4\u5219',
|
|
|
|
|
|
rule_version: 'v1.0.0'
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
editor.openApplicationPreviewEditor(message, 'time_return', '2026-02-23')
|
|
|
|
|
|
editor.applicationPreviewEditor.value.draftValue = '2026-02-24'
|
|
|
|
|
|
const committed = await editor.commitApplicationPreviewEditor(message)
|
|
|
|
|
|
|
|
|
|
|
|
assert.equal(committed, true)
|
|
|
|
|
|
assert.deepEqual(requestedPayloads.at(-1), {
|
|
|
|
|
|
days: 5,
|
|
|
|
|
|
location: '\u4e0a\u6d77',
|
|
|
|
|
|
grade: 'P5',
|
|
|
|
|
|
transport_mode: '\u706b\u8f66',
|
|
|
|
|
|
origin_location: null,
|
|
|
|
|
|
travel_date: '2026-02-20'
|
|
|
|
|
|
})
|
|
|
|
|
|
assert.equal(message.applicationPreview.fields.time, '2026-02-20 \u81f3 2026-02-24')
|
|
|
|
|
|
assert.equal(message.applicationPreview.fields.time_return, undefined)
|
|
|
|
|
|
assert.equal(message.applicationPreview.fields.days, '5\u5929')
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-06-23 09:42:13 +08:00
|
|
|
|
test('application preview editor opens date fields with native date input values', () => {
|
|
|
|
|
|
const preview = normalizeApplicationPreview({
|
|
|
|
|
|
fields: {
|
|
|
|
|
|
applicationType: '\u5dee\u65c5\u8d39\u7528\u7533\u8bf7',
|
|
|
|
|
|
time: '2026-02-20 \u81f3 2026-02-23',
|
|
|
|
|
|
location: '\u4e0a\u6d77',
|
|
|
|
|
|
reason: '\u670d\u52a1\u9879\u76ee\u90e8\u7f72',
|
|
|
|
|
|
days: '4\u5929'
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
const message = {
|
|
|
|
|
|
id: 'application-preview-editor-native-date-message',
|
|
|
|
|
|
applicationPreview: preview,
|
|
|
|
|
|
text: ''
|
|
|
|
|
|
}
|
|
|
|
|
|
const editor = useApplicationPreviewEditor({
|
|
|
|
|
|
persistSessionState: () => {},
|
|
|
|
|
|
toast: () => {}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
editor.openApplicationPreviewEditor(message, 'time', message.applicationPreview.fields.time)
|
|
|
|
|
|
assert.equal(editor.resolveApplicationPreviewEditorControl('time'), 'date')
|
|
|
|
|
|
assert.equal(editor.applicationPreviewEditor.value.draftValue, '2026-02-20')
|
|
|
|
|
|
assert.equal(editor.resolveApplicationPreviewEditorDateMax(message, 'time'), '2026-02-23')
|
|
|
|
|
|
|
|
|
|
|
|
editor.openApplicationPreviewEditor(message, 'time_return', '2026-02-23')
|
|
|
|
|
|
assert.equal(editor.resolveApplicationPreviewEditorControl('time_return'), 'date')
|
|
|
|
|
|
assert.equal(editor.applicationPreviewEditor.value.draftValue, '2026-02-23')
|
|
|
|
|
|
assert.equal(editor.resolveApplicationPreviewEditorDateMin(message, 'time_return'), '2026-02-20')
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
test('application preview editor blocks invalid date ranges', async () => {
|
|
|
|
|
|
const preview = normalizeApplicationPreview({
|
|
|
|
|
|
fields: {
|
|
|
|
|
|
applicationType: '\u5dee\u65c5\u8d39\u7528\u7533\u8bf7',
|
|
|
|
|
|
time: '2026-02-20 \u81f3 2026-02-23',
|
|
|
|
|
|
location: '\u4e0a\u6d77',
|
|
|
|
|
|
reason: '\u670d\u52a1\u9879\u76ee\u90e8\u7f72',
|
|
|
|
|
|
days: '4\u5929',
|
|
|
|
|
|
transportMode: '\u706b\u8f66',
|
|
|
|
|
|
amount: ''
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
const message = {
|
|
|
|
|
|
id: 'application-preview-editor-invalid-date-message',
|
|
|
|
|
|
applicationPreview: preview,
|
|
|
|
|
|
text: ''
|
|
|
|
|
|
}
|
|
|
|
|
|
const toastMessages = []
|
|
|
|
|
|
const editor = useApplicationPreviewEditor({
|
|
|
|
|
|
persistSessionState: () => {},
|
|
|
|
|
|
toast: (messageText) => {
|
|
|
|
|
|
toastMessages.push(messageText)
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
editor.openApplicationPreviewEditor(message, 'time_return', '2026-02-23')
|
|
|
|
|
|
editor.applicationPreviewEditor.value.draftValue = '2026-02-19'
|
|
|
|
|
|
const returnCommitted = await editor.commitApplicationPreviewEditor(message)
|
|
|
|
|
|
|
|
|
|
|
|
assert.equal(returnCommitted, false)
|
|
|
|
|
|
assert.equal(message.applicationPreview.fields.time, '2026-02-20 \u81f3 2026-02-23')
|
|
|
|
|
|
assert.equal(message.applicationPreview.fields.days, '4\u5929')
|
|
|
|
|
|
assert.equal(toastMessages.at(-1), '\u51fa\u53d1\u65f6\u95f4\u4e0d\u80fd\u665a\u4e8e\u8fd4\u56de\u65f6\u95f4\uff0c\u8bf7\u91cd\u65b0\u9009\u62e9\u3002')
|
|
|
|
|
|
|
|
|
|
|
|
editor.openApplicationPreviewEditor(message, 'time', message.applicationPreview.fields.time)
|
|
|
|
|
|
editor.applicationPreviewEditor.value.draftValue = '2026-02-24'
|
|
|
|
|
|
const startCommitted = await editor.commitApplicationPreviewEditor(message)
|
|
|
|
|
|
|
|
|
|
|
|
assert.equal(startCommitted, false)
|
|
|
|
|
|
assert.equal(message.applicationPreview.fields.time, '2026-02-20 \u81f3 2026-02-23')
|
|
|
|
|
|
assert.equal(toastMessages.at(-1), '\u51fa\u53d1\u65f6\u95f4\u4e0d\u80fd\u665a\u4e8e\u8fd4\u56de\u65f6\u95f4\uff0c\u8bf7\u91cd\u65b0\u9009\u62e9\u3002')
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-06-22 15:56:06 +08:00
|
|
|
|
test('application preview editor estimates after shorthand return date input', async () => {
|
|
|
|
|
|
const preview = normalizeApplicationPreview({
|
|
|
|
|
|
fields: {
|
|
|
|
|
|
applicationType: '\u5dee\u65c5\u8d39\u7528\u7533\u8bf7',
|
|
|
|
|
|
time: '2026-06-23',
|
|
|
|
|
|
location: '\u5317\u4eac',
|
|
|
|
|
|
reason: '\u652f\u6491\u5ba2\u6237\u73b0\u573a\u5b9e\u65bd',
|
|
|
|
|
|
days: '',
|
|
|
|
|
|
transportMode: '',
|
|
|
|
|
|
amount: '',
|
|
|
|
|
|
grade: 'P5',
|
|
|
|
|
|
applicant: '\u674e\u6587\u9759',
|
|
|
|
|
|
department: '\u6280\u672f\u90e8',
|
|
|
|
|
|
position: '\u8d22\u52a1\u667a\u80fd\u5316\u4ea7\u54c1\u7ecf\u7406',
|
|
|
|
|
|
managerName: '\u5411\u4e07\u7ea2'
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
const message = {
|
|
|
|
|
|
id: 'application-preview-editor-shorthand-return-date-message',
|
|
|
|
|
|
applicationPreview: preview,
|
|
|
|
|
|
text: ''
|
|
|
|
|
|
}
|
|
|
|
|
|
const requestedPayloads = []
|
|
|
|
|
|
const editor = useApplicationPreviewEditor({
|
|
|
|
|
|
persistSessionState: () => {},
|
|
|
|
|
|
toast: () => {},
|
|
|
|
|
|
currentUser: ref({ grade: 'P5', location: '\u6b66\u6c49' }),
|
|
|
|
|
|
calculateTravelReimbursement: async (payload) => {
|
|
|
|
|
|
requestedPayloads.push(payload)
|
|
|
|
|
|
return {
|
|
|
|
|
|
days: payload.days,
|
|
|
|
|
|
location: payload.location,
|
|
|
|
|
|
matched_city: payload.location,
|
|
|
|
|
|
grade: payload.grade,
|
|
|
|
|
|
hotel_rate: 450,
|
|
|
|
|
|
hotel_amount: 1350,
|
|
|
|
|
|
total_allowance_rate: 100,
|
|
|
|
|
|
allowance_amount: 300,
|
|
|
|
|
|
total_amount: 1650,
|
|
|
|
|
|
rule_name: '\u516c\u53f8\u5dee\u65c5\u8d39\u62a5\u9500\u89c4\u5219',
|
|
|
|
|
|
rule_version: 'v1.0.0'
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
editor.openApplicationPreviewEditor(message, 'time_return', '\u5f85\u8865\u5145')
|
|
|
|
|
|
editor.applicationPreviewEditor.value.draftValue = '6\u670825\u65e5'
|
|
|
|
|
|
const committed = await editor.commitApplicationPreviewEditor(message)
|
|
|
|
|
|
|
|
|
|
|
|
assert.equal(committed, true)
|
|
|
|
|
|
assert.deepEqual(requestedPayloads.at(-1), {
|
|
|
|
|
|
days: 3,
|
|
|
|
|
|
location: '\u5317\u4eac',
|
|
|
|
|
|
grade: 'P5',
|
|
|
|
|
|
transport_mode: null,
|
|
|
|
|
|
origin_location: '\u6b66\u6c49',
|
|
|
|
|
|
travel_date: '2026-06-23'
|
|
|
|
|
|
})
|
|
|
|
|
|
assert.equal(message.applicationPreview.fields.time, '2026-06-23 \u81f3 2026-06-25')
|
|
|
|
|
|
assert.equal(message.applicationPreview.fields.days, '3\u5929')
|
|
|
|
|
|
assert.equal(message.applicationPreview.fields.lodgingDailyCap, '450\u5143/\u5929')
|
|
|
|
|
|
assert.equal(message.applicationPreview.fields.subsidyDailyCap, '100\u5143/\u5929')
|
|
|
|
|
|
assert.equal(message.applicationPreview.fields.amount, '1,650\u5143\uff08\u4e0d\u542b\u4ea4\u901a\uff09')
|
|
|
|
|
|
assert.match(message.applicationPreview.fields.policyEstimate, /\u4ea4\u901a\u5f85\u8865\u5145/)
|
|
|
|
|
|
})
|