Files
X-Financial/web/tests/workbench-ai-action-router.test.mjs

392 lines
13 KiB
JavaScript
Raw Normal View History

import assert from 'node:assert/strict'
import test from 'node:test'
import { buildInlineApplicationPreview } from '../src/composables/workbenchAiMode/workbenchAiApplicationPreviewModel.js'
import { buildStewardSuggestedActions } from '../src/views/scripts/stewardPlanModel.js'
import { useWorkbenchAiActionRouter } from '../src/composables/workbenchAiMode/useWorkbenchAiActionRouter.js'
import {
CONTINUE_REIMBURSEMENT_DRAFT_ACTION,
CREATE_STANDALONE_REIMBURSEMENT_DRAFT_ACTION
} from '../src/views/scripts/travelReimbursementAssociationGateModel.js'
test('workbench steward application confirmation opens inline application preview directly', () => {
const [action] = buildStewardSuggestedActions({
plan_id: 'steward-plan-ready-application',
plan_status: 'ready',
tasks: [
{
task_id: 'task-application-beijing',
task_type: 'expense_application',
title: '费用申请 2026-06-23 北京',
summary: '明天前往北京出差3天支撑客户现场实施。',
assigned_agent: 'application_assistant',
ontology_fields: {
expense_type: 'travel',
time_range: '2026-06-23 至 2026-06-25',
location: '北京',
days: '3天',
reason: '支撑客户现场实施'
},
missing_fields: ['transport_mode']
}
],
confirmation_groups: [
{
confirmation_id: 'confirm-application-beijing',
action_type: 'confirm_create_application',
target_task_id: 'task-application-beijing'
}
]
})
let previewPayload = null
let fallbackConversationStarted = false
const router = useWorkbenchAiActionRouter({
aiExpenseDraft: { value: null },
applicationFlow: {
isInlineSuggestedActionDisabled: () => false,
executeInlineApplicationPreviewAction: () => {}
},
assistantDraft: { value: '' },
attachmentFlow: {
confirmAiAttachmentAssociation: () => {}
},
emit: () => {},
expenseFlow: {
linkAiExpenseApplication: () => {},
pushInlineExpenseSceneSelectionPrompt: () => {},
startAiApplicationPreviewFromAction: (payload) => {
previewPayload = payload
},
startAiExpenseDraft: () => {}
},
focusAiModeInput: () => {},
hasInlineAttachmentOcrDetails: () => false,
resolveLatestInlineUserPrompt: () => '',
selectedFiles: { value: [] },
startInlineConversation: () => {
fallbackConversationStarted = true
},
toast: () => {},
toggleInlineAttachmentOcrDetails: () => {}
})
router.handleInlineSuggestedAction(action)
assert.equal(fallbackConversationStarted, false)
assert.equal(previewPayload?.flow_id, 'travel_application')
assert.equal(previewPayload?.expense_type, 'travel')
assert.equal(previewPayload?.expense_type_label, '差旅费')
assert.match(previewPayload?.carry_text || '', /支撑客户现场实施/)
const preview = buildInlineApplicationPreview(previewPayload.expense_type_label, previewPayload.carry_text, {
name: '测试用户',
departmentName: '交付部',
position: '实施顾问',
managerName: '张经理',
grade: 'P5'
})
assert.equal(preview.fields.time, '2026-06-23 至 2026-06-25')
assert.equal(preview.fields.location, '北京')
assert.equal(preview.fields.reason, '支撑客户现场实施')
assert.equal(preview.fields.days, '3天')
assert.equal(preview.fields.transportMode, '')
})
test('workbench reimbursement skip link action opens new reimbursement flow', () => {
let sceneSelectionPayload = null
let fallbackConversationStarted = false
const router = useWorkbenchAiActionRouter({
aiExpenseDraft: { value: null },
applicationFlow: {
isInlineSuggestedActionDisabled: () => false,
executeInlineApplicationPreviewAction: () => {}
},
assistantDraft: { value: '' },
attachmentFlow: {
confirmAiAttachmentAssociation: () => {}
},
emit: () => {},
expenseFlow: {
linkAiExpenseApplication: () => {},
pushInlineExpenseSceneSelectionPrompt: (sourceText, label) => {
sceneSelectionPayload = { sourceText, label }
},
startAiApplicationPreviewFromAction: () => {},
startAiExpenseDraft: () => {}
},
focusAiModeInput: () => {},
hasInlineAttachmentOcrDetails: () => false,
resolveLatestInlineUserPrompt: () => '',
selectedFiles: { value: [] },
startInlineConversation: () => {
fallbackConversationStarted = true
},
toast: () => {},
toggleInlineAttachmentOcrDetails: () => {}
})
router.handleInlineSuggestedAction({
label: '不关联,单独新建报销单',
action_type: 'skip_required_application_link',
payload: {
original_message: '我要报销'
}
})
assert.equal(fallbackConversationStarted, false)
assert.deepEqual(sceneSelectionPayload, {
sourceText: '我要报销',
label: '不关联,单独新建报销单'
})
})
test('workbench draft continuation action asks for attachments or description', () => {
let continuationPayload = null
let fallbackConversationStarted = false
const router = useWorkbenchAiActionRouter({
aiExpenseDraft: { value: null },
applicationFlow: {
isInlineSuggestedActionDisabled: () => false,
executeInlineApplicationPreviewAction: () => {}
},
assistantDraft: { value: '' },
attachmentFlow: {
confirmAiAttachmentAssociation: () => {}
},
emit: () => {},
expenseFlow: {
linkAiExpenseApplication: () => {},
promptAiReimbursementDraftContinuation: (payload) => {
continuationPayload = payload
},
promptStandaloneReimbursementDraftCreation: () => {},
pushInlineExpenseSceneSelectionPrompt: () => {},
startAiApplicationPreviewFromAction: () => {},
startAiExpenseDraft: () => {},
startAiReimbursementAssociationGate: () => {}
},
focusAiModeInput: () => {},
hasInlineAttachmentOcrDetails: () => false,
resolveLatestInlineUserPrompt: () => '',
selectedFiles: { value: [] },
startInlineConversation: () => {
fallbackConversationStarted = true
},
toast: () => {},
toggleInlineAttachmentOcrDetails: () => {}
})
router.handleInlineSuggestedAction({
label: '继续关联草稿 RE-202606-010',
action_type: CONTINUE_REIMBURSEMENT_DRAFT_ACTION,
payload: {
claim_id: 'draft-travel-1',
claim_no: 'RE-202606-010',
original_message: '我要报销'
}
})
assert.equal(fallbackConversationStarted, false)
assert.deepEqual(continuationPayload, {
claim_id: 'draft-travel-1',
claim_no: 'RE-202606-010',
original_message: '我要报销'
})
})
test('workbench standalone draft action asks before creating a new reimbursement draft', () => {
let standalonePrompt = null
let fallbackConversationStarted = false
const router = useWorkbenchAiActionRouter({
aiExpenseDraft: { value: null },
applicationFlow: {
isInlineSuggestedActionDisabled: () => false,
executeInlineApplicationPreviewAction: () => {}
},
assistantDraft: { value: '' },
attachmentFlow: {
confirmAiAttachmentAssociation: () => {}
},
emit: () => {},
expenseFlow: {
linkAiExpenseApplication: () => {},
promptAiReimbursementDraftContinuation: () => {},
promptStandaloneReimbursementDraftCreation: (sourceText, label) => {
standalonePrompt = { sourceText, label }
},
pushInlineExpenseSceneSelectionPrompt: () => {},
startAiApplicationPreviewFromAction: () => {},
startAiExpenseDraft: () => {},
startAiReimbursementAssociationGate: () => {}
},
focusAiModeInput: () => {},
hasInlineAttachmentOcrDetails: () => false,
resolveLatestInlineUserPrompt: () => '',
selectedFiles: { value: [] },
startInlineConversation: () => {
fallbackConversationStarted = true
},
toast: () => {},
toggleInlineAttachmentOcrDetails: () => {}
})
router.handleInlineSuggestedAction({
label: '独立新建报销单',
action_type: CREATE_STANDALONE_REIMBURSEMENT_DRAFT_ACTION,
payload: {
original_message: '我要报销'
}
})
assert.equal(fallbackConversationStarted, false)
assert.deepEqual(standalonePrompt, {
sourceText: '我要报销',
label: '独立新建报销单'
})
})
test('workbench steward executable submit action runs precheck before submit and writes result message', async () => {
const requests = []
const originalFetch = globalThis.fetch
globalThis.fetch = async (_url, options = {}) => {
const body = JSON.parse(String(options.body || '{}'))
requests.push(body)
if (body.action_type === 'run_duplicate_precheck') {
return {
ok: true,
async json() {
return {
action_type: 'run_duplicate_precheck',
status: 'succeeded',
message: '未发现重复或冲突申请,可以继续提交。',
result_payload: {
status: 'ok',
blocking: false
}
}
}
}
}
return {
ok: true,
async json() {
return {
action_type: 'submit_application',
status: 'succeeded',
message: '申请已提交审批。',
result_payload: {
draft_payload: {
claim_id: 'claim-app-1',
claim_no: 'A1BCDEF2'
}
}
}
}
}
}
try {
const messages = []
let messageSeq = 0
const createInlineMessage = (role, content, options = {}) => ({
id: options.id || `msg-${++messageSeq}`,
role,
content,
pending: Boolean(options.pending),
suggestedActions: Array.isArray(options.suggestedActions) ? options.suggestedActions : []
})
const replaceInlineMessage = (id, nextMessage) => {
const index = messages.findIndex((item) => item.id === id)
if (index >= 0) {
messages.splice(index, 1, nextMessage)
}
}
let persisted = false
const router = useWorkbenchAiActionRouter({
aiExpenseDraft: { value: null },
applicationFlow: {
isInlineSuggestedActionDisabled: () => false,
executeInlineApplicationPreviewAction: () => {}
},
assistantDraft: { value: '' },
attachmentFlow: {
confirmAiAttachmentAssociation: () => {}
},
conversationMessages: { value: messages },
createInlineMessage,
emit: () => {},
expenseFlow: {
linkAiExpenseApplication: () => {},
pushInlineExpenseSceneSelectionPrompt: () => {},
startAiApplicationPreviewFromAction: () => {},
startAiExpenseDraft: () => {}
},
focusAiModeInput: () => {},
hasInlineAttachmentOcrDetails: () => false,
persistCurrentConversation: () => {
persisted = true
},
replaceInlineMessage,
resolveLatestInlineUserPrompt: () => '2026-02-20 至 2026-02-23去上海出差交通火车直接提交',
scrollInlineConversationToBottom: () => {},
selectedFiles: { value: [] },
startInlineConversation: () => {},
toast: () => {},
toggleInlineAttachmentOcrDetails: () => {}
})
const sourceMessage = {
suggestedActionsLocked: false
}
await router.handleInlineSuggestedAction({
label: '确认提交申请',
action_type: 'switch_session',
payload: {
steward_execute_action: true,
steward_plan_id: 'plan-submit-1',
steward_action_type: 'submit_application',
steward_action_requires_confirmation: true,
steward_action_step: {
step_id: 'task-app-1:05',
action_type: 'submit_application',
requires_confirmation: true
},
steward_current_task: {
task_id: 'task-app-1',
task_type: 'expense_application',
assigned_agent: 'application_assistant',
title: '上海出差申请',
summary: '2026-02-20 至 2026-02-23 去上海出差,交通火车。',
requested_action: 'submit',
ontology_fields: {
expense_type: 'travel',
time_range: '2026-02-20 至 2026-02-23',
location: '上海',
reason: '辅助国网仿生产服务器部署',
transport_mode: 'train'
},
missing_fields: [],
action_steps: [
{ step_id: 'task-app-1:04', action_type: 'run_duplicate_precheck' },
{ step_id: 'task-app-1:05', action_type: 'submit_application', requires_confirmation: true }
]
},
carry_text: '2026-02-20 至 2026-02-23去上海出差交通火车直接提交'
}
}, sourceMessage)
assert.equal(requests.length, 2)
assert.equal(requests[0].action_type, 'run_duplicate_precheck')
assert.equal(requests[1].action_type, 'submit_application')
assert.equal(requests[1].confirmed, true)
assert.equal(requests[1].context_json.precheck_result.status, 'ok')
assert.equal(sourceMessage.suggestedActionsLocked, true)
assert.equal(persisted, true)
assert.match(messages.at(-1).content, /申请已提交审批/)
assert.equal(messages.at(-1).suggestedActions[0].action_type, 'open_application_detail')
} finally {
globalThis.fetch = originalFetch
}
})