import assert from 'node:assert/strict' import test from 'node:test' import { buildAiDocumentQueryConditionSummary, buildAiDocumentQueryMessage, filterAiDocumentQueryRecords, mergeAiDocumentQueryPayloads, resolveAiDocumentQueryIntent } from '../src/utils/aiDocumentQueryModel.js' import { renderAiConversationHtml } from '../src/utils/aiConversationHtmlRenderer.js' import { resolveWorkbenchIntentActionRoute } from '../src/composables/workbenchAiMode/workbenchIntentActionPolicy.js' import { resolveWorkbenchIntentFrame } from '../src/composables/workbenchAiMode/workbenchIntentFrameModel.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, 'accessible') 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 intent detects short approval workbench commands', () => { const reviewIntent = resolveAiDocumentQueryIntent('我要审核', { today }) const todoIntent = resolveAiDocumentQueryIntent('待办审批', { today }) assert.equal(reviewIntent?.source, 'approval') assert.equal(reviewIntent?.documentType, 'all') assert.equal(reviewIntent?.sourceLabel, '待我审核的单据') assert.equal(todoIntent?.source, 'approval') assert.equal(todoIntent?.sourceLabel, '待我审核的单据') }) test('AI document query intent keeps approval policy questions out of document query', () => { assert.equal(resolveAiDocumentQueryIntent('审批规则怎么走', { today }), null) }) test('AI document query keeps explicit own-document scope separate from accessible documents', () => { const intent = resolveAiDocumentQueryIntent('我名下有哪些单据?', { today }) assert.equal(intent?.source, 'mine') assert.equal(intent?.sourceLabel, '我的单据') }) test('AI document query merges own and approval records for generic document list questions', () => { const intent = resolveAiDocumentQueryIntent('我现在有哪些单据?', { today }) const approvalClaims = [{ id: 'approval-1', claim_no: 'CL-APPROVAL-001', document_type_code: 'reimbursement', expense_type: 'meeting', status: 'pending', approval_stage: '直属领导审批', reason: '待我审核的会议费', employee_name: '李文静', department_name: '市场部', occurred_at: '2026-02-22T09:00:00Z', amount: 880 }] const merged = mergeAiDocumentQueryPayloads(claims, { items: approvalClaims, querySource: 'approval' }, [claims[0]]) const records = filterAiDocumentQueryRecords(merged, intent) assert.equal(intent?.source, 'accessible') assert.deepEqual( records.map((record) => record.documentNo), ['CL-20260305001', 'CL-APPROVAL-001', 'CL-20260221001', 'AP-20260220001'] ) }) 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 filters draft candidates by relative day', () => { const intent = resolveAiDocumentQueryIntent('我的 3天前 草稿单据', { today: '2026-06-24' }) const records = filterAiDocumentQueryRecords([ { id: 'draft-3-days-ago', claim_no: 'AP-20260621001', document_type_code: 'application', status: 'draft', reason: '三天前保存的申请草稿', created_at: '2026-06-21T09:00:00Z' }, { id: 'draft-yesterday', claim_no: 'AP-20260623001', document_type_code: 'application', status: 'draft', reason: '昨天保存的申请草稿', created_at: '2026-06-23T09:00:00Z' } ], intent) assert.equal(intent?.source, 'mine') assert.equal(intent?.statusFilter?.label, '草稿') assert.equal(intent?.timeRange?.start, '2026-06-21') assert.equal(intent?.timeRange?.end, '2026-06-21') assert.deepEqual(records.map((record) => record.documentNo), ['AP-20260621001']) }) test('AI document query filters no-risk approval application candidates', () => { const intent = resolveAiDocumentQueryIntent('待我审核 无风险 申请单', { today }) const payload = [{ id: 'approval-no-risk', claim_no: 'AP-NORISK-001', document_type_code: 'application', expense_type: 'travel_application', status: 'submitted', reason: '合规差旅申请', created_at: '2026-06-20T09:00:00Z', risk_flags_json: [], risk_summary: '无' }, { id: 'approval-high-risk', claim_no: 'AP-RISK-001', document_type_code: 'application', expense_type: 'travel_application', status: 'submitted', reason: '住宿超标申请', created_at: '2026-06-20T10:00:00Z', risk_flags_json: [{ severity: 'high', summary: '住宿超标' }], risk_summary: '住宿超标' }] const records = filterAiDocumentQueryRecords({ items: payload, querySource: 'approval' }, intent) assert.equal(intent?.source, 'approval') assert.equal(intent?.documentType, 'application') assert.equal(intent?.riskFilter?.level, 'none') assert.match(buildAiDocumentQueryConditionSummary(intent), /风险:无风险/) assert.deepEqual(records.map((record) => record.documentNo), ['AP-NORISK-001']) }) 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, //) assert.match(message, /
/) assert.match(message, /查询范围<\/span>/) assert.match(message, //) assert.match(message, //) assert.match(message, /
/) // 申请单 app-1 状态为 approved → is-success 语义类 assert.match(message, /
/) assert.match(message, /
/) assert.match(message, /已审批<\/span>/) assert.match(message, /差旅费用申请<\/strong>/) assert.match(message, /
/) assert.match(message, /
/) assert.match(message, /日期<\/span>/) assert.match(message, /2026-02-20/) assert.match(message, /预计金额<\/span>/) assert.match(message, /¥3,000\.00<\/strong>/) assert.doesNotMatch(message, /当前节点<\/span>/) assert.match(message, /单据类型<\/span>/) assert.match(message, /申请单 · 差旅费用申请<\/strong>/) assert.match(message, /地点<\/span>/) assert.match(message, /上海/) assert.match(message, /事由<\/span>/) assert.match(message, /辅助国网仿生产服务器部署/) assert.match(message, /申请人<\/span>/) assert.match(message, /曹小筑 · 交付部<\/strong>/) assert.match(message, /AP-20260220001<\/strong>/) assert.match(message, /
/) assert.doesNotMatch(message, /ai-document-card__meta/) assert.doesNotMatch(message, /ai-document-card__meta-item/) assert.match(message, /href="#ai-open-document-detail:claim_id%3Dapp-1%26claim_no%3DAP-20260220001"/) // 报销单 claim-1 状态为 submitted → is-pending 语义类 assert.match(message, /
/) assert.match(message, /审批中<\/span>/) assert.match(message, /href="#ai-open-document-detail:claim_id%3Dclaim-1%26claim_no%3DCL-20260221001"/) assert.doesNotMatch(message, />submitted /m) assert.doesNotMatch(message, /\*\*查询范围\*\*/) }) test('AI document query highlights approval-task cards and shows pending review status', () => { const intent = resolveAiDocumentQueryIntent('我有哪些审核单', { today }) const message = buildAiDocumentQueryMessage(intent, [{ id: 'approval-1', claim_no: 'AP-APPROVAL-001', document_type_code: 'application', expense_type: 'travel_application', status: 'submitted', approval_stage: '直属领导审批', reason: '参加相关残联会议', employee_name: '曹笑竹', department_name: '技术部', location: '上海', occurred_at: '2026-02-20T09:00:00Z', amount: 2120 }]) assert.match(message, /ai-document-card--application ai-document-card--approval-task is-pending/) assert.match(message, /待审批<\/span>/) assert.match(message, /预计金额<\/span>/) assert.match(message, /¥2,120\.00<\/strong>/) assert.doesNotMatch(message, /当前节点<\/span>/) assert.match(message, /单据类型<\/span>/) assert.match(message, /申请单 · 差旅费用申请<\/strong>/) assert.match(message, /href="#ai-open-document-detail:claim_id%3Dapproval-1%26claim_no%3DAP-APPROVAL-001"/) }) 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>/) assert.match(rendered, /
/) assert.match(rendered, /
/) assert.match(rendered, /
/) 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:claim_id%3Dclaim-1%26claim_no%3DCL-20260221001"/) assert.doesNotMatch(rendered, /ai-document-card__meta/) assert.doesNotMatch(rendered, /<section class="ai-document-card-list/) assert.doesNotMatch(rendered, /
/) }) test('AI document query keeps high-risk command guidance in trusted rendered markup', () => { const frame = resolveWorkbenchIntentFrame('删除申请单草稿', { today: '2026-06-24' }) const route = resolveWorkbenchIntentActionRoute(frame) const intent = resolveAiDocumentQueryIntent(route.queryPrompt, { today: '2026-06-24' }) const rendered = renderAiConversationHtml(buildAiDocumentQueryMessage(intent, [{ id: 'draft-application-1', claim_no: 'AP-DRAFT-001', document_type_code: 'application', expense_type: 'travel_application', status: 'draft', reason: '测试申请草稿', created_at: '2026-06-24T09:00:00Z' }], { commandFrame: frame })) assert.match(rendered, /

已查询到相关单据<\/h3>/) assert.match(rendered, /
/) assert.match(rendered, /系统不会直接删除相关单据/) assert.match(rendered, /进入单据详情核对后再操作/) assert.match(rendered, /
/) assert.match(rendered, /进入详情确认删除/) }) test('AI document query trusted html rejects unsafe card markup', () => { const rendered = renderAiConversationHtml([ '### 查询结果', '', '', '
', '
', '
', '' ].join('\n')) assert.match(rendered, /

查询结果<\/h3>/) assert.doesNotMatch(rendered, /ai-document-card-list/) assert.doesNotMatch(rendered, /