2026-06-22 15:55:59 +08:00
|
|
|
|
import assert from 'node:assert/strict'
|
|
|
|
|
|
import { readFileSync } from 'node:fs'
|
|
|
|
|
|
import { join } from 'node:path'
|
|
|
|
|
|
import test from 'node:test'
|
|
|
|
|
|
|
2026-06-24 10:42:50 +08:00
|
|
|
|
import {
|
|
|
|
|
|
buildLinkedDraftRunningText,
|
|
|
|
|
|
useWorkbenchAiExpenseFlow
|
|
|
|
|
|
} from '../src/composables/workbenchAiMode/useWorkbenchAiExpenseFlow.js'
|
|
|
|
|
|
import {
|
|
|
|
|
|
CONTINUE_REIMBURSEMENT_DRAFT_ACTION,
|
|
|
|
|
|
CREATE_STANDALONE_REIMBURSEMENT_DRAFT_ACTION
|
|
|
|
|
|
} from '../src/views/scripts/travelReimbursementAssociationGateModel.js'
|
2026-06-22 15:55:59 +08:00
|
|
|
|
|
|
|
|
|
|
const personalWorkbenchAiMode = readFileSync(
|
|
|
|
|
|
join(process.cwd(), 'web/src/composables/workbenchAiMode/usePersonalWorkbenchAiMode.js'),
|
|
|
|
|
|
'utf8'
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
function createInlineMessage(role, content, extras = {}) {
|
|
|
|
|
|
return {
|
|
|
|
|
|
id: `${role}-${Math.random().toString(16).slice(2)}`,
|
|
|
|
|
|
role,
|
|
|
|
|
|
content,
|
|
|
|
|
|
text: content,
|
|
|
|
|
|
...extras
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildFlow(options = {}) {
|
|
|
|
|
|
const conversationMessages = { value: [] }
|
|
|
|
|
|
const aiExpenseDraft = { value: null }
|
|
|
|
|
|
const activated = []
|
|
|
|
|
|
let persisted = 0
|
|
|
|
|
|
const scrolled = []
|
|
|
|
|
|
|
|
|
|
|
|
const flow = useWorkbenchAiExpenseFlow({
|
|
|
|
|
|
activateInlineConversation: (payload) => {
|
|
|
|
|
|
activated.push(payload)
|
|
|
|
|
|
conversationStarted.value = true
|
|
|
|
|
|
},
|
|
|
|
|
|
aiExpenseDraft,
|
|
|
|
|
|
assistantDraft: { value: '我要报销' },
|
|
|
|
|
|
clearAiModeFiles: () => {},
|
|
|
|
|
|
closeWorkbenchDatePicker: () => {},
|
|
|
|
|
|
conversationMessages,
|
|
|
|
|
|
conversationStarted,
|
|
|
|
|
|
createInlineMessage,
|
|
|
|
|
|
currentUser: { value: options.currentUser || { name: '张小青', username: 'xiaoqing.zhang' } },
|
2026-06-24 10:42:50 +08:00
|
|
|
|
createLinkedReimbursementDraftJobForAi: options.createLinkedReimbursementDraftJobForAi,
|
2026-06-22 15:55:59 +08:00
|
|
|
|
fetchExpenseClaimsForAi: options.fetchExpenseClaimsForAi,
|
2026-06-24 10:42:50 +08:00
|
|
|
|
fetchLinkedReimbursementDraftJobForAi: options.fetchLinkedReimbursementDraftJobForAi,
|
|
|
|
|
|
linkedDraftJobPollIntervalMs: options.linkedDraftJobPollIntervalMs ?? 0,
|
|
|
|
|
|
linkedDraftJobMaxPolls: options.linkedDraftJobMaxPolls ?? 2,
|
2026-06-22 15:55:59 +08:00
|
|
|
|
runOrchestratorForAi: options.runOrchestratorForAi,
|
|
|
|
|
|
associationQueryTimeoutMs: options.associationQueryTimeoutMs,
|
|
|
|
|
|
persistCurrentConversation: () => {
|
|
|
|
|
|
persisted += 1
|
|
|
|
|
|
},
|
|
|
|
|
|
pushInlineUserMessage: (text) => {
|
|
|
|
|
|
conversationMessages.value.push(createInlineMessage('user', text))
|
|
|
|
|
|
},
|
|
|
|
|
|
removeWorkbenchDateTag: () => {},
|
|
|
|
|
|
resolveLatestInlineUserPrompt: () => '我要报销',
|
|
|
|
|
|
scrollInlineConversationToBottom: (payload) => {
|
|
|
|
|
|
scrolled.push(payload || {})
|
|
|
|
|
|
},
|
|
|
|
|
|
startAiApplicationPreview: () => {}
|
|
|
|
|
|
})
|
|
|
|
|
|
return { activated, aiExpenseDraft, conversationMessages, flow, get persisted() { return persisted }, scrolled }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const conversationStarted = { value: false }
|
|
|
|
|
|
|
2026-06-24 10:42:50 +08:00
|
|
|
|
test('linked reimbursement draft running text avoids duplicate status wording', () => {
|
|
|
|
|
|
const content = buildLinkedDraftRunningText(
|
|
|
|
|
|
{ message: '正在后台生成报销草稿...' },
|
|
|
|
|
|
'AVF9ST8TT'
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
const repeated = content.match(/正在后台生成报销草稿/g) || []
|
|
|
|
|
|
assert.equal(repeated.length, 1)
|
|
|
|
|
|
assert.doesNotMatch(content, /处理状态:\s*正在后台生成报销草稿/)
|
|
|
|
|
|
assert.match(content, /回来后我会继续查询任务结果/)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-06-22 15:55:59 +08:00
|
|
|
|
test('reimbursement intent checks drafts before recommending approved application documents', async () => {
|
|
|
|
|
|
conversationStarted.value = false
|
|
|
|
|
|
let queried = 0
|
|
|
|
|
|
const { conversationMessages, flow } = buildFlow({
|
|
|
|
|
|
fetchExpenseClaimsForAi: async () => {
|
|
|
|
|
|
queried += 1
|
|
|
|
|
|
return {
|
|
|
|
|
|
items: [
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'app-travel-1',
|
|
|
|
|
|
claim_no: 'AP-202606-001',
|
|
|
|
|
|
employee_name: '张小青',
|
|
|
|
|
|
expense_type: 'travel_application',
|
|
|
|
|
|
reason: '北京客户现场实施',
|
|
|
|
|
|
location: '北京',
|
|
|
|
|
|
status: 'approved',
|
|
|
|
|
|
start_date: '2026-06-23',
|
|
|
|
|
|
end_date: '2026-06-25',
|
|
|
|
|
|
amount: 1650
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 're-linked-1',
|
|
|
|
|
|
claim_no: 'RE-202606-001',
|
|
|
|
|
|
employee_name: '张小青',
|
|
|
|
|
|
expense_type: 'travel',
|
|
|
|
|
|
status: 'submitted',
|
|
|
|
|
|
risk_flags_json: [{
|
|
|
|
|
|
source: 'application_link',
|
|
|
|
|
|
application_claim_no: 'AP-202606-002'
|
|
|
|
|
|
}]
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'app-linked-1',
|
|
|
|
|
|
claim_no: 'AP-202606-002',
|
|
|
|
|
|
employee_name: '张小青',
|
|
|
|
|
|
expense_type: 'travel_application',
|
|
|
|
|
|
reason: '已被关联的申请',
|
|
|
|
|
|
status: 'approved'
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
await flow.startAiReimbursementAssociationGate('我要报销')
|
|
|
|
|
|
|
|
|
|
|
|
assert.equal(queried, 1)
|
|
|
|
|
|
assert.equal(conversationMessages.value[0]?.role, 'user')
|
|
|
|
|
|
assert.equal(conversationMessages.value[0]?.content, '我要报销')
|
|
|
|
|
|
const assistantMessage = conversationMessages.value.at(-1)
|
|
|
|
|
|
assert.equal(assistantMessage.role, 'assistant')
|
|
|
|
|
|
assert.match(assistantMessage.content, /先检查.*报销草稿/)
|
|
|
|
|
|
assert.match(assistantMessage.content, /没有查到可继续的报销草稿/)
|
|
|
|
|
|
assert.match(assistantMessage.content, /先查询.*可关联申请单/)
|
|
|
|
|
|
assert.match(assistantMessage.content, /AP-202606-001/)
|
|
|
|
|
|
assert.doesNotMatch(assistantMessage.content, /AP-202606-002/)
|
|
|
|
|
|
assert.equal(assistantMessage.suggestedActions[0].action_type, 'select_required_application')
|
|
|
|
|
|
assert.equal(assistantMessage.suggestedActions[0].payload.application_claim_no, 'AP-202606-001')
|
|
|
|
|
|
assert.equal(assistantMessage.suggestedActions.at(-1).action_type, 'skip_required_application_link')
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
test('reimbursement intent stops at existing reimbursement drafts before application association', async () => {
|
|
|
|
|
|
conversationStarted.value = false
|
|
|
|
|
|
const { conversationMessages, flow } = buildFlow({
|
|
|
|
|
|
fetchExpenseClaimsForAi: async () => ({
|
|
|
|
|
|
items: [
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'draft-travel-1',
|
|
|
|
|
|
claim_no: 'RE-202606-010',
|
|
|
|
|
|
employee_name: '张小青',
|
|
|
|
|
|
expense_type: 'travel',
|
|
|
|
|
|
reason: '北京客户现场实施报销',
|
|
|
|
|
|
location: '北京',
|
|
|
|
|
|
status: 'draft',
|
2026-06-24 10:42:50 +08:00
|
|
|
|
amount: '0.00',
|
2026-06-22 15:55:59 +08:00
|
|
|
|
created_at: '2026-06-23T10:00:00Z'
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'app-travel-1',
|
|
|
|
|
|
claim_no: 'AP-202606-001',
|
|
|
|
|
|
employee_name: '张小青',
|
|
|
|
|
|
expense_type: 'travel_application',
|
|
|
|
|
|
reason: '北京客户现场实施',
|
|
|
|
|
|
location: '北京',
|
|
|
|
|
|
status: 'approved',
|
|
|
|
|
|
start_date: '2026-06-23',
|
|
|
|
|
|
end_date: '2026-06-25',
|
|
|
|
|
|
amount: 1650
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
await flow.startAiReimbursementAssociationGate('我要报销')
|
|
|
|
|
|
|
|
|
|
|
|
const assistantMessage = conversationMessages.value.at(-1)
|
|
|
|
|
|
assert.match(assistantMessage.content, /先检查.*报销草稿/)
|
|
|
|
|
|
assert.match(assistantMessage.content, /查到 1 个可继续的报销草稿/)
|
|
|
|
|
|
assert.match(assistantMessage.content, /RE-202606-010/)
|
2026-06-24 10:42:50 +08:00
|
|
|
|
assert.match(assistantMessage.content, /待确认/)
|
|
|
|
|
|
assert.doesNotMatch(assistantMessage.content, />0\.00</)
|
|
|
|
|
|
assert.match(assistantMessage.content, /2026-06-23 10:00/)
|
|
|
|
|
|
assert.doesNotMatch(assistantMessage.content, /T10:00:00Z/)
|
2026-06-22 15:55:59 +08:00
|
|
|
|
assert.doesNotMatch(assistantMessage.content, /AP-202606-001/)
|
2026-06-24 10:42:50 +08:00
|
|
|
|
assert.match(assistantMessage.content, /下方三个按钮/)
|
|
|
|
|
|
assert.doesNotMatch(assistantMessage.content, /跳过草稿后再关联申请单/)
|
|
|
|
|
|
assert.equal(assistantMessage.suggestedActions.length, 3)
|
2026-06-22 15:55:59 +08:00
|
|
|
|
assert.equal(assistantMessage.suggestedActions[0].action_type, 'open_application_detail')
|
2026-06-24 10:42:50 +08:00
|
|
|
|
assert.equal(assistantMessage.suggestedActions[0].label, '查看草稿 RE-202606-010')
|
|
|
|
|
|
assert.equal(assistantMessage.suggestedActions[1].action_type, CONTINUE_REIMBURSEMENT_DRAFT_ACTION)
|
|
|
|
|
|
assert.equal(assistantMessage.suggestedActions[1].label, '继续关联草稿 RE-202606-010')
|
|
|
|
|
|
assert.equal(assistantMessage.suggestedActions[2].action_type, CREATE_STANDALONE_REIMBURSEMENT_DRAFT_ACTION)
|
|
|
|
|
|
assert.equal(assistantMessage.suggestedActions[2].label, '独立新建报销单')
|
2026-06-22 15:55:59 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
test('reimbursement association gate shows thinking before querying and renders application cards', async () => {
|
|
|
|
|
|
conversationStarted.value = false
|
|
|
|
|
|
let queried = 0
|
|
|
|
|
|
let resolveClaims = null
|
|
|
|
|
|
const claimsPromise = new Promise((resolve) => {
|
|
|
|
|
|
resolveClaims = resolve
|
|
|
|
|
|
})
|
|
|
|
|
|
const { conversationMessages, flow } = buildFlow({
|
|
|
|
|
|
fetchExpenseClaimsForAi: async () => {
|
|
|
|
|
|
queried += 1
|
|
|
|
|
|
return claimsPromise
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const gatePromise = flow.startAiReimbursementAssociationGate('我要报销')
|
|
|
|
|
|
await Promise.resolve()
|
|
|
|
|
|
|
|
|
|
|
|
assert.equal(queried, 0)
|
|
|
|
|
|
assert.equal(conversationMessages.value[0]?.role, 'user')
|
|
|
|
|
|
const thinkingMessage = conversationMessages.value[1]
|
|
|
|
|
|
assert.equal(thinkingMessage.role, 'assistant')
|
|
|
|
|
|
assert.equal(thinkingMessage.pending, true)
|
|
|
|
|
|
assert.equal(thinkingMessage.stewardPlan.streamStatus, 'streaming')
|
|
|
|
|
|
assert.match(thinkingMessage.stewardPlan.thinkingEvents[0].title, /判断用户意图/)
|
|
|
|
|
|
|
|
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, 360))
|
|
|
|
|
|
assert.equal(queried, 1)
|
|
|
|
|
|
const queryMessage = conversationMessages.value[1]
|
|
|
|
|
|
assert.notEqual(queryMessage, thinkingMessage)
|
|
|
|
|
|
assert.match(queryMessage.stewardPlan.thinkingEvents[1].title, /检查报销草稿/)
|
|
|
|
|
|
resolveClaims({
|
|
|
|
|
|
items: [{
|
|
|
|
|
|
id: 'app-travel-card',
|
|
|
|
|
|
claim_no: 'AP-202606-006',
|
|
|
|
|
|
employee_name: '张小青',
|
|
|
|
|
|
expense_type: 'travel_application',
|
|
|
|
|
|
reason: '北京客户现场实施',
|
|
|
|
|
|
location: '北京',
|
|
|
|
|
|
status: 'approved',
|
|
|
|
|
|
start_date: '2026-06-23',
|
|
|
|
|
|
end_date: '2026-06-25',
|
|
|
|
|
|
amount: 1650
|
|
|
|
|
|
}]
|
|
|
|
|
|
})
|
|
|
|
|
|
await gatePromise
|
|
|
|
|
|
|
|
|
|
|
|
const finalMessage = conversationMessages.value[1]
|
|
|
|
|
|
assert.notEqual(finalMessage, queryMessage)
|
|
|
|
|
|
assert.equal(finalMessage.pending, false)
|
|
|
|
|
|
assert.equal(finalMessage.stewardPlan.streamStatus, 'completed')
|
|
|
|
|
|
assert.match(finalMessage.content, /没有查到可继续的报销草稿/)
|
|
|
|
|
|
assert.match(finalMessage.content, /ai-document-card-list/)
|
|
|
|
|
|
assert.match(finalMessage.content, /ai-document-card--application/)
|
|
|
|
|
|
assert.match(finalMessage.content, /AP-202606-006/)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
test('reimbursement association gate times out stalled claim query and unlocks fallback actions', async () => {
|
|
|
|
|
|
conversationStarted.value = false
|
|
|
|
|
|
const { conversationMessages, flow } = buildFlow({
|
|
|
|
|
|
associationQueryTimeoutMs: 5,
|
|
|
|
|
|
fetchExpenseClaimsForAi: async () => new Promise(() => {})
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const gatePromise = flow.startAiReimbursementAssociationGate('发起报销')
|
|
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, 360))
|
|
|
|
|
|
await gatePromise
|
|
|
|
|
|
|
|
|
|
|
|
const assistantMessage = conversationMessages.value.at(-1)
|
|
|
|
|
|
assert.equal(assistantMessage.pending, false)
|
|
|
|
|
|
assert.equal(assistantMessage.stewardPlan.streamStatus, 'failed')
|
|
|
|
|
|
assert.match(assistantMessage.content, /查询可关联申请单.*超时|查询可关联申请单时出现异常/)
|
|
|
|
|
|
assert.equal(assistantMessage.suggestedActions.at(-1).action_type, 'skip_required_application_link')
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
test('reimbursement association gate matches short username with returned employee email', async () => {
|
|
|
|
|
|
conversationStarted.value = false
|
|
|
|
|
|
const { conversationMessages, flow } = buildFlow({
|
|
|
|
|
|
currentUser: { name: 'caoxiaozhu', username: 'caoxiaozhu' },
|
|
|
|
|
|
fetchExpenseClaimsForAi: async () => ({
|
|
|
|
|
|
items: [
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'app-short-owner',
|
|
|
|
|
|
claim_no: 'AVF9ST8TT',
|
|
|
|
|
|
employee_id: 'emp-caoxiaozhu',
|
|
|
|
|
|
employee_name: '曹笑竹',
|
|
|
|
|
|
employee_email: 'caoxiaozhu@xf.com',
|
|
|
|
|
|
employee_no: 'E90919',
|
|
|
|
|
|
expense_type: 'travel_application',
|
|
|
|
|
|
reason: '参加相关残联会议',
|
|
|
|
|
|
location: '上海',
|
|
|
|
|
|
status: 'approved',
|
|
|
|
|
|
amount: 1200
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
await flow.startAiReimbursementAssociationGate('发起报销')
|
|
|
|
|
|
|
|
|
|
|
|
const assistantMessage = conversationMessages.value.at(-1)
|
|
|
|
|
|
assert.match(assistantMessage.content, /AVF9ST8TT/)
|
|
|
|
|
|
assert.equal(assistantMessage.suggestedActions[0].action_type, 'select_required_application')
|
|
|
|
|
|
assert.equal(assistantMessage.suggestedActions[0].payload.application_claim_no, 'AVF9ST8TT')
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
test('linked application selection can create reimbursement draft from association gate', async () => {
|
|
|
|
|
|
conversationStarted.value = false
|
|
|
|
|
|
const orchestratorCalls = []
|
2026-06-24 10:42:50 +08:00
|
|
|
|
const createJobCalls = []
|
|
|
|
|
|
const fetchJobCalls = []
|
2026-06-22 15:55:59 +08:00
|
|
|
|
const { aiExpenseDraft, conversationMessages, flow } = buildFlow({
|
|
|
|
|
|
fetchExpenseClaimsForAi: async () => ({ items: [] }),
|
2026-06-24 10:42:50 +08:00
|
|
|
|
createLinkedReimbursementDraftJobForAi: async (payload) => {
|
|
|
|
|
|
createJobCalls.push(payload)
|
|
|
|
|
|
return {
|
|
|
|
|
|
job_id: 'linked-draft-job-1',
|
|
|
|
|
|
status: 'queued',
|
|
|
|
|
|
message: '已创建后台生成任务。'
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
fetchLinkedReimbursementDraftJobForAi: async (jobId) => {
|
|
|
|
|
|
fetchJobCalls.push(jobId)
|
|
|
|
|
|
return {
|
|
|
|
|
|
job_id: jobId,
|
|
|
|
|
|
status: 'succeeded',
|
|
|
|
|
|
message: '报销草稿已生成。',
|
|
|
|
|
|
draft_payload: {
|
|
|
|
|
|
claim_id: 'draft-linked-1',
|
|
|
|
|
|
claim_no: 'RE-202606-009',
|
|
|
|
|
|
status: 'draft',
|
|
|
|
|
|
expense_type: 'travel',
|
|
|
|
|
|
reason: '北京客户现场实施'
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
2026-06-22 15:55:59 +08:00
|
|
|
|
runOrchestratorForAi: async (payload, options) => {
|
|
|
|
|
|
orchestratorCalls.push({ payload, options })
|
|
|
|
|
|
return {
|
|
|
|
|
|
status: 'succeeded',
|
|
|
|
|
|
conversation_id: 'conv-linked-draft',
|
|
|
|
|
|
result: {
|
|
|
|
|
|
draft_payload: {
|
|
|
|
|
|
claim_id: 'draft-linked-1',
|
|
|
|
|
|
claim_no: 'RE-202606-009',
|
|
|
|
|
|
status: 'draft',
|
|
|
|
|
|
expense_type: 'travel',
|
|
|
|
|
|
reason: '北京客户现场实施'
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
await flow.linkAiExpenseApplication({
|
|
|
|
|
|
application_claim_no: 'AP-202606-001',
|
|
|
|
|
|
application_expense_type: 'travel_application',
|
|
|
|
|
|
application_reason: '北京客户现场实施',
|
|
|
|
|
|
application_location: '北京',
|
|
|
|
|
|
application_business_time: '2026-06-23 至 2026-06-25',
|
|
|
|
|
|
application_amount_label: '1,650元'
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-06-24 10:42:50 +08:00
|
|
|
|
assert.equal(orchestratorCalls.length, 0)
|
|
|
|
|
|
assert.equal(createJobCalls.length, 1)
|
|
|
|
|
|
assert.equal(createJobCalls[0].context_json.review_action, 'save_draft')
|
|
|
|
|
|
assert.equal(createJobCalls[0].context_json.expense_scene_selection.application_claim_no, 'AP-202606-001')
|
|
|
|
|
|
assert.equal(createJobCalls[0].context_json.review_form_values.application_claim_no, 'AP-202606-001')
|
|
|
|
|
|
assert.deepEqual(fetchJobCalls, ['linked-draft-job-1'])
|
2026-06-22 15:55:59 +08:00
|
|
|
|
assert.equal(aiExpenseDraft.value, null)
|
|
|
|
|
|
assert.match(conversationMessages.value.at(-1).content, /报销草稿 RE-202606-009 已生成/)
|
|
|
|
|
|
assert.equal(conversationMessages.value.at(-1).draftPayload.claim_no, 'RE-202606-009')
|
|
|
|
|
|
assert.equal(conversationMessages.value.at(-1).suggestedActions[0].action_type, 'open_application_detail')
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-06-24 10:42:50 +08:00
|
|
|
|
test('linked reimbursement draft job resumes from pending history message', async () => {
|
|
|
|
|
|
conversationStarted.value = true
|
|
|
|
|
|
const fetchJobCalls = []
|
|
|
|
|
|
const { conversationMessages, flow } = buildFlow({
|
|
|
|
|
|
fetchLinkedReimbursementDraftJobForAi: async (jobId) => {
|
|
|
|
|
|
fetchJobCalls.push(jobId)
|
|
|
|
|
|
return {
|
|
|
|
|
|
job_id: jobId,
|
|
|
|
|
|
status: 'succeeded',
|
|
|
|
|
|
message: '报销草稿已生成。',
|
|
|
|
|
|
draft_payload: {
|
|
|
|
|
|
claim_id: 'draft-resumed-1',
|
|
|
|
|
|
claim_no: 'RE-202606-011',
|
|
|
|
|
|
status: 'draft',
|
|
|
|
|
|
expense_type: 'travel'
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
conversationMessages.value.push(createInlineMessage('assistant', '已关联申请单 AP-202606-001,正在后台生成报销草稿...', {
|
|
|
|
|
|
id: 'pending-linked-draft-message',
|
|
|
|
|
|
pending: true,
|
|
|
|
|
|
linkedReimbursementDraftJob: {
|
|
|
|
|
|
jobId: 'linked-draft-job-resume',
|
|
|
|
|
|
status: 'queued',
|
|
|
|
|
|
applicationClaimNo: 'AP-202606-001'
|
|
|
|
|
|
}
|
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
|
|
flow.resumePendingLinkedReimbursementDraftJobs()
|
|
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, 5))
|
|
|
|
|
|
|
|
|
|
|
|
assert.deepEqual(fetchJobCalls, ['linked-draft-job-resume'])
|
|
|
|
|
|
assert.match(conversationMessages.value.at(-1).content, /报销草稿 RE-202606-011 已生成/)
|
|
|
|
|
|
assert.match(conversationMessages.value.at(-1).content, /AP-202606-001/)
|
|
|
|
|
|
assert.equal(conversationMessages.value.at(-1).pending, false)
|
|
|
|
|
|
assert.equal(conversationMessages.value.at(-1).draftPayload.claim_no, 'RE-202606-011')
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
test('continuing an existing reimbursement draft prompts for attachments or description', () => {
|
|
|
|
|
|
conversationStarted.value = false
|
|
|
|
|
|
const { conversationMessages, flow } = buildFlow()
|
|
|
|
|
|
|
|
|
|
|
|
flow.promptAiReimbursementDraftContinuation({
|
|
|
|
|
|
claim_id: 'draft-travel-1',
|
|
|
|
|
|
claim_no: 'RE-202606-010',
|
|
|
|
|
|
original_message: '我要报销'
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
assert.equal(conversationMessages.value.at(-2).role, 'user')
|
|
|
|
|
|
assert.equal(conversationMessages.value.at(-2).content, '继续关联草稿 RE-202606-010')
|
|
|
|
|
|
const assistantMessage = conversationMessages.value.at(-1)
|
|
|
|
|
|
assert.equal(assistantMessage.role, 'assistant')
|
|
|
|
|
|
assert.match(assistantMessage.content, /请上传相关的附件/)
|
|
|
|
|
|
assert.match(assistantMessage.content, /补充说明/)
|
|
|
|
|
|
assert.equal(assistantMessage.suggestedActions[0].action_type, 'open_application_detail')
|
|
|
|
|
|
assert.equal(assistantMessage.suggestedActions[0].label, '查看草稿 RE-202606-010')
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
test('standalone reimbursement draft branch asks before creating a new draft', () => {
|
|
|
|
|
|
conversationStarted.value = false
|
|
|
|
|
|
const { conversationMessages, flow } = buildFlow()
|
|
|
|
|
|
|
|
|
|
|
|
flow.promptStandaloneReimbursementDraftCreation('我要报销', '独立新建报销单')
|
|
|
|
|
|
|
|
|
|
|
|
assert.equal(conversationMessages.value.at(-2).role, 'user')
|
|
|
|
|
|
assert.equal(conversationMessages.value.at(-2).content, '独立新建报销单')
|
|
|
|
|
|
const assistantMessage = conversationMessages.value.at(-1)
|
|
|
|
|
|
assert.equal(assistantMessage.role, 'assistant')
|
|
|
|
|
|
assert.match(assistantMessage.content, /是否新建草稿单据/)
|
|
|
|
|
|
assert.equal(assistantMessage.suggestedActions[0].label, '新建草稿单据')
|
|
|
|
|
|
assert.equal(assistantMessage.suggestedActions[0].action_type, 'skip_required_application_link')
|
|
|
|
|
|
assert.equal(assistantMessage.suggestedActions[1].label, '暂不新建')
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-06-22 15:55:59 +08:00
|
|
|
|
test('personal workbench routes reimbursement creation intent to association gate before steward', () => {
|
2026-06-23 11:21:18 +08:00
|
|
|
|
assert.match(
|
|
|
|
|
|
personalWorkbenchAiMode,
|
|
|
|
|
|
/import \{ isReimbursementCreationIntent \} from '\.\/workbenchAiApplicationGateModel\.js'/
|
|
|
|
|
|
)
|
2026-06-22 15:55:59 +08:00
|
|
|
|
const startConversationIndex = personalWorkbenchAiMode.indexOf('function startInlineConversation')
|
|
|
|
|
|
const gateIndex = personalWorkbenchAiMode.indexOf('expenseFlow.startAiReimbursementAssociationGate(cleanPrompt', startConversationIndex)
|
|
|
|
|
|
const stewardIndex = personalWorkbenchAiMode.indexOf('stewardFlow.requestInlineAssistantReply(cleanPrompt', startConversationIndex)
|
|
|
|
|
|
|
|
|
|
|
|
assert.ok(startConversationIndex >= 0)
|
|
|
|
|
|
assert.ok(gateIndex > startConversationIndex)
|
|
|
|
|
|
assert.ok(stewardIndex > gateIndex)
|
|
|
|
|
|
assert.match(personalWorkbenchAiMode, /function runAiModeAction\(item\)[\s\S]*expenseFlow\.startAiReimbursementAssociationGate\(item\.prompt, item\.label\)/)
|
|
|
|
|
|
})
|