Files
X-Financial/web/tests/ai-document-query-model.test.mjs
caoxiaozhu 3b74a330a3 feat(web): AI 文档查询卡片重构与单号判定统一
- documentClassification 抽出 isApplicationDocumentNo,统一兼容 AP-/APP- 旧格式与 A+8 新格式,aiDocumentQueryModel 复用
- aiDocumentQueryModel 文档卡片改为结构化字段布局(单据类型/金额/申请人/编号/操作),新增查询范围摘要区,渲染走 HTML 信任块
- AppShellRouteView/useAppShell/useRequests/detailAlerts/riskVisibility 等差旅详情模型适配单号判定
- 同步更新 ai-document-query-model/workbench-ai-mode-switch 测试,新增 document-classification 测试
2026-06-20 22:04:37 +08:00

194 lines
8.9 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 assert from 'node:assert/strict'
import test from 'node:test'
import {
buildAiDocumentQueryConditionSummary,
buildAiDocumentQueryMessage,
filterAiDocumentQueryRecords,
resolveAiDocumentQueryIntent
} from '../src/utils/aiDocumentQueryModel.js'
import { renderAiConversationHtml } from '../src/utils/aiConversationHtmlRenderer.js'
const today = '2026-06-20'
const claims = [
{
id: 'claim-1',
claim_no: 'CL-20260221001',
document_type_code: 'reimbursement',
expense_type: 'travel',
status: 'submitted',
reason: '上海出差报销',
employee_name: '曹小筑',
department_name: '交付部',
location: '上海',
occurred_at: '2026-02-21T09:00:00Z',
updated_at: '2026-02-22T10:00:00Z',
amount: 1200
},
{
id: 'app-1',
claim_no: 'AP-20260220001',
document_type_code: 'application',
expense_type: 'travel_application',
status: 'approved',
reason: '辅助国网仿生产服务器部署',
employee_name: '曹小筑',
department_name: '交付部',
location: '上海',
occurred_at: '2026-02-20T09:00:00Z',
updated_at: '2026-02-20T10:00:00Z',
amount: 3000
},
{
id: 'claim-2',
claim_no: 'CL-20260305001',
document_type_code: 'reimbursement',
expense_type: 'office',
status: 'draft',
reason: '办公用品采购',
occurred_at: '2026-03-05T09:00:00Z',
amount: 500
}
]
test('AI document query intent detects my document list questions', () => {
const intent = resolveAiDocumentQueryIntent('我现在有哪些单据?', { today })
assert.equal(intent?.source, 'mine')
assert.equal(intent?.documentType, 'all')
assert.equal(intent?.sourceLabel, '我的单据')
})
test('AI document query intent detects approval document questions', () => {
const intent = resolveAiDocumentQueryIntent('我有哪些审核单', { today })
assert.equal(intent?.source, 'approval')
assert.equal(intent?.sourceLabel, '待我审核的单据')
})
test('AI document query filters by month and document type', () => {
const intent = resolveAiDocumentQueryIntent('我2月有哪些申请单', { today })
const records = filterAiDocumentQueryRecords(claims, intent)
assert.equal(intent?.documentType, 'application')
assert.equal(intent?.timeRange?.start, '2026-02-01')
assert.equal(intent?.timeRange?.end, '2026-02-28')
assert.deepEqual(records.map((record) => record.documentNo), ['AP-20260220001'])
})
test('AI document query filters by single day and document type', () => {
const intent = resolveAiDocumentQueryIntent('2月20日发生的申请单有哪些', { today })
const records = filterAiDocumentQueryRecords(claims, intent)
assert.equal(intent?.timeRange?.start, '2026-02-20')
assert.equal(intent?.timeRange?.end, '2026-02-20')
assert.deepEqual(records.map((record) => record.documentNo), ['AP-20260220001'])
})
test('AI document query combines natural-language filters', () => {
const intent = resolveAiDocumentQueryIntent('查一下2月审批中的差旅报销单金额超过1000上海相关的单据有哪些', { today })
const records = filterAiDocumentQueryRecords(claims, intent)
assert.equal(intent?.documentType, 'reimbursement')
assert.equal(intent?.timeRange?.label, '2026年2月')
assert.equal(intent?.statusFilter?.label, '审批中')
assert.equal(intent?.expenseTypeFilter?.label, '差旅费')
assert.equal(intent?.keywordFilter?.label, '上海')
assert.equal(intent?.amountFilter?.min, 1000)
assert.deepEqual(records.map((record) => record.documentNo), ['CL-20260221001'])
assert.match(buildAiDocumentQueryConditionSummary(intent), /状态:审批中/)
assert.match(buildAiDocumentQueryConditionSummary(intent), /费用类型:差旅费/)
assert.match(buildAiDocumentQueryConditionSummary(intent), /关键词:上海/)
assert.match(buildAiDocumentQueryConditionSummary(intent), /金额不少于1000元/)
})
test('AI document query excludes undated rows when a time condition is present', () => {
const intent = resolveAiDocumentQueryIntent('我2月有哪些单据', { today })
const records = filterAiDocumentQueryRecords([
...claims,
{
id: 'no-date',
claim_no: 'CL-NO-DATE',
document_type_code: 'reimbursement',
expense_type: 'travel',
status: 'submitted',
reason: '缺少业务日期的单据',
amount: 800
}
], intent)
assert.deepEqual(records.map((record) => record.documentNo), ['CL-20260221001', 'AP-20260220001'])
})
test('AI document query message renders html document cards with detail actions', () => {
const intent = resolveAiDocumentQueryIntent('我2月有哪些单据', { today })
const message = buildAiDocumentQueryMessage(intent, claims)
assert.match(message, /### 已查询到相关单据/)
assert.match(message, /<!-- ai-trusted-html:start -->/)
assert.match(message, /<section class="ai-document-query-summary" aria-label="单据查询范围">/)
assert.match(message, /<span class="ai-document-query-summary__label">查询范围<\/span>/)
assert.match(message, /<strong class="ai-document-query-summary__scope">/)
assert.match(message, /<span class="ai-document-query-summary__count">/)
assert.match(message, /<section class="ai-document-card-list" aria-label="单据查询结果">/)
// 申请单 app-1 状态为 approved → is-success 语义类
assert.match(message, /<article class="ai-document-card ai-document-card--application is-success" aria-label="单据详情">/)
assert.match(message, /<header class="ai-document-card__head">/)
assert.match(message, /<span class="ai-document-card__status">已审批<\/span>/)
assert.match(message, /<strong class="ai-document-card__reason">辅助国网仿生产服务器部署<\/strong>/)
assert.match(message, /<div class="ai-document-card__details">/)
assert.match(message, /<span class="ai-document-card__label">单据类型<\/span>/)
assert.match(message, /<strong class="ai-document-card__value">申请单 · 差旅费用申请<\/strong>/)
assert.match(message, /<span class="ai-document-card__label">申请人<\/span>/)
assert.match(message, /<strong class="ai-document-card__value">曹小筑 · 交付部<\/strong>/)
assert.match(message, /<strong class="ai-document-card__value ai-document-card__number">AP-20260220001<\/strong>/)
assert.match(message, /<strong class="ai-document-card__amount">¥3,000\.00<\/strong>/)
assert.match(message, /<div class="ai-document-card__field ai-document-card__field--action">/)
assert.doesNotMatch(message, /ai-document-card__meta/)
assert.doesNotMatch(message, /ai-document-card__meta-item/)
assert.match(message, /href="#ai-open-document-detail:AP-20260220001"/)
// 报销单 claim-1 状态为 submitted → is-pending 语义类
assert.match(message, /<article class="ai-document-card ai-document-card--reimbursement is-pending" aria-label="单据详情">/)
assert.match(message, /href="#ai-open-document-detail:CL-20260221001"/)
assert.doesNotMatch(message, /\| 单据编号 \|/)
assert.doesNotMatch(message, /^> /m)
assert.doesNotMatch(message, /\*\*查询范围\*\*/)
})
test('AI document query html cards render as trusted card markup', () => {
const intent = resolveAiDocumentQueryIntent('我2月有哪些单据', { today })
const rendered = renderAiConversationHtml(buildAiDocumentQueryMessage(intent, claims))
assert.match(rendered, /<h3 class="ai-html-title">已查询到相关单据<\/h3>/)
assert.match(rendered, /<section class="ai-document-query-summary" aria-label="单据查询范围">/)
assert.match(rendered, /<section class="ai-document-card-list" aria-label="单据查询结果">/)
assert.match(rendered, /<article class="ai-document-card ai-document-card--application is-success" aria-label="单据详情">/)
assert.match(rendered, /class="ai-document-card__head"/)
assert.match(rendered, /class="ai-document-card__details"/)
assert.match(rendered, /class="ai-document-card__field"/)
assert.match(rendered, /class="ai-document-card__label"/)
assert.match(rendered, /class="ai-html-action-link ai-html-action-link-document ai-document-card__action"/)
assert.match(rendered, /href="#ai-open-document-detail:CL-20260221001"/)
assert.doesNotMatch(rendered, /ai-document-card__meta/)
assert.doesNotMatch(rendered, /&lt;section class=&quot;ai-document-card-list/)
assert.doesNotMatch(rendered, /<blockquote>/)
})
test('AI document query trusted html rejects unsafe card markup', () => {
const rendered = renderAiConversationHtml([
'### 查询结果',
'',
'<!-- ai-trusted-html:start -->',
'<section class="ai-document-card-list" aria-label="单据查询结果" onclick="alert(1)">',
'<article class="ai-document-card"><script>alert(1)</script></article>',
'</section>',
'<!-- ai-trusted-html:end -->'
].join('\n'))
assert.match(rendered, /<h3 class="ai-html-title">查询结果<\/h3>/)
assert.doesNotMatch(rendered, /ai-document-card-list/)
assert.doesNotMatch(rendered, /<script>/)
assert.doesNotMatch(rendered, /onclick=/)
})