import assert from 'node:assert/strict' import { readFileSync } from 'node:fs' import test from 'node:test' import { fileURLToPath } from 'node:url' import { ATTACHMENT_ASSOCIATION_CONFIRM_HREF, buildAttachmentAssociationConfirmationMessage, buildOcrFilePreviews, buildReviewFilePreviewsFromReviewPayload } from '../src/views/scripts/travelReimbursementAttachmentModel.js' import { buildDraftAssociationQueryPayload, buildExpenseQueryHint, EXPENSE_CENTER_HREF, normalizeExpenseQueryPayload } from '../src/views/scripts/travelReimbursementExpenseQueryModel.js' import { renderMarkdown } from '../src/utils/markdown.js' test('attachment association prompt prints recognized receipt details before confirmation link', () => { const message = buildAttachmentAssociationConfirmationMessage({ claimNo: 'EXP-202605-001', fileNames: ['train-ticket.pdf'], ocrDocuments: [ { filename: 'train-ticket.pdf', document_type: 'train_ticket', scene_label: '差旅票据', summary: '铁路电子客票 武汉-上海 票价 354 元', document_fields: [ { key: 'route', label: '行程', value: '武汉-上海' }, { key: 'amount', label: '票价', value: '354.00' }, { key: 'date', label: '乘车日期', value: '2026-02-20' } ] } ] }) assert.match(message, /已识别附件信息:/) assert.match(message, /> \*\*附件 1:train-ticket\.pdf\*\*/) assert.match(message, /附件类型:差旅票据/) assert.match(message, /行程:武汉-上海/) assert.match(message, /票价:354.00/) assert.match(message, /草稿单号:EXP-202605-001/) assert.match(message, new RegExp(`\\n\\n\\n如果 \\*\\*\\[确认\\]\\(${ATTACHMENT_ASSOCIATION_CONFIRM_HREF}\\)\\*\\* 该信息`)) const rendered = renderMarkdown(message) assert.match(rendered, /
/) const questionIndex = rendered.indexOf('请问是否确定将票据信息归集到单据') const attachmentCardCloseIndex = rendered.indexOf('
') assert.ok(attachmentCardCloseIndex > -1 && questionIndex > attachmentCardCloseIndex) const attachmentCardHtml = rendered.slice( rendered.indexOf('
'), attachmentCardCloseIndex ) assert.doesNotMatch(attachmentCardHtml, /请问是否确定将票据信息归集到单据/) assert.match(rendered, /

/) assert.match(rendered, /确认<\/a><\/strong>/) }) test('multiple recognized attachments render as separated attachment cards', () => { const message = buildAttachmentAssociationConfirmationMessage({ claimNo: 'EXP-202605-001', ocrDocuments: [ { filename: '2月20 武汉-上海.pdf', document_type: 'train_ticket', document_type_label: '火车/高铁票', document_fields: [ { key: 'amount', label: '金额', value: '354元' }, { key: 'route', label: '行程', value: '武汉-上海' } ] }, { filename: '2月23 上海-武汉.pdf', document_type: 'train_ticket', document_type_label: '火车/高铁票', document_fields: [ { key: 'amount', label: '金额', value: '354元' }, { key: 'route', label: '行程', value: '上海-武汉' } ] } ] }) const rendered = renderMarkdown(message) assert.equal((rendered.match(/class="markdown-attachment-card"/g) || []).length, 2) assert.match(rendered, /附件 1:2月20 武汉-上海\.pdf<\/strong>/) assert.match(rendered, /附件 2:2月23 上海-武汉\.pdf<\/strong>/) assert.match(rendered, /本次待归集附件:2 份/) }) test('attachment upload association uses conversation selection instead of legacy modal', () => { const viewSource = readFileSync( fileURLToPath(new URL('../src/views/TravelReimbursementCreateView.vue', import.meta.url)), 'utf8' ) const submitComposerSource = readFileSync( fileURLToPath(new URL('../src/views/scripts/useTravelReimbursementSubmitComposer.js', import.meta.url)), 'utf8' ) assert.doesNotMatch(viewSource, /检测到你已有单据事件|uploadDecisionDialogOpen|continueExistingUpload|createNewUploadDocument/) assert.doesNotMatch(submitComposerSource, /uploadDecisionDialogOpen|hasExistingDocumentEvent|skipUploadDecisionPrompt/) assert.doesNotMatch(submitComposerSource, /查询可关联草稿失败,已继续按新单据识别/) assert.match( submitComposerSource, /const claims = await fetchExpenseClaims\(\)[\s\S]*const queryPayload = buildDraftAssociationQueryPayload\(claims\)[\s\S]*meta: \['等待选择关联单据'\][\s\S]*queryPayload/ ) assert.match(submitComposerSource, /meta: \['单据查询失败'\][\s\S]*return null/) assert.match( submitComposerSource, /files\.length[\s\S]*!resolvedUploadDisposition[\s\S]*!options\.skipDraftAssociationPrompt[\s\S]*!reviewAction/ ) }) test('OCR preview builders keep hotel receipt image previews when preview kind is omitted', () => { const dataUrl = 'data:image/png;base64,abc123' const ocrPreviews = buildOcrFilePreviews({ documents: [ { filename: 'hotel.png', preview_data_url: dataUrl } ] }) const reviewPreviews = buildReviewFilePreviewsFromReviewPayload({ document_cards: [ { filename: 'hotel.png', preview_url: dataUrl } ] }) assert.deepEqual(ocrPreviews, [{ filename: 'hotel.png', kind: 'image', url: dataUrl }]) assert.deepEqual(reviewPreviews, [{ filename: 'hotel.png', kind: 'image', url: dataUrl }]) }) test('draft association query keeps a single candidate selectable in the conversation', () => { const payload = buildDraftAssociationQueryPayload([ { id: 'claim-1', claim_no: 'EXP-202605-001', status: 'draft', expense_type: 'travel', reason: '上海出差', amount: 1280 } ]) assert.equal(payload.selectionMode, 'draft_association') assert.equal(payload.title, '选择关联草稿') assert.equal(payload.records.length, 1) assert.equal(payload.records[0].claimId, 'claim-1') }) test('expense query payload keeps structured risk items for claim-level risk drilldown', () => { const payload = normalizeExpenseQueryPayload({ result_type: 'expense_claim_list', records: [ { claim_id: 'claim-risk', claim_no: 'EXP-202605-009', amount: 880, risk_flags: [ { key: 'hotel-limit', level: 'high', level_label: '高风险', title: '酒店超标', summary: '住宿金额超过城市标准', detail: '上海 P5 住宿标准为 600 元,本次 880 元。' } ] } ] }) assert.equal(payload.records[0].riskItems.length, 1) assert.equal(payload.records[0].riskItems[0].levelLabel, '高风险') assert.equal(payload.records[0].riskItems[0].summary, '住宿金额超过城市标准') }) test('expense query hint guides users to the reimbursement center after the top five results', () => { const payload = normalizeExpenseQueryPayload({ result_type: 'expense_claim_list', title: '最近 5 条你的归档报销单', scope_label: '你的归档报销单', record_count: 8, preview_count: 5, preview_limit: 5, records: [ { claim_id: 'claim-1', claim_no: 'EXP-1', amount: 100 } ] }) const hint = buildExpenseQueryHint(payload) assert.match(hint, /最近的 5 条记录/) assert.match(hint, new RegExp(`\\[\\*\\*这里\\*\\*\\]\\(${EXPENSE_CENTER_HREF}\\)`)) })