Files
X-Financial/web/tests/ai-conversation-html-renderer.test.mjs
caoxiaozhu e5b03c6601 feat(web): 文档查询意图补充风险过滤与 X 天前范围
- aiDocumentQueryIntent 新增风险等级过滤(无/高/中/低/有风险)与 N 天前日期范围解析
- aiDocumentQueryModel/aiConversationHtmlRenderer 渲染适配风险过滤标签
- useWorkbenchAiDocumentQueryFlow/aiWorkbenchConversationStore 会话流转适配命令意图
- 更新 ai-document-query-model/ai-conversation-html-renderer/assistant-session-draft-delete 测试
2026-06-24 22:59:05 +08:00

181 lines
9.6 KiB
JavaScript
Raw Permalink 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 { renderAiConversationHtml } from '../src/utils/aiConversationHtmlRenderer.js'
test('AI conversation renderer turns business copy into spacious semantic HTML', () => {
const rendered = renderAiConversationHtml([
'### 出差申请办理确认',
'',
'**我已在您的输入中提取到关键信息**,如下表所示:',
'',
'> **前置查询结果**:我已查询您名下可关联的差旅申请单,当前未查到可关联单据。',
'',
'> **需要您确认**:发起新的出差申请属于业务操作,需要您手动确认后我再继续办理。',
'',
'点击下方 **确认发起出差申请** 后,我会继续完成:',
'',
'- **单据重叠核查**:检查同一时间段是否已有申请单,避免重复申请。',
'- **预算与审批预审**:查看部门预算影响,判断是否可能增加预算管理者审核。'
].join('\n'))
assert.match(rendered, /<div class="ai-html-flow">/)
assert.match(rendered, /<h3 class="ai-html-title">出差申请办理确认<\/h3>/)
assert.match(rendered, /<section class="ai-html-focus-grid" aria-label="重点信息">/)
assert.match(rendered, /<article class="ai-html-focus-card">[\s\S]*前置查询结果[\s\S]*当前未查到可关联单据/)
assert.match(rendered, /<article class="ai-html-focus-card">[\s\S]*需要您确认[\s\S]*需要您手动确认后我再继续办理/)
assert.match(rendered, /<ul class="ai-html-steps">[\s\S]*单据重叠核查[\s\S]*预算与审批预审/)
assert.doesNotMatch(rendered, /<blockquote>/)
assert.doesNotMatch(rendered, /<ul>\s*<li><strong>/)
})
test('AI conversation renderer supports tables and escapes unsafe HTML', () => {
const rendered = renderAiConversationHtml([
'### 查询结果',
'',
'| 字段 | 内容 |',
'| --- | --- |',
'| 事由 | 辅助 <script>alert(1)</script> 部署 |',
'| 地点 | 上海 |'
].join('\n'))
assert.match(rendered, /<div class="ai-html-table-wrap">/)
assert.match(rendered, /<th>字段<\/th>/)
assert.match(rendered, /&lt;script&gt;alert\(1\)&lt;\/script&gt;/)
assert.doesNotMatch(rendered, /<script>/)
})
test('AI conversation renderer renders application detail action links as buttons', () => {
const rendered = renderAiConversationHtml([
'| 单据类型 | 单据编号 | 单据状态 | 当前节点 | 日期 | 地点 | 事由 | 操作 |',
'| --- | --- | --- | --- | --- | --- | --- | --- |',
'| 出差申请 | AP-OVERLAP | submitted | 直属领导审批 | 2026-02-20 至 2026-02-23 | 上海 | 辅助国网仿生产服务器部署 | [查看](#ai-open-application-detail:AP-OVERLAP) |'
].join('\n'))
assert.match(rendered, /<section class="ai-document-card-list" role="list" aria-label="单据结果">/)
assert.match(rendered, /<article class="ai-document-card is-pending" role="listitem" aria-label="单据详情">/)
assert.match(rendered, /<strong class="ai-document-card__reason">出差申请<\/strong>/)
assert.match(rendered, /<span class="ai-document-card__status">审批中<\/span>/)
assert.match(rendered, /<div class="ai-document-card__summary">/)
assert.match(rendered, /<span class="ai-document-card__label">日期<\/span>/)
assert.match(rendered, /2026-02-20 至 2026-02-23/)
assert.match(rendered, /<span class="ai-document-card__label">当前节点<\/span>/)
assert.match(rendered, /直属领导审批/)
assert.match(rendered, /<span class="ai-document-card__label">地点<\/span>/)
assert.match(rendered, /上海/)
assert.match(rendered, /<span class="ai-document-card__label">事由<\/span>/)
assert.match(rendered, /辅助国网仿生产服务器部署/)
assert.match(rendered, /<strong class="ai-document-card__value ai-document-card__number">AP-OVERLAP<\/strong>/)
assert.match(rendered, /class="ai-html-action-link ai-document-card__action ai-html-action-link-application"/)
assert.match(rendered, /data-ai-action="open-application-detail"/)
assert.match(rendered, /href="#ai-open-application-detail:AP-OVERLAP"/)
assert.doesNotMatch(rendered, /<table>/)
assert.doesNotMatch(rendered, /ai-html-record-item/)
assert.doesNotMatch(rendered, /target="_blank"[\s\S]{0,120}#ai-open-application-detail/)
})
test('AI conversation renderer renders deleted application detail actions as disabled buttons', () => {
const rendered = renderAiConversationHtml([
'| 单据类型 | 单据编号 | 单据状态 | 当前节点 | 操作 |',
'| --- | --- | --- | --- | --- |',
'| 出差申请 | AP-20260620-DRAFT | 已删除 | 已删除 | [草稿已删除](#ai-deleted-application-detail:claim-draft-1) |'
].join('\n'))
assert.match(rendered, /class="ai-html-action-link ai-document-card__action ai-html-action-link-application is-disabled"/)
assert.match(rendered, /aria-disabled="true"/)
assert.match(rendered, /data-ai-action="deleted-application-detail"/)
assert.doesNotMatch(rendered, /href="#ai-deleted-application-detail/)
})
test('AI conversation renderer turns application conflict tables into record lists', () => {
const rendered = renderAiConversationHtml([
'| 单据编号 | 申请时间 | 状态 | 事由 | 操作 |',
'| --- | --- | --- | --- | --- |',
'| AP-20260620063557-4JU2MWEF | 2026-02-20 至 2026-02-23 | 审批中 | 辅助国网仿生产服务器部署 | [查看](#ai-open-application-detail:AP-20260620063557-4JU2MWEF) |'
].join('\n'))
assert.match(rendered, /<section class="ai-document-card-list" role="list" aria-label="单据结果">/)
assert.match(rendered, /<article class="ai-document-card is-pending" role="listitem" aria-label="单据详情">/)
assert.match(rendered, /<div class="ai-document-card__summary">/)
assert.match(rendered, /<span class="ai-document-card__label">日期<\/span>/)
assert.match(rendered, /2026-02-20 至 2026-02-23/)
assert.match(rendered, /<span class="ai-document-card__label">当前节点<\/span>/)
assert.match(rendered, /辅助国网仿生产服务器部署/)
assert.match(rendered, /ai-document-card__field--action/)
assert.doesNotMatch(rendered, /<table>/)
assert.doesNotMatch(rendered, /ai-html-record-item/)
})
test('AI conversation renderer renders document detail action links as buttons', () => {
const rendered = renderAiConversationHtml('[查看单据](#ai-open-document-detail:CL-20260221001)')
assert.match(rendered, /class="ai-html-action-link ai-html-action-link-document"/)
assert.match(rendered, /data-ai-action="open-document-detail"/)
assert.match(rendered, /href="#ai-open-document-detail:CL-20260221001"/)
assert.doesNotMatch(rendered, /target="_blank"[\s\S]{0,120}#ai-open-document-detail/)
})
test('AI conversation renderer renders deleted document detail actions as disabled buttons', () => {
const rendered = renderAiConversationHtml('[单据已删除](#ai-deleted-document-detail:claim-deleted-1)')
assert.match(rendered, /class="ai-html-action-link ai-html-action-link-document is-disabled"/)
assert.match(rendered, /aria-disabled="true"/)
assert.match(rendered, /data-ai-action="deleted-document-detail"/)
assert.doesNotMatch(rendered, /href="#ai-deleted-document-detail/)
})
test('AI conversation renderer renders images as html and rejects unsafe image sources', () => {
const rendered = renderAiConversationHtml([
'### 图片材料',
'',
'![票据预览](https://example.com/receipt.png)',
'',
'内联图片:![危险](javascript:alert(1))'
].join('\n'))
assert.match(rendered, /<figure class="ai-html-image-frame">/)
assert.match(rendered, /<img class="ai-html-image" src="https:\/\/example\.com\/receipt\.png" alt="票据预览" loading="lazy" \/>/)
assert.match(rendered, /<figcaption class="ai-html-image-caption">票据预览<\/figcaption>/)
assert.doesNotMatch(rendered, /javascript:alert/)
})
test('AI conversation renderer keeps separated step bullets in one numbered sequence', () => {
const rendered = renderAiConversationHtml([
'点击下方 **确认发起出差申请** 后,我会继续完成:',
'',
'- **单据重叠核查**:检查同一时间段是否已有申请单,避免重复申请。',
'',
'- **预算与审批预审**:查看部门预算影响,判断是否可能增加预算管理者审核。',
'',
'- **申请表生成**:预审完成后,再展示完整申请表并自动预填已识别信息。'
].join('\n'))
assert.equal((rendered.match(/class="ai-html-steps"/g) || []).length, 1)
assert.match(rendered, /<span class="ai-html-step-index">1<\/span>[\s\S]*单据重叠核查/)
assert.match(rendered, /<span class="ai-html-step-index">2<\/span>[\s\S]*预算与审批预审/)
assert.match(rendered, /<span class="ai-html-step-index">3<\/span>[\s\S]*申请表生成/)
})
test('AI conversation renderer hides noisy attachment association reason fragments', () => {
const rendered = renderAiConversationHtml([
'### 我已先识别票据,并匹配到最可能的报销单',
'',
'本次附件1 份2月20 武汉-上海.pdf',
'',
'识别摘要2月20 武汉-上海.pdf电子发票铁路电子客票 武汉-上海 票价 354元',
'',
'推荐关联R74CB7C2R',
'',
'单据事项::26429165800002785705; :2026; 05',
'',
'匹配依据:票据日期与报销单日期一致;地点或行程包含 上海;当前单据仍是可归集草稿'
].join('\n'))
assert.match(rendered, /ai-attachment-association-card/)
assert.match(rendered, /R74CB7C2R/)
assert.doesNotMatch(rendered, /单据事项/)
assert.doesNotMatch(rendered, /关联事项/)
assert.doesNotMatch(rendered, /26429165800002785705/)
assert.doesNotMatch(rendered, /:2026/)
})