@@ -20,6 +20,7 @@ import {
normalizeTransportModeOption ,
resolveApplicationDateRange ,
resolveApplicationTimeLabel ,
shouldRequireApplicationModelReview ,
shouldUseLocalApplicationPreview
} from '../src/utils/expenseApplicationPreview.js'
import {
@@ -50,6 +51,14 @@ import {
import {
shouldUseBudgetCompileReport
} from '../src/views/scripts/budgetAssistantReportModel.js'
import { resolveStewardTypewriterNextIndex } from '../src/views/scripts/stewardTypewriter.js'
import {
ASSISTANT _SCOPE _SESSION _APPLICATION ,
ASSISTANT _SCOPE _SESSION _EXPENSE ,
ASSISTANT _SCOPE _SESSION _STEWARD ,
inferAssistantScopeTarget ,
resolveAssistantScopeGuard
} from '../src/utils/assistantSessionScope.js'
import { useTravelReimbursementFlow } from '../src/views/scripts/useTravelReimbursementFlow.js'
import { useApplicationPreviewEditor } from '../src/views/scripts/useApplicationPreviewEditor.js'
@@ -65,6 +74,26 @@ const createViewScript = readFileSync(
fileURLToPath ( new URL ( '../src/views/scripts/TravelReimbursementCreateView.js' , import . meta . url ) ) ,
'utf8'
)
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'
)
const stewardPlanFlowScript = readFileSync (
fileURLToPath ( new URL ( '../src/views/scripts/useStewardPlanFlow.js' , import . meta . url ) ) ,
'utf8'
@@ -131,7 +160,7 @@ function createFlowHarness() {
}
test ( 'application intent uses local preview instead of immediate orchestrator call' , ( ) => {
const prompt = '申请 2026-05-20 至 2026-05-23 去上海支撑上海电力部署项目,出差3 天, 高铁, 预计金额2358元'
const prompt = '申请 2026-05-20 至 2026-05-23 去上海支撑上海电力部署项目,出差4 天, 高铁, 预计金额2358元'
assert . equal (
shouldUseLocalApplicationPreview ( prompt , {
sessionType : 'application' ,
@@ -150,6 +179,33 @@ test('application intent uses local preview instead of immediate orchestrator ca
} ) ,
false
)
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
)
const preview = buildLocalApplicationPreview ( prompt , {
name : '李文静' ,
@@ -161,7 +217,7 @@ test('application intent uses local preview instead of immediate orchestrator ca
assert . equal ( preview . fields . applicationType , '差旅费用申请' )
assert . equal ( preview . fields . time , '2026-05-20 至 2026-05-23' )
assert . equal ( preview . fields . location , '上海' )
assert . equal ( preview . fields . days , '3 天' )
assert . equal ( preview . fields . days , '4 天' )
assert . equal ( preview . fields . transportMode , '火车' )
assert . equal ( preview . fields . amount , '2358元' )
assert . equal ( preview . fields . applicant , '李文静' )
@@ -175,6 +231,33 @@ test('application intent uses local preview instead of immediate orchestrator ca
assert . match ( buildLocalApplicationPreviewMessage ( preview ) , /点击对应行即可直接编辑/ )
} )
test ( 'assistant scope guard blocks unsupported non-financial intent' , ( ) => {
const guard = resolveAssistantScopeGuard ( '帮我写一首诗,主题是春天' , ASSISTANT _SCOPE _SESSION _APPLICATION )
assert . equal ( guard . blocked , true )
assert . equal ( guard . targetSessionType , '' )
assert . match ( guard . text , /此意图系统不支持/ )
assert . match ( guard . text , /当前系统支持的业务范围/ )
assert . deepEqual ( guard . suggestedActions , [ ] )
} )
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 )
} )
test ( 'travel application submit can continue with conversational planning recommendation' , ( ) => {
const preview = normalizeApplicationPreview ( {
fields : {
@@ -273,12 +356,12 @@ test('application estimate builds deterministic mock transport amount and total'
assert . equal ( trainEstimate . amountDisplay , '1,040' )
assert . equal ( datedTrainEstimate . queryDate , '2026-05-25' )
assert . equal ( datedTrainEstimate . amountDisplay , '1,100' )
assert . equal ( datedTrainEstimate . source , 'mock_ticket_price_query _v1' )
assert . equal ( datedTrainEstimate . source , 'fallback_transport_budget_estimate _v1' )
assert . equal ( datedTrainEstimate . basisText , '预估交通费用 1,100元' )
assert . ok ( datedTrainEstimate . simulatedLatencyMs >= 360 )
assert . ok ( datedTrainEstimate . simulatedLatencyMs <= 779 )
assert . equal ( resolveMockApplicationTransportWaitMs ( datedTrainEstimate ) , 320 )
assert . equal ( flightEstimate . amountDisplay , '3,6 00' )
assert . equal ( flightEstimate . amountDisplay , '3,2 00' )
assert . equal ( shipEstimate . amountDisplay , '1,040' )
assert . equal ( totalEstimate . transportAmountDisplay , '1,040' )
assert . equal ( totalEstimate . totalAmountDisplay , '3,200' )
@@ -323,6 +406,168 @@ test('application preview uses selected date range and business-specific time la
assert . doesNotMatch ( submitText , /发生时间:/ )
} )
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 , /小财管家继续执行/ )
} )
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 , [ ] )
} )
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 , [ ] )
} )
test ( 'application preview keeps labeled reason in structured travel form' , ( ) => {
const preview = buildLocalApplicationPreview ( [
'发生时间: 2026-02-20 至 2026-02-23' ,
@@ -392,7 +637,80 @@ test('application preview can be refined by ontology model extraction', () => {
assert . equal ( refinedPreview . fields . transportMode , '火车' )
} )
test ( 'application preview ignores model-only transport mode guess es' , ( ) => {
test ( 'application preview preserves ontology amount roles for travel estimat es' , ( ) => {
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' , ( ) => {
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' ,
@@ -421,10 +739,25 @@ test('application preview ignores model-only transport mode guesses', () => {
assert . equal ( localPreview . fields . transportMode , '' )
assert . equal ( refinedPreview . fields . transportMode , '' )
assert . ok ( refinedPreview . missingFields . includes ( '\u51fa\u884c\u65b9\u5f0f' ) )
assert . equal ( refinedPreview . missingFields . includes ( '\u51fa\u884c\u65b9\u5f0f' ) , true )
assert . equal ( refinedPreview . readyToSubmit , false )
} )
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 , '火车' )
} )
test ( 'application preview precomputes a date range from today when only days are provided' , ( ) => {
const preview = buildLocalApplicationPreview (
'去北京出差3天, 支撑国网仿生产环境部署, 飞机, 预计费用12000元' ,
@@ -438,7 +771,7 @@ test('application preview precomputes a date range from today when only days are
} )
test ( 'application preview keeps rule fallback distinct from model reviewed result' , ( ) => {
const rawText = '申请 2026-05-20 至 2026-05-23 去上海支撑服务器部署,出差3 天, 火车, 预计费用1800元'
const rawText = '申请 2026-05-20 至 2026-05-23 去上海支撑服务器部署,出差4 天, 火车, 预计费用1800元'
const localPreview = buildLocalApplicationPreview ( rawText , { name : '李文静' , grade : 'P5' } )
const fallbackPreview = buildModelRefinedApplicationPreview (
localPreview ,
@@ -545,11 +878,11 @@ test('application session shows intent flow, persists preview, and supports inli
assert . doesNotMatch ( createViewScript , /if \(isApplicationSession\.value\) \{\s*return false\s*\}/ )
assert . match ( createViewScript , /activeFlowSteps\.value\.length > 0/ )
assert . match ( createViewScript , /useApplicationPreviewEditor/ )
assert . match ( createView Script, /message-bubble-application-preview/ )
assert . match ( createView Script, /buildApplicationPreviewFooterMessage/ )
assert . match ( createView Script, /function buildApplicationPreviewFooterText\(message\)/ )
assert . match ( createView Script, /buildApplicationPreviewSubmitText/ )
assert . match ( createView Script, /user_input_text: applicationSubmitText/ )
assert . match ( messageActions Script, /message-bubble-application-preview/ )
assert . match ( messageActions Script, /buildApplicationPreviewFooterMessage/ )
assert . match ( messageActions Script, /function buildApplicationPreviewFooterText\(message\)/ )
assert . match ( stewardRuntime Script, /buildApplicationPreviewSubmitText/ )
assert . match ( stewardRuntime Script, /user_input_text: applicationSubmitText/ )
assert . match ( conversationModelScript , /applicationPreview: null/ )
assert . match ( conversationModelScript , /applicationPreview: message\.applicationPreview \|\| null/ )
assert . match ( conversationModelScript , /\|\| message\.applicationPreview/ )
@@ -564,7 +897,7 @@ test('application session shows intent flow, persists preview, and supports inli
assert . match ( messageItemTemplate , /v-html="ui\.renderMarkdown\(ui\.buildApplicationPreviewFooterText\(message\)\)"/ )
assert . doesNotMatch ( messageItemTemplate , /class="application-date-editor-layer"/ )
assert . doesNotMatch ( messageItemTemplate , /ui\.commitApplicationPreviewDateEditor\(message\)/ )
assert . m atch( messageItemTemplate , /application-preview-date-chip/ )
assert . doesNotM atch( messageItemTemplate , /application-preview-date-chip/ )
assert . match ( messageItemTemplate , /申请单据已生成/ )
assert . match ( messageItemTemplate , /ui\.shouldShowDraftSavedCard\(message\)/ )
assert . match ( messageItemTemplate , /报销草稿已生成/ )
@@ -575,9 +908,9 @@ test('application session shows intent flow, persists preview, and supports inli
assert . match ( messageItemTemplate , /查看详情/ )
assert . match ( messageItemTemplate , /class="reimbursement-draft-pending-detail"/ )
assert . match ( messageItemTemplate , /保存后可查看详情/ )
assert . match ( createView Script, /function canOpenDraftDetail\(message\)/ )
assert . match ( messageActions Script, /function canOpenDraftDetail\(message\)/ )
assert . match ( createViewScript , /canOpenDraftDetail,/ )
assert . match ( createView Script, /保存后生成/ )
assert . match ( messageActions Script, /保存后生成/ )
assert . doesNotMatch ( messageItemTemplate , /ui\.buildReimbursementDraftSummaryItems\(message\.draftPayload\)/ )
assert . doesNotMatch ( messageItemTemplate , /可以继续上传票据,我会归集到这张草稿。/ )
assert . ok (
@@ -619,12 +952,12 @@ test('application session shows intent flow, persists preview, and supports inli
assert . match ( createViewScript , /onComposerDateSelection: applyLinkedApplicationPreviewDateSelection/ )
assert . match ( createViewScript , /function openApplicationPreviewEditorFromUi/ )
assert . match ( createViewScript , /syncComposerDateFromApplicationEditor/ )
assert . match ( createView Script, /function shouldShowAssistantMessageActions/ )
assert . match ( createView Script, /function buildMessageOperationFeedbackContext/ )
assert . match ( createView Script, /function isMessageFeedbackSelected/ )
assert . match ( createView Script, /function submitOperationFeedbackForMessage/ )
assert . match ( createView Script, /const stewardSubmitContinuation = message\?\.stewardContinuation \|\| null/ )
assert . match ( createView Script, /stewardContinuation:\s*stewardSubmitContinuation/ )
assert . match ( messageActions Script, /function shouldShowAssistantMessageActions/ )
assert . match ( messageActions Script, /function buildMessageOperationFeedbackContext/ )
assert . match ( messageActions Script, /function isMessageFeedbackSelected/ )
assert . match ( messageActions Script, /function submitOperationFeedbackForMessage/ )
assert . match ( stewardRuntime Script, /const stewardSubmitContinuation = message\?\.stewardContinuation \|\| null/ )
assert . match ( stewardRuntime Script, /stewardContinuation:\s*stewardSubmitContinuation/ )
assert . match ( createViewTemplate , /handleComposerDateInputChange\('single'\)/ )
assert . match ( createViewTemplate , /handleComposerDateInputChange\('range-start'\)/ )
assert . match ( createViewTemplate , /handleComposerDateInputChange\('range-end'\)/ )
@@ -675,26 +1008,26 @@ test('application session shows intent flow, persists preview, and supports inli
assert . match ( flowScript , /refreshCompleted/ )
} )
test ( 'steward application missing transport asks before rendering preview table' , ( ) => {
test ( 'steward application missing transport blocks preview table' , ( ) => {
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/ )
assert . match ( submitComposerScript , /我已经识别出这一步要先处理申请单,但现在还不能生成可提交的申请核对表/ )
assert . match ( submitComposerScript , /applicationPreview:\s*normalized/ )
assert . m atch( submitComposerScript , /请先告诉我你打算怎么出行:\*\*火车、飞机或轮船\*\*/ )
assert . doesNotMatch ( submitComposerScript , /缺少“出行方式”[\s\S]{0,500}更新下方核对表/ )
assert . doesNotM atch( submitComposerScript , /请先告诉我你打算怎么出行:\*\*火车、飞机或轮船\*\*/ )
assert . match ( createView Script, /payload\.applicationPreview/ )
assert . match ( createView Script, /function continueStewardApplicationFieldCompletion/ )
assert . match ( createView Script, /submitComposerInternal\(\{[\s\S]*stewardContinuation: continuation/ )
assert . match ( createView Script, /skipUserMessage:\s*true/ )
assert . match ( createView Script, /targetMessage\.applicationPreview = normalizeApplicationPreview\(sourcePreview\)/ )
assert . match ( createView Script, /openApplicationPreviewEditor\(targetMessage, fieldKey/ )
assert . match ( createView Script, /commitApplicationPreviewEditor\(targetMessage\)/ )
assert . match ( suggestedActions Script, /payload\.applicationPreview/ )
assert . match ( suggestedActions Script, /function continueStewardApplicationFieldCompletion/ )
assert . match ( suggestedActions Script, /submitComposerInternal\(\{[\s\S]*stewardContinuation: continuation/ )
assert . match ( suggestedActions Script, /skipUserMessage:\s*true/ )
assert . match ( suggestedActions Script, /targetMessage\.applicationPreview = normalizeApplicationPreview\(sourcePreview\)/ )
assert . match ( suggestedActions Script, /openApplicationPreviewEditor\(targetMessage, fieldKey/ )
assert . match ( suggestedActions Script, /commitApplicationPreviewEditor\(targetMessage\)/ )
assert . match ( stewardFieldCompletionScript , /transportMode:\s*'transport_mode'/ )
assert . match ( stewardFieldCompletionScript , /模拟查询交通票据 / )
assert . match ( stewardFieldCompletionScript , /基础规则交通费用预估表 / )
} )
test ( 'steward field completion reruns application preview instead of directly rendering table' , ( ) => {
@@ -739,7 +1072,7 @@ test('steward field completion reruns application preview instead of directly re
assert . match ( carryText , /用户已补充:出行方式:火车/ )
assert . match ( carryText , /地点:北京/ )
assert . match ( carryText , /天数: 3天/ )
assert . match ( carryText , /请先根据已补齐字段模拟查询交通票据 / )
assert . match ( carryText , /请先根据已补齐字段按基础规则交通费用预估表 / )
const rebuiltPreview = buildLocalApplicationPreview ( carryText , { name : '曹笑竹' , grade : 'P5' } )
assert . equal ( rebuiltPreview . fields . location , '北京' )
@@ -758,7 +1091,7 @@ test('budget compile report does not steal steward delegated application rerun',
'用户已补充:出行方式:火车。' ,
'地点:北京' ,
'天数: 3天' ,
'处理要求:请先根据已补齐字段模拟查询交通票据和 费用口径,完成系统预估金额测算,再生成申请单核对表。'
'处理要求:请先根据已补齐字段按基础规则交通费用预估表测算 费用口径,完成系统预估金额测算,再生成申请单核对表。'
] . join ( '\n' )
assert . equal ( shouldUseBudgetCompileReport ( stewardApplicationText , {
@@ -777,66 +1110,109 @@ test('budget compile report does not steal steward delegated application rerun',
test ( 'text confirmation submits pending application preview before replanning steward task' , ( ) => {
assert . match ( stewardServiceScript , /fetchStewardRuntimeDecision/ )
assert . match ( stewardServiceScript , /\/steward\/runtime-decisions/ )
assert . match ( createView Script, /function buildStewardRuntimeState/ )
assert . match ( createView Script, /function buildStewardRuntimeFastPathDecision/ )
assert . match ( createView Script, /function shouldUseStewardRuntimeLlmDecision/ )
assert . match ( createView Script, /function findPendingSlotSuggestedActionContextByInput/ )
assert . match ( createView Script, /function shouldPlanNewStewardTasksLocally/ )
assert . match ( createView Script, /function resolveStewardRuntimeTransportAlias/ )
assert . match ( createView Script, /const actionTransportAlias = resolveStewardRuntimeTransportAlias/ )
assert . match ( createView Script, /actionTransportAlias === transportAlias/ )
assert . match ( createView Script, /next_action:\s*'continue_next_task'/ )
assert . match ( createView Script, /next_action:\s*'submit_current_application'/ )
assert . match ( createView Script, /next_action:\s*'fill_current_slot'/ )
assert . match ( createView Script, /next_action:\s*'plan_new_tasks'/ )
assert . match ( createView Script, /suppressUserEcho:\s*userMessageAlreadyAdded/ )
assert . match ( createView Script, /if \(!action\?\.suppressUserEcho\) \{[\s\S]*messages\.value\.push\(createMessage\('user', userText\)\)/ )
assert . match ( createView Script, /skipApplicationModelReview:\s*true/ )
assert . match ( createView Script, /skipApplicationModelReview:\s*targetSessionType === SESSION_TYPE_APPLICATION/ )
assert . match ( createView Script, /skipStewardSlotDecision:\s*targetSessionType === SESSION_TYPE_APPLICATION/ )
assert . match ( stewardRuntime Script, /function buildStewardRuntimeState/ )
assert . match ( stewardRuntime Script, /function buildStewardRuntimeFastPathDecision/ )
assert . match ( stewardRuntime Script, /function shouldUseStewardRuntimeLlmDecision/ )
assert . match ( stewardRuntime Script, /function findPendingSlotSuggestedActionContextByInput/ )
assert . match ( stewardRuntimeTextModel Script, /function shouldPlanNewStewardTasksLocally/ )
assert . match ( stewardRuntimeTextModel Script, /function resolveStewardRuntimeTransportAlias/ )
assert . match ( stewardRuntime Script, /const actionTransportAlias = resolveStewardRuntimeTransportAlias/ )
assert . match ( stewardRuntime Script, /actionTransportAlias === transportAlias/ )
assert . match ( stewardRuntime Script, /next_action:\s*'continue_next_task'/ )
assert . match ( stewardRuntime Script, /next_action:\s*'submit_current_application'/ )
assert . match ( stewardRuntime Script, /next_action:\s*'fill_current_slot'/ )
assert . match ( stewardRuntime Script, /next_action:\s*'plan_new_tasks'/ )
assert . match ( stewardRuntime Script, /suppressUserEcho:\s*userMessageAlreadyAdded/ )
assert . match ( suggestedActions Script, /if \(!action\?\.suppressUserEcho\) \{[\s\S]*messages\.value\.push\(createMessage\('user', userText\)\)/ )
assert . match ( suggestedActions Script, /skipApplicationModelReview:\s*true/ )
assert . match ( suggestedActions Script, /skipApplicationModelReview:\s*targetSessionType === SESSION_TYPE_APPLICATION/ )
assert . match ( suggestedActions Script, /skipStewardSlotDecision:\s*targetSessionType === SESSION_TYPE_APPLICATION/ )
assert . match ( submitComposerScript , /skipModelReview:\s*Boolean\(stewardDelegated && options\.skipApplicationModelReview\)/ )
assert . match ( submitComposerScript , /if \(options\.skipModelReview\) \{[\s\S]*结构化快路径 / )
assert . match ( submitComposerScript , /const requireModelReview = shouldRequireApplicationModelReview\(rawText\) / )
assert . match ( submitComposerScript , /if \(options\.skipModelReview && !requireModelReview\) \{[\s\S]*结构化快路径/ )
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'\)/ )
assert . match ( createView Script, /async function handleStewardRuntimeDecision/ )
assert . match ( createView Script, /const runtimeState = buildStewardRuntimeState\(\)/ )
assert . match ( createView Script, /if \(!hasActiveStewardRuntimeDecisionContext\(runtimeState\)\) \{[\s\S]*return false/ )
assert . match ( createView Script, /function pushStewardRuntimeUserMessage\(userText = ''\)/ )
assert . match ( createView Script, /const userMessageAlreadyAdded = options\.skipUserMessage[\s\S]*pushStewardRuntimeUserMessage\(rawText\)/ )
assert . match ( createView Script, /const fastDecision = buildStewardRuntimeFastPathDecision\(rawText, runtimeState\)[\s\S]*submitStewardPlan\(\{[\s\S]*skipUserMessage: userMessageAlreadyAdded \|\| options\.skipUserMessage/ )
assert . match ( createView Script, /executeStewardRuntimeDecision\(fastDecision, rawText, \{ userMessageAlreadyAdded \}\)[\s\S]*if \(!shouldUseStewardRuntimeLlmDecision\(rawText, runtimeState\)\)/ )
assert . match ( createView Script, /if \(!shouldUseStewardRuntimeLlmDecision\(rawText, runtimeState\)\)[\s\S]*fetchStewardRuntimeDecision/ )
assert . match ( createView Script, /executeStewardRuntimeDecision\(decision, rawText, \{ userMessageAlreadyAdded \}\)/ )
assert . match ( createView Script, /skipUserMessage: userMessageAlreadyAdded \|\| options\.skipUserMessage/ )
assert . match ( createView Script, /fetchStewardRuntimeDecision\(\{[\s\S]*runtime_state: runtimeState/ )
assert . match ( stewardRuntime Script, /async function handleStewardRuntimeDecision/ )
assert . match ( stewardRuntime Script, /const runtimeState = buildStewardRuntimeState\(\)/ )
assert . match ( stewardRuntime Script, /if \(!hasActiveStewardRuntimeDecisionContext\(runtimeState\)\) \{[\s\S]*return false/ )
assert . match ( stewardRuntime Script, /function pushStewardRuntimeUserMessage\(userText = ''\)/ )
assert . match ( stewardRuntime Script, /const userMessageAlreadyAdded = options\.skipUserMessage[\s\S]*pushStewardRuntimeUserMessage\(rawText\)/ )
assert . match ( stewardRuntime Script, /const fastDecision = buildStewardRuntimeFastPathDecision\(rawText, runtimeState\)[\s\S]*submitStewardPlan\(\{[\s\S]*skipUserMessage: userMessageAlreadyAdded \|\| options\.skipUserMessage/ )
assert . match ( stewardRuntime Script, /executeStewardRuntimeDecision\(fastDecision, rawText, \{ userMessageAlreadyAdded \}\)[\s\S]*if \(!shouldUseStewardRuntimeLlmDecision\(rawText, runtimeState\)\)/ )
assert . match ( stewardRuntime Script, /if \(!shouldUseStewardRuntimeLlmDecision\(rawText, runtimeState\)\)[\s\S]*fetchStewardRuntimeDecision/ )
assert . match ( stewardRuntime Script, /executeStewardRuntimeDecision\(decision, rawText, \{ userMessageAlreadyAdded \}\)/ )
assert . match ( stewardRuntime Script, /skipUserMessage: userMessageAlreadyAdded \|\| options\.skipUserMessage/ )
assert . match ( stewardRuntime Script, /fetchStewardRuntimeDecision\(\{[\s\S]*runtime_state: runtimeState/ )
assert . match ( createViewScript , /if \(await handleStewardRuntimeDecision\(options\)\) \{[\s\S]*return null/ )
assert . match ( createView Script, /function isApplicationSubmitConfirmationText/ )
assert . match ( createView Script, /APPLICATION_SUBMIT_CONFIRM_TEXT_PATTERN[\s\S]*确认提交[\s\S]*提交审批/ )
assert . match ( createView Script, /function findPendingApplicationSubmitMessage/ )
assert . match ( createView Script, /normalizedPreview\.readyToSubmit/ )
assert . match ( createView Script, /async function handleApplicationSubmitConfirmationText/ )
assert . match ( createView Script, /await confirmApplicationSubmit\(\{ userText: rawText \}\)/ )
assert . match ( stewardRuntimeTextModel Script, /function isApplicationSubmitConfirmationText/ )
assert . match ( stewardRuntimeTextModel Script, /APPLICATION_SUBMIT_CONFIRM_TEXT_PATTERN[\s\S]*确认提交[\s\S]*提交审批/ )
assert . match ( stewardRuntime Script, /function findPendingApplicationSubmitMessage/ )
assert . match ( stewardRuntime Script, /normalizedPreview\.readyToSubmit/ )
assert . match ( stewardRuntime Script, /async function handleApplicationSubmitConfirmationText/ )
assert . match ( stewardRuntime Script, /await confirmApplicationSubmit\(\{ userText: rawText \}\)/ )
assert . match ( createViewScript , /if \(await handleApplicationSubmitConfirmationText\(options\)\) \{[\s\S]*return null[\s\S]*\}[\s\S]*if \(isStewardSession\.value && !options\.skipStewardPlan/ )
assert . match ( createView Script, /message\.applicationSubmitConfirmed = true/ )
assert . match ( createView Script, /message\.applicationSubmitConfirmed[\s\S]*continue/ )
assert . match ( stewardRuntime Script, /message\.applicationSubmitConfirmed = true/ )
assert . match ( stewardRuntime Script, /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,/ )
} )
test ( 'steward streaming uses chunked typewriter to reduce perceived latency' , ( ) => {
assert . match ( stewardPlanFlowScript , /STEWARD_TYPEWRITER_CHUNK_SIZE = 4/ )
assert . match ( stewardPlanFlowScript , /STEWARD_THINKING_TYPEWRITER_CHUNK_SIZE = 5/ )
assert . match ( stewardPlanFlowScript , /index = Math\.min\(total, index \+ STEWARD_TYPEWRITER_CHUNK_SIZE \)/ )
assert . match ( stewardPlanFlowScript , /resolveStewardTypewriterNextIndex\(chars, index \)/ )
assert . match ( stewardPlanFlowScript , /index = Math\.min\(chars\.length, index \+ STEWARD_THINKING_TYPEWRITER_CHUNK_SIZE\)/ )
assert . match ( submitComposerScript , /STEWARD_DELEGATED_TYPEWRITER_CHUNK_SIZE = 4/ )
assert . match ( submitComposerScript , /STEWARD_DELEGATED_THINKING_CHUNK_SIZE = 5/ )
assert . match ( submitComposerScript , /index = Math\.min\(chars\.length, index \+ STEWARD_DELEGATED_TYPEWRITER_CHUNK_SIZE \)/ )
assert . match ( submitComposerScript , /resolveStewardTypewriterNextIndex\(chars, index \)/ )
assert . match ( submitComposerScript , /index = Math\.min\(chars\.length, index \+ STEWARD_DELEGATED_THINKING_CHUNK_SIZE\)/ )
assert . match ( createVie wScript, /STEWARD_FOLLOWUP_TYPEWRITER _CHUNK_SIZE = 4 / )
assert . match ( createViewScript , /STEWARD_FOLLOWUP_THINKING_CHUNK_SIZE = 5 /)
assert . match ( createVie wScript, /index = Math\.min\(chars\.length, index \+ STEWARD_FOLLOWUP_TYPEWRITER _CHUNK_SIZE\)/ )
assert . match ( createViewScript , /index = Math\.min\(chars\.length, index \+ STEWARD_FOLLOWUP_THINKING_CHUNK_SIZE\)/ )
assert . match ( stewardFollowupFlo wScript, /STEWARD_FOLLOWUP_THINKING _CHUNK_SIZE = 5 / )
assert . match ( stewardFollowupFlowScript , /resolveStewardTypewriterNextIndex\(chars, index\) /)
assert . match ( stewardFollowupFlo wScript, /index = Math\.min\(chars\.length, index \+ STEWARD_FOLLOWUP_THINKING _CHUNK_SIZE\)/ )
} )
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 )
} )
test ( 'steward initial workbench entry shows recognition state before messages arrive' , ( ) => {
@@ -883,7 +1259,9 @@ test('steward application carry text does not leak transport examples into extra
assert . match ( carryText , /费用类型:差旅/ )
assert . doesNotMatch ( carryText , /费用类型: travel/ )
assert . match ( carryText , /还需要补充:出行方式/ )
assert . m atch( carryText , /请先追问上述缺失信息/ )
assert . doesNotM atch( carryText , /请先追问上述缺失信息/ )
assert . doesNotMatch ( carryText , /请直接生成申请单核对结果/ )
assert . doesNotMatch ( carryText , /入库或提交审批前/ )
assert . doesNotMatch ( carryText , /高铁|火车|飞机|轮船|自驾|出租车/ )
assert . doesNotMatch ( carryText , /预计金额|附件\/凭证|员工编号|金额/ )
assert . equal ( currentTask ? . task _type , 'expense_application' )
@@ -909,7 +1287,7 @@ test('steward application carry text does not leak transport examples into extra
assert . match ( submitComposerScript , /fetchStewardApplicationSlotDecision/ )
assert . match ( submitComposerScript , /task_type:\s*'expense_application'/ )
assert . match ( submitComposerScript , /steward_continuation:\s*continuation/ )
assert . match ( createView Script, /currentTask:\s*actionPayload\.steward_current_task/ )
assert . match ( suggestedActions Script, /currentTask:\s*actionPayload\.steward_current_task/ )
} )
test ( 'steward application slot fallback ignores non-blocking application fields' , ( ) => {
@@ -921,7 +1299,7 @@ test('steward application slot fallback ignores non-blocking application fields'
assert . match ( submitComposerScript , /formatStewardDecisionUserText\(decision\.question/ )
assert . match ( submitComposerScript , /formatStewardDecisionUserText\(decision\.rationale/ )
assert . match ( submitComposerScript , /normalizeTransportModeOption\(value \|\| label, ''\)/ )
assert . match ( createView Script, /normalizeTransportModeOption\(value, ''\)/ )
assert . match ( suggestedActions Script, /normalizeTransportModeOption\(value, ''\)/ )
assert . equal ( normalizeTransportModeOption ( '高铁' , '' ) , '火车' )
assert . equal ( normalizeTransportModeOption ( '自驾' , '' ) , '' )
assert . match ( submitComposerScript , /function resolveBlockingApplicationMissingFieldsForSteward/ )
@@ -1045,8 +1423,10 @@ test('assistant markdown tables render with component-scoped table styling', ()
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;/ )
assert . match ( messageItemStyles , /\.message-answer-markdown :deep\(table\) \{[\s\S]*min-width: 4 60px;[\s\S]*border-collapse: separate ;/ )
assert . match ( messageItemStyles , /\.message-answer-markdown :deep\(th\),[\s\S]*\.message-answer-markdown :deep\(td\) \{[\s\S]*padding: 8px 10px;/ )
assert . match ( messageItemStyles , /\.message-answer-markdown :deep\(table\) \{[\s\S]*min-width: 5 60px;[\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;/ )
} )
test ( 'assistant reimbursement recognition copy renders structured markdown sections' , ( ) => {
@@ -1082,17 +1462,24 @@ test('application date overlap blocks steward preview before duplicate applicati
assert . match ( submitComposerScript , /buildApplicationDateConflictMessage\(applicationDateConflict\)/ )
assert . match ( submitComposerScript , /meta: \[STEWARD_ASSISTANT_NAME, '申请日期冲突'\]/ )
assert . match ( submitComposerScript , /applicationPreview: pauseForMissingFields \? null : applicationPreview/ )
assert . match ( createView Script, /actionType === 'open_application_detail'/ )
assert . match ( suggestedActions Script, /actionType === 'open_application_detail'/ )
} )
test ( 'application preview merges rule center travel estimate into highlighted rows' , ( ) => {
const preview = buildLocalApplicationPreview ( '申请 2026-05-25 至 2026-05-28 去上海出差3天, 服务项目部署, 火车, 预计费用1800元' , {
const preview = buildLocalApplicationPreview ( '申请 2026-05-25 至 2026-05-27 去上海出差3天, 服务项目部署, 火车, 预计费用1800元' , {
name : '李文静' ,
grade : 'P5'
} )
const request = buildApplicationPolicyEstimateRequest ( preview , { grade : 'P5' } )
assert . equal ( request . canCalculate , true )
assert . deepEqual ( request . payload , { days : 3 , location : '上海' , grade : 'P5' } )
assert . deepEqual ( request . payload , {
days : 3 ,
location : '上海' ,
grade : 'P5' ,
transport _mode : '火车' ,
origin _location : null ,
travel _date : '2026-05-25'
} )
const estimatedPreview = applyApplicationPolicyEstimateResult ( preview , {
days : 3 ,
@@ -1103,24 +1490,84 @@ test('application preview merges rule center travel estimate into highlighted ro
hotel _amount : 1800 ,
total _allowance _rate : 120 ,
allowance _amount : 360 ,
total _a mount : 2160 ,
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 ,
rule _name : '公司差旅费报销规则' ,
rule _version : '2026版'
} , { grade : 'P5' } )
assert . equal ( estimatedPreview . fields . lodgingDailyCap , '600元/天' )
assert . equal ( estimatedPreview . fields . subsidyDailyCap , '120元/天' )
assert . equal ( estimatedPreview . fields . transportPolicy , '预估交通费用 1,100元 ' )
assert . equal ( estimatedPreview . fields . transportPolicy , '当前尚未接通实时票务价格查询 API, 无法获取当前实际票价; 先按《交通费用预估表》武汉-上海火车往返(二等座预估)暂估 720元用于申请阶段预算占用, 最终报销以实际票据金额为准 ' )
assert . doesNotMatch ( estimatedPreview . fields . transportPolicy , /参考票价|查询耗时|2026-05-25|真实票据/ )
assert . match ( estimatedPreview . fields . policyEstimate , /交通 1,10 0元/ )
assert . match ( estimatedPreview . fields . policyEstimate , /3,26 0元/ )
assert . equal ( estimatedPreview . fields . transportEstimatedAmount , '1,10 0元' )
assert . equal ( estimatedPreview . fields . transportEstimateDat e , '2026-05-25 ' )
assert . match ( estimatedPreview . fields . transportQueryLatencyMs , /^\d+ms$/ )
assert . equal ( estimatedPreview . fields . amount , '3,26 0元' )
assert . match ( estimatedPreview . fields . policyEstimate , /交通 72 0元/ )
assert . match ( estimatedPreview . fields . policyEstimate , /2,88 0元/ )
assert . equal ( estimatedPreview . fields . transportEstimatedAmount , '72 0元' )
assert . equal ( estimatedPreview . fields . transportEstimateSourc e , 'basic_rule_transport_estimate ' )
assert . equal ( estimatedPreview . fields . transportQueryLatencyMs , '' )
assert . equal ( estimatedPreview . fields . amount , '2,88 0元' )
assert . equal ( buildApplicationPreviewRows ( estimatedPreview ) . find ( ( row ) => row . key === 'policyEstimate' ) ? . highlight , true )
} )
test ( 'application preview blocks policy estimate when transport mode is missing' , ( ) => {
const currentUser = { name : '李文静' , grade : 'P5' , location : '武汉' }
const preview = buildLocalApplicationPreview (
'我要申请2月20日-23日去上海出差, 辅助国网仿生产项目部署' ,
currentUser ,
{ today : '2026-06-09' }
)
const request = buildApplicationPolicyEstimateRequest ( preview , currentUser )
assert . equal ( request . canCalculate , false )
assert . equal ( request . reason , '缺少出行方式' )
assert . equal ( request . payload , null )
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 , '' )
assert . equal ( blockedEstimatePreview . fields . policyEstimate , '填写地点和天数后自动测算' )
assert . equal ( blockedEstimatePreview . missingFields . includes ( '出行方式' ) , true )
assert . equal ( staleEstimatePreview . fields . reason , '辅助国网仿生产项目部署' )
assert . equal ( staleEstimatePreview . fields . transportMode , '火车' )
assert . equal ( staleEstimatePreview . missingFields . includes ( '出行方式' ) , false )
assert . equal ( staleEstimatePreview . fields . transportPolicy , '当前尚未接通实时票务价格查询 API, 无法获取当前实际票价; 先按《交通费用预估表》武汉-上海火车往返(二等座预估)暂估 720元用于申请阶段预算占用, 最终报销以实际票据金额为准' )
assert . match ( staleEstimatePreview . fields . policyEstimate , /交通 720元/ )
assert . equal ( staleEstimatePreview . fields . amount , '2,120元' )
} )
test ( 'application preview editor refreshes transport estimate after mode change' , async ( ) => {
const preview = applyApplicationPolicyEstimateResult (
buildLocalApplicationPreview ( '申请 2026-05-25 至 2026-05-27 去上海出差3天, 服务项目部署' , {
@@ -1162,9 +1609,9 @@ test('application preview editor refreshes transport estimate after mode change'
assert . equal ( committed , true )
assert . equal ( message . applicationPreview . fields . transportMode , '飞机' )
assert . equal ( message . applicationPreview . fields . transportEstimatedAmount , '2 ,33 0元' )
assert . equal ( message . applicationPreview . fields . amount , '4,49 0元' )
assert . equal ( message . applicationPreview . fields . transportPolicy , '预估交通费用 2 ,33 0元' )
assert . equal ( message . applicationPreview . fields . transportEstimatedAmount , '1 ,38 0元' )
assert . equal ( message . applicationPreview . fields . amount , '3,54 0元' )
assert . equal ( message . applicationPreview . fields . transportPolicy , '预估交通费用 1 ,38 0元' )
assert . doesNotMatch ( message . applicationPreview . fields . transportPolicy , /参考票价|查询耗时|2026-05-25|真实票据/ )
assert . doesNotMatch ( message . applicationPreview . fields . transportPolicy , /模拟/ )
assert . ok ( persistCount >= 2 )
@@ -1223,7 +1670,14 @@ test('application preview editor recalculates days and subsidy after date range
const committed = await editor . commitApplicationPreviewDateEditor ( message )
assert . equal ( committed , true )
assert . deepEqual ( requestedPayloads . at ( - 1 ) , { days : 4 , location : '\u4e0a\u6d77' , grade : 'P5' } )
assert . deepEqual ( requestedPayloads . at ( - 1 ) , {
days : 4 ,
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-23' )
assert . equal ( message . applicationPreview . fields . days , '4\u5929' )
assert . equal ( message . applicationPreview . fields . lodgingDailyCap , '450\u5143/\u5929' )