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 } })