import assert from 'node:assert/strict' import { readFileSync } from 'node:fs' import test from 'node:test' import { fileURLToPath } from 'node:url' import { buildAiAdviceViewModel, buildAttachmentInsightViewModel, buildAttachmentRiskCards } from '../src/views/scripts/travelRequestDetailInsights.js' const detailViewTemplate = readFileSync( fileURLToPath(new URL('../src/views/TravelRequestDetailView.vue', import.meta.url)), 'utf8' ) const detailViewScript = readFileSync( fileURLToPath(new URL('../src/views/scripts/TravelRequestDetailView.js', import.meta.url)), 'utf8' ) const requestsComposableScript = readFileSync( fileURLToPath(new URL('../src/composables/useRequests.js', import.meta.url)), 'utf8' ) const approvalCenterTemplate = readFileSync( fileURLToPath(new URL('../src/views/ApprovalCenterView.vue', import.meta.url)), 'utf8' ) const approvalCenterScript = readFileSync( fileURLToPath(new URL('../src/views/scripts/ApprovalCenterView.js', import.meta.url)), 'utf8' ) const returnReasonDialog = readFileSync( fileURLToPath(new URL('../src/components/shared/ReturnReasonDialog.vue', import.meta.url)), 'utf8' ) const attachmentMeta = { file_name: 'taxi-invoice.pdf', media_type: 'application/pdf', previewable: true, document_info: { document_type: 'taxi_receipt', document_type_label: '出租车/网约车票据', fields: [ { label: '金额', value: '121.54' }, { label: '日期', value: '2026-03-04' } ] }, requirement_check: { matches: false, message: '附件类型与当前费用项目不匹配。' }, analysis: { severity: 'high', label: '高风险', headline: '票据类型不匹配', summary: '交通票据挂在办公费明细下。', points: ['票据识别为出租车/网约车票据', '当前费用项目为办公费'], suggestion: '把费用项目调整为交通费,或更换为办公用品票据。' } } test('attachment insight exposes recognition fields and rule basis', () => { const insight = buildAttachmentInsightViewModel(attachmentMeta, { name: '办公费', itemType: 'office' }) assert.equal(insight.documentTypeLabel, '出租车/网约车票据') assert.equal(insight.requirementLabel, '不符合当前费用类型') assert.deepEqual(insight.fields, ['金额:121.54', '日期:2026-03-04']) assert.ok(insight.ruleBasis.some((item) => item.includes('附件类型与当前费用项目不匹配'))) }) test('AI advice card splits every attachment risk point with basis and suggestion', () => { const riskCards = buildAttachmentRiskCards({ expenseItems: [ { id: 'item-1', name: '办公费', invoiceId: 'taxi-invoice.pdf' } ], attachmentMetaByItemId: { 'item-1': attachmentMeta } }) const advice = buildAiAdviceViewModel({ completionItems: [], riskCards }) assert.equal(riskCards.length, 2) assert.equal(advice.badge, '优先整改') assert.equal(advice.riskCards.length, 2) assert.ok(advice.riskCards.every((card) => card.ruleBasis.length > 0)) assert.ok(advice.riskCards.every((card) => card.suggestion.includes('费用项目调整为交通费'))) }) test('AI advice shows only the latest manual return while preserving return count context', () => { const riskCards = buildAttachmentRiskCards({ claimRiskFlags: [ { source: 'manual_return', severity: 'medium', label: '人工退回', message: '第一次退回:缺少附件。', reason: '缺少附件。', return_count: 1, return_stage: '直属领导审批', risk_points: ['附件缺失或不清晰'] }, { source: 'manual_return', severity: 'medium', label: '人工退回', message: '第二次退回:超标说明不完整。', reason: '超标说明不完整。', return_count: 2, return_stage: '财务审批', risk_points: ['超出制度标准或缺少超标说明'] } ] }) assert.equal(riskCards.length, 1) assert.equal(riskCards[0].risk, '第二次退回:超标说明不完整。') assert.ok(riskCards[0].ruleBasis.some((item) => item.includes('累计退回 2 次'))) assert.ok(riskCards[0].ruleBasis.some((item) => item.includes('财务审批'))) }) test('expense attachment actions keep preview as the only recognition entry point', () => { assert.match(detailViewTemplate, /:aria-label="resolveAttachmentPreviewTitle\(item\)"/) assert.match(detailViewScript, /return fileName \? `预览附件:\$\{fileName\}` : '预览附件'/) assert.doesNotMatch(detailViewTemplate, /aria-label="识别附件"/) assert.doesNotMatch(detailViewTemplate, /点击识别按钮/) assert.doesNotMatch(detailViewScript, /recognizeExpenseAttachment/) assert.doesNotMatch(detailViewScript, /recognizingExpenseId/) }) test('expense detail table omits compact-breaking summary labels', () => { assert.match(detailViewTemplate, /