Files
X-Financial/web/tests/ai-attachment-association-model.test.mjs
2026-06-22 11:58:53 +08:00

282 lines
9.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import test from 'node:test'
import assert from 'node:assert/strict'
import {
buildAiAttachmentAssociationActions,
buildAiAttachmentAssociationMessage,
buildAiAttachmentAssociationResultMessage,
resolveAiAttachmentAssociationMatch
} from '../src/utils/aiAttachmentAssociationModel.js'
import { renderAiConversationHtml } from '../src/utils/aiConversationHtmlRenderer.js'
function findAction(actions, actionType) {
return actions.find((action) => action.action_type === actionType)
}
test('根据票据日期和行程自动匹配最可能关联的报销单', () => {
const claims = [
{
id: 'claim-wuhan-shanghai',
claim_no: 'BX-20260220-001',
expense_type: 'travel',
status: 'draft',
reason: '辅助国网仿生产服务器部署,武汉往返上海',
location: '上海',
occurred_at: '2026-02-20'
},
{
id: 'claim-nanjing',
claim_no: 'BX-20260301-002',
expense_type: 'travel',
status: 'draft',
reason: '南京客户拜访',
location: '南京',
occurred_at: '2026-03-01'
}
]
const ocrDocuments = [
{
filename: '2月20 武汉-上海.pdf',
summary: '武汉至上海高铁票',
document_fields: [
{ key: 'date', label: '日期', value: '2026-02-20' },
{ key: 'route', label: '行程', value: '武汉-上海' }
]
},
{
filename: '2月23 上海-武汉.pdf',
summary: '上海至武汉高铁票',
document_fields: [
{ key: 'date', label: '日期', value: '2026-02-23' },
{ key: 'route', label: '行程', value: '上海-武汉' }
]
}
]
const match = resolveAiAttachmentAssociationMatch(claims, ocrDocuments)
assert.equal(match.highConfidence, true)
assert.equal(match.best.record.claimId, 'claim-wuhan-shanghai')
assert.match(match.best.reasons.join('、'), /日期/)
assert.match(match.best.reasons.join('、'), /地点|行程/)
const message = buildAiAttachmentAssociationMessage({
match,
fileNames: ocrDocuments.map((item) => item.filename),
ocrDocuments
})
assert.match(message, /BX-20260220-001/)
assert.match(message, /确认是否自动归集/)
assert.match(message, /ai-trusted-html:start/)
assert.ok(message.indexOf('票据识别结果') < message.indexOf('可能关联单据'))
const html = renderAiConversationHtml(message)
assert.match(html, /ai-ocr-recognition-card/)
assert.match(html, /ai-attachment-association-card/)
assert.match(html, /可能关联单据/)
assert.doesNotMatch(html, /26429165800002785705/)
const actions = buildAiAttachmentAssociationActions(match, 'assoc-1', { includeOcrDetails: true })
assert.equal(actions[0].action_type, 'show_ai_attachment_ocr_details')
assert.equal(findAction(actions, 'confirm_ai_attachment_association').payload.association_id, 'assoc-1')
assert.ok(findAction(actions, 'open_application_detail'))
})
test('自动归集消息展示票面 OCR 关键字段', () => {
const match = resolveAiAttachmentAssociationMatch([
{
id: 'claim-wuhan-shanghai',
claim_no: 'BX-20260220-001',
expense_type: 'travel',
status: 'draft',
reason: '辅助国网仿生产服务器部署,武汉往返上海',
location: '上海',
occurred_at: '2026-02-20'
}
], [
{
filename: '2月20 武汉-上海.pdf',
summary: '电子发票(铁路电子客票) 武汉-上海 票价 354元',
document_fields: [
{ key: 'amount', label: '金额', value: '354元' },
{ key: 'departed_at', label: '列车出发时间', value: '2026-02-20 07:55' },
{ key: 'route', label: '行程', value: '武汉-上海' }
]
}
])
const message = buildAiAttachmentAssociationMessage({
match,
fileNames: ['2月20 武汉-上海.pdf'],
ocrDocuments: [
{
filename: '2月20 武汉-上海.pdf',
summary: '电子发票(铁路电子客票) 武汉-上海 票价 354元',
document_fields: [
{ key: 'amount', label: '金额', value: '354元' },
{ key: 'departed_at', label: '列车出发时间', value: '2026-02-20 07:55' },
{ key: 'route', label: '行程', value: '武汉-上海' }
]
}
]
})
const html = renderAiConversationHtml(message)
assert.ok(message.indexOf('票据识别结果') < message.indexOf('可能关联单据'))
assert.match(message, /票面识别/)
assert.match(html, /ai-ocr-recognition-card/)
assert.match(html, /金额354元/)
assert.match(html, /列车出发时间2026-02-20 07:55/)
assert.match(html, /行程:武汉-上海/)
})
test('自动归集卡片不把票据数字残片当成单据事项', () => {
const dirtyReason = ':26429165800002785705; :2026; 05'
const ocrDocuments = [
{
filename: '2月20 武汉-上海.pdf',
summary: '电子发票(铁路电子客票) 武汉-上海 票价 354元',
document_fields: [
{ key: 'invoice_number', label: '', value: '26429165800002785705' },
{ key: 'year', label: '', value: '2026' },
{ key: 'amount', label: '金额', value: '354元' },
{ key: 'route', label: '行程', value: '武汉-上海' }
]
}
]
const match = resolveAiAttachmentAssociationMatch([
{
id: 'claim-dirty-reason',
claim_no: 'R74CB7C2R',
expense_type: 'travel',
status: 'draft',
reason: dirtyReason,
location: '上海',
occurred_at: '2026-02-20'
}
], ocrDocuments)
const message = buildAiAttachmentAssociationMessage({
match,
fileNames: ['2月20 武汉-上海.pdf'],
ocrDocuments
})
const html = renderAiConversationHtml(message)
assert.match(html, /R74CB7C2R/)
assert.match(html, /ai-ocr-recognition-card/)
assert.match(html, /票面识别/)
assert.match(html, /金额354元/)
assert.match(html, /行程:武汉-上海/)
assert.doesNotMatch(html, /单据事项/)
assert.doesNotMatch(html, /26429165800002785705/)
assert.doesNotMatch(html, /:2026/)
})
test('没有可关联草稿时给出清晰提示', () => {
const match = resolveAiAttachmentAssociationMatch([
{
id: 'claim-submitted',
claim_no: 'BX-20260220-001',
status: 'submitted',
reason: '已提交报销单',
location: '上海',
occurred_at: '2026-02-20'
}
], [
{
filename: '2月20 武汉-上海.pdf',
summary: '武汉至上海高铁票',
document_fields: [
{ key: 'date', label: '日期', value: '2026-02-20' },
{ key: 'route', label: '行程', value: '武汉-上海' }
]
}
])
assert.equal(match.highConfidence, false)
assert.equal(match.rankedRecords.length, 0)
const message = buildAiAttachmentAssociationMessage({ match, fileNames: ['2月20 武汉-上海.pdf'] })
assert.match(message, /没有查到可关联的报销草稿/)
})
test('自动归集结果用卡片告知上传结果', () => {
const message = buildAiAttachmentAssociationResultMessage({
claimNo: 'BX-20260220-001',
uploadedCount: 2,
skippedCount: 0,
fileNames: ['2月20 武汉-上海.pdf', '2月23 上海-武汉.pdf']
})
const html = renderAiConversationHtml(message)
assert.match(message, /已完成自动归集/)
assert.match(html, /ai-attachment-association-card/)
assert.match(html, /2 份/)
assert.match(html, /BX-20260220-001/)
})
test('低置信候选也提供快捷确认关联动作', () => {
const claims = [
{
id: 'claim-shanghai',
claim_no: 'R74CB7C2R',
expense_type: 'travel',
status: 'draft',
reason: '出差报销',
location: '上海',
occurred_at: '2026-05-18'
}
]
const ocrDocuments = [
{
filename: '2月20 武汉-上海.pdf',
summary: 'G458 Wuhan Shanghaihongqiao',
document_fields: [
{ key: 'route', label: '行程', value: 'Wuhan Shanghaihongqiao' }
]
}
]
const match = resolveAiAttachmentAssociationMatch(claims, ocrDocuments)
assert.equal(match.highConfidence, false)
assert.equal(match.recommended.record.claimId, 'claim-shanghai')
const message = buildAiAttachmentAssociationMessage({
match,
fileNames: ['2月20 武汉-上海.pdf'],
ocrDocuments
})
const html = renderAiConversationHtml(message)
assert.match(html, /候选单据待核对/)
assert.match(html, /ai-attachment-association-card/)
const actions = buildAiAttachmentAssociationActions(match, 'assoc-low-confidence', { includeOcrDetails: true })
assert.equal(actions[0].action_type, 'show_ai_attachment_ocr_details')
assert.equal(findAction(actions, 'confirm_ai_attachment_association').payload.claim_no, 'R74CB7C2R')
})
test('旧版纯文本关联消息也渲染为卡片', () => {
const legacyMessage = [
'### 我已先识别票据,并匹配到最可能的报销单',
'',
'本次附件1 份2月20 武汉-上海.pdf',
'',
'识别摘要2月20 武汉-上海.pdfG458 Wuhan Shanghaihongqiao',
'',
'推荐关联R74CB7C2R',
'',
'单据事项:上海差旅报销',
'',
'匹配依据:地点或行程包含 上海;当前单据仍是可归集草稿',
'',
'你可以直接点下方“查看匹配单据”核对详情,不需要再手动查找。'
].join('\n')
const html = renderAiConversationHtml(legacyMessage)
assert.match(html, /ai-attachment-association-card/)
assert.match(html, /R74CB7C2R/)
assert.doesNotMatch(html, /\*\*R74CB7C2R\*\*/)
assert.doesNotMatch(html, /ai-html-title/)
})