feat(web): 工作台 AI 模式报销预审与文档查询模型拆分
- 新增 aiApplicationPrecheckModel/aiDocumentQueryModel/aiApplicationPreviewActions/aiConversationHtmlRenderer 四个独立模型与服务,按职责从主组件拆出 - PersonalWorkbenchAiMode 接入拆分后的预审、文档查询与 HTML 渲染逻辑,配合 markdown 工具增强结构化展示 - 文档中心与归档筛选、风险可见性、申请预览等工具同步适配,补充对应单元测试 - 新增 AI 文档卡片背景资源
This commit is contained in:
114
web/tests/ai-application-precheck-model.test.mjs
Normal file
114
web/tests/ai-application-precheck-model.test.mjs
Normal file
@@ -0,0 +1,114 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
|
||||
import {
|
||||
buildAiApplicationPrecheck,
|
||||
buildAiApplicationPrecheckMessage,
|
||||
buildAiApplicationPrecheckThinkingEvents
|
||||
} from '../src/utils/aiApplicationPrecheckModel.js'
|
||||
|
||||
const preview = {
|
||||
fields: {
|
||||
applicationType: '差旅费用申请',
|
||||
time: '2026-02-20 至 2026-02-23',
|
||||
location: '上海',
|
||||
reason: '辅助国网仿生产服务器部署',
|
||||
amount: '2,120元',
|
||||
days: '4天',
|
||||
transportMode: '火车'
|
||||
},
|
||||
missingFields: []
|
||||
}
|
||||
|
||||
test('application precheck blocks application generation when existing application overlaps', () => {
|
||||
const precheck = buildAiApplicationPrecheck(preview, {
|
||||
currentUser: { name: '曹笑竹', departmentName: '技术部' },
|
||||
claimsPayload: {
|
||||
items: [
|
||||
{
|
||||
claim_no: 'AP-OVERLAP',
|
||||
document_type: 'expense_application',
|
||||
expense_type: 'travel_application',
|
||||
employee_name: '曹笑竹',
|
||||
status: 'submitted',
|
||||
risk_flags_json: [
|
||||
{
|
||||
source: 'application_detail',
|
||||
application_detail: {
|
||||
business_time: '2026-02-21 至 2026-02-22',
|
||||
reason: '同时间段现场支持',
|
||||
location: '上海'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
budgetSummary: {
|
||||
total_amount: 10000,
|
||||
reserved_amount: 8000,
|
||||
consumed_amount: 500,
|
||||
available_amount: 1500
|
||||
}
|
||||
})
|
||||
|
||||
assert.equal(precheck.overlap.status, 'warning')
|
||||
assert.match(precheck.overlap.summary, /可能重叠/)
|
||||
assert.equal(precheck.overlap.matches[0].claimNo, 'AP-OVERLAP')
|
||||
assert.equal(precheck.budget.status, 'warning')
|
||||
assert.equal(precheck.budget.requiresBudgetReview, true)
|
||||
assert.match(precheck.budget.summary, /预算管理者审核/)
|
||||
|
||||
const message = buildAiApplicationPrecheckMessage(preview, precheck)
|
||||
assert.match(message, /### 发现同时间段已有申请单/)
|
||||
assert.match(message, /时间重叠提醒/)
|
||||
assert.match(message, /AP-OVERLAP/)
|
||||
assert.match(message, /\| 单据编号 \| 申请时间 \| 状态 \| 事由 \| 操作 \|/)
|
||||
assert.match(message, /\| AP-OVERLAP \| 2026-02-21 至 2026-02-22 \| 审批中 \| 同时间段现场支持 \| \[查看\]\(#ai-open-application-detail:AP-OVERLAP\) \|/)
|
||||
assert.match(message, /2026-02-21 至 2026-02-22/)
|
||||
assert.match(message, /同时间段现场支持/)
|
||||
assert.match(message, /请先检查本次申请时间是否填写正确/)
|
||||
assert.doesNotMatch(message, /出差申请表草稿已生成/)
|
||||
})
|
||||
|
||||
test('application precheck emits thinking events for overlap, budget, and form generation', () => {
|
||||
const precheck = buildAiApplicationPrecheck(preview, {
|
||||
currentUser: { name: '曹笑竹' },
|
||||
claimsPayload: [],
|
||||
budgetSummary: {
|
||||
total_amount: 10000,
|
||||
reserved_amount: 1000,
|
||||
consumed_amount: 1000,
|
||||
available_amount: 8000
|
||||
}
|
||||
})
|
||||
const events = buildAiApplicationPrecheckThinkingEvents(precheck)
|
||||
|
||||
assert.equal(events.length, 3)
|
||||
assert.deepEqual(
|
||||
events.map((event) => event.eventId),
|
||||
['application-precheck-overlap', 'application-precheck-budget', 'application-precheck-form']
|
||||
)
|
||||
assert.match(events[1].content, /预算/)
|
||||
})
|
||||
|
||||
test('application precheck ignores application candidates without parseable business time', () => {
|
||||
const precheck = buildAiApplicationPrecheck(preview, {
|
||||
currentUser: { name: '曹笑竹' },
|
||||
claimsPayload: {
|
||||
items: [
|
||||
{
|
||||
claim_no: 'AP-NO-TIME',
|
||||
document_type: 'expense_application',
|
||||
expense_type: 'travel_application',
|
||||
employee_name: '曹笑竹',
|
||||
status: 'submitted'
|
||||
}
|
||||
]
|
||||
},
|
||||
budgetSummary: {}
|
||||
})
|
||||
|
||||
assert.equal(precheck.overlap.status, 'ok')
|
||||
assert.deepEqual(precheck.overlap.matches, [])
|
||||
})
|
||||
127
web/tests/ai-application-preview-actions.test.mjs
Normal file
127
web/tests/ai-application-preview-actions.test.mjs
Normal file
@@ -0,0 +1,127 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
|
||||
import {
|
||||
AI_APPLICATION_ACTION_SAVE_DRAFT,
|
||||
AI_APPLICATION_ACTION_SUBMIT,
|
||||
buildAiApplicationPreviewActionPayload
|
||||
} from '../src/services/aiApplicationPreviewActions.js'
|
||||
import {
|
||||
applyApplicationPolicyEstimateResult,
|
||||
buildApplicationPolicyEstimateRequest,
|
||||
buildLocalApplicationPreview
|
||||
} from '../src/utils/expenseApplicationPreview.js'
|
||||
|
||||
const applicationPreview = {
|
||||
fields: {
|
||||
applicationType: '差旅费用申请',
|
||||
applicant: '曹笑竹',
|
||||
grade: 'P5',
|
||||
department: '技术部',
|
||||
position: '财务智能化产品经理',
|
||||
managerName: '向万红',
|
||||
time: '2026-02-20 至 2026-02-23',
|
||||
location: '上海',
|
||||
reason: '辅助国网仿生产服务器部署',
|
||||
days: '4天',
|
||||
transportMode: '火车',
|
||||
lodgingDailyCap: '250元/天',
|
||||
subsidyDailyCap: '100元/天',
|
||||
transportPolicy: '按交通费用预估表暂估',
|
||||
policyEstimate: '交通 720元 + 住宿 1,000元 + 补贴 400元 = 2,120元(4天)',
|
||||
amount: '2,120元'
|
||||
}
|
||||
}
|
||||
|
||||
const currentUser = {
|
||||
username: 'caoxiaozhu@xf.com',
|
||||
name: '曹笑竹',
|
||||
departmentName: '技术部',
|
||||
position: '财务智能化产品经理',
|
||||
grade: 'P5',
|
||||
managerName: '向万红',
|
||||
roleCodes: ['employee']
|
||||
}
|
||||
|
||||
test('save application preview payload uses save draft action without submit wording', () => {
|
||||
const payload = buildAiApplicationPreviewActionPayload({
|
||||
actionType: AI_APPLICATION_ACTION_SAVE_DRAFT,
|
||||
applicationPreview,
|
||||
currentUser,
|
||||
conversationId: 'inline-1'
|
||||
})
|
||||
|
||||
assert.equal(payload.user_id, 'caoxiaozhu@xf.com')
|
||||
assert.equal(payload.conversation_id, 'inline-1')
|
||||
assert.equal(payload.context_json.session_type, 'application')
|
||||
assert.equal(payload.context_json.review_action, undefined)
|
||||
assert.equal(payload.context_json.application_action, 'save_draft')
|
||||
assert.equal(payload.context_json.application_preview.fields.transportMode, '火车')
|
||||
assert.match(payload.message, /费用申请保存草稿/)
|
||||
assert.match(payload.message, /保存草稿/)
|
||||
assert.doesNotMatch(payload.message, /确认提交/)
|
||||
})
|
||||
|
||||
test('submit application preview payload keeps existing draft id for resubmission', () => {
|
||||
const payload = buildAiApplicationPreviewActionPayload({
|
||||
actionType: AI_APPLICATION_ACTION_SUBMIT,
|
||||
applicationPreview,
|
||||
currentUser,
|
||||
conversationId: 'inline-1',
|
||||
draftPayload: {
|
||||
claim_id: 'draft-001',
|
||||
claim_no: 'AP-202602200001'
|
||||
}
|
||||
})
|
||||
|
||||
assert.equal(payload.context_json.review_action, undefined)
|
||||
assert.equal(payload.context_json.application_edit_claim_id, 'draft-001')
|
||||
assert.equal(payload.context_json.draft_claim_id, 'draft-001')
|
||||
assert.match(payload.message, /费用申请确认提交/)
|
||||
assert.match(payload.message, /确认提交/)
|
||||
})
|
||||
|
||||
test('travel application preview calculates base standards before transport mode is selected', () => {
|
||||
const preview = buildLocalApplicationPreview(
|
||||
'2月20-23日去上海出差,辅助国网仿生产服务器部署',
|
||||
{ name: '曹笑竹', grade: 'P5', location: '武汉' },
|
||||
{ today: '2026-06-20' }
|
||||
)
|
||||
const request = buildApplicationPolicyEstimateRequest(preview, { grade: 'P5', location: '武汉' })
|
||||
|
||||
assert.equal(request.canCalculate, true)
|
||||
assert.deepEqual(request.payload, {
|
||||
days: 4,
|
||||
location: '上海',
|
||||
grade: 'P5',
|
||||
transport_mode: null,
|
||||
origin_location: '武汉',
|
||||
travel_date: '2026-02-20'
|
||||
})
|
||||
|
||||
const estimatedPreview = applyApplicationPolicyEstimateResult(preview, {
|
||||
days: 4,
|
||||
location: '上海',
|
||||
matched_city: '上海',
|
||||
grade: 'P5',
|
||||
hotel_rate: 450,
|
||||
hotel_amount: 1800,
|
||||
total_allowance_rate: 100,
|
||||
allowance_amount: 400,
|
||||
transport_mode: '火车',
|
||||
transport_origin: '武汉',
|
||||
transport_destination: '上海',
|
||||
transport_estimated_amount: 720,
|
||||
total_amount: 2200,
|
||||
rule_name: '公司差旅费报销规则',
|
||||
rule_version: 'v1.0.0'
|
||||
}, { grade: 'P5', location: '武汉' })
|
||||
|
||||
assert.equal(estimatedPreview.fields.transportMode, '')
|
||||
assert.equal(estimatedPreview.missingFields.includes('出行方式'), true)
|
||||
assert.equal(estimatedPreview.fields.lodgingDailyCap, '450元/天')
|
||||
assert.equal(estimatedPreview.fields.subsidyDailyCap, '100元/天')
|
||||
assert.equal(estimatedPreview.fields.transportPolicy, '选择火车、飞机或轮船后自动预估交通费用')
|
||||
assert.equal(estimatedPreview.fields.policyEstimate, '交通待补充 + 住宿 1,800元 + 补贴 400元 = 2,200元(4天,不含交通)')
|
||||
assert.equal(estimatedPreview.fields.amount, '2,200元(不含交通)')
|
||||
})
|
||||
100
web/tests/ai-conversation-html-renderer.test.mjs
Normal file
100
web/tests/ai-conversation-html-renderer.test.mjs
Normal file
@@ -0,0 +1,100 @@
|
||||
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, /<script>alert\(1\)<\/script>/)
|
||||
assert.doesNotMatch(rendered, /<script>/)
|
||||
})
|
||||
|
||||
test('AI conversation renderer renders application detail action links as buttons', () => {
|
||||
const rendered = renderAiConversationHtml([
|
||||
'| 单据编号 | 操作 |',
|
||||
'| --- | --- |',
|
||||
'| AP-OVERLAP | [查看](#ai-open-application-detail:AP-OVERLAP) |'
|
||||
].join('\n'))
|
||||
|
||||
assert.match(rendered, /class="ai-html-action-link 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, /target="_blank"[\s\S]{0,120}#ai-open-application-detail/)
|
||||
})
|
||||
|
||||
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 images as html and rejects unsafe image sources', () => {
|
||||
const rendered = renderAiConversationHtml([
|
||||
'### 图片材料',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'内联图片:)'
|
||||
].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]*申请表生成/)
|
||||
})
|
||||
181
web/tests/ai-document-query-model.test.mjs
Normal file
181
web/tests/ai-document-query-model.test.mjs
Normal file
@@ -0,0 +1,181 @@
|
||||
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-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, /<span class="ai-document-card__owner">曹小筑<\/span>/)
|
||||
assert.match(message, /<span class="ai-document-card__dept">交付部<\/span>/)
|
||||
assert.match(message, /<span class="ai-document-card__number">AP-20260220001<\/span>/)
|
||||
assert.match(message, /<strong class="ai-document-card__amount">¥3,000\.00<\/strong>/)
|
||||
assert.match(message, /<div class="ai-document-card__meta">/)
|
||||
assert.match(message, /<span class="ai-document-card__meta-item">上海<\/span>/)
|
||||
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)
|
||||
})
|
||||
|
||||
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-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__meta"/)
|
||||
assert.match(rendered, /class="ai-document-card__meta-item"/)
|
||||
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, /<section class="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=/)
|
||||
})
|
||||
@@ -10,7 +10,8 @@ import {
|
||||
extractArchiveMonth,
|
||||
formatArchiveMonthLabel,
|
||||
formatArchiveRiskCountLabel,
|
||||
hasActiveArchiveListFilters
|
||||
hasActiveArchiveListFilters,
|
||||
resolveArchiveRiskTone
|
||||
} from '../src/utils/archiveCenterListFilters.js'
|
||||
|
||||
const sampleRows = [
|
||||
@@ -117,3 +118,35 @@ test('hasActiveArchiveListFilters detects active criteria', () => {
|
||||
assert.equal(hasActiveArchiveListFilters({ risk: 'high' }), true)
|
||||
assert.equal(hasActiveArchiveListFilters({ risk: 'all', type: 'all' }), false)
|
||||
})
|
||||
|
||||
test('list risk tone hides budget governance risk from applicant for consistency with detail', () => {
|
||||
// 申请单列表里,预算治理类风险(对申请人不可见)不应计入申请人看到的风险等级,
|
||||
// 避免出现“列表显示中风险、详情却看不到”的不一致。
|
||||
const applicationRequest = {
|
||||
id: 'AP-202606200001',
|
||||
documentTypeCode: 'application',
|
||||
typeCode: 'travel_application',
|
||||
employeeId: 'EMP-001',
|
||||
employeeName: '张三'
|
||||
}
|
||||
const submitter = { id: 'EMP-001', name: '张三', employeeId: 'EMP-001' }
|
||||
|
||||
const riskFlags = [
|
||||
{
|
||||
source: 'budget_control',
|
||||
severity: 'high',
|
||||
message: '预算可用余额不足。',
|
||||
business_stage: 'expense_application',
|
||||
risk_domain: 'budget',
|
||||
visibility_scope: 'budget_manager',
|
||||
actionability: 'budget_governance'
|
||||
}
|
||||
]
|
||||
const viewerForSubmitter = { request: applicationRequest, currentUser: submitter }
|
||||
|
||||
// 申请人:列表不应显示该预算风险 → tone 为 low、count 为 0
|
||||
assert.equal(resolveArchiveRiskTone(riskFlags, '', viewerForSubmitter), 'low')
|
||||
assert.equal(countClaimRisks(riskFlags, '', viewerForSubmitter), 0)
|
||||
// 不传 viewerOptions 时保持向后兼容(原样统计)
|
||||
assert.equal(resolveArchiveRiskTone(riskFlags, ''), 'high')
|
||||
})
|
||||
|
||||
@@ -343,7 +343,7 @@ test('documents center list renders risk level tags instead of status tags', ()
|
||||
assert.match(documentsCenterView, /<th>风险等级<\/th>/)
|
||||
assert.match(documentsCenterView, /<td data-label="风险等级">[\s\S]*class="risk-level-tags"[\s\S]*v-for="tag in row\.riskTags"/)
|
||||
assert.match(documentsCenterView, /import \{ countClaimRisks, resolveArchiveRiskTone \} from '..\/utils\/archiveCenterListFilters\.js'/)
|
||||
assert.match(documentsCenterView, /function buildDocumentRiskMeta\(row\) \{[\s\S]*countClaimRisks\(riskFlags, riskSummary\)/)
|
||||
assert.match(documentsCenterView, /function buildDocumentRiskMeta\(row\) \{[\s\S]*countClaimRisks\(riskFlags, riskSummary, viewerOptions\)/)
|
||||
assert.match(documentsCenterView, /riskTone: riskMeta\.tone,[\s\S]*riskLabel: riskMeta\.label,[\s\S]*riskCount: riskMeta\.count,[\s\S]*riskTags: riskMeta\.tags/)
|
||||
assert.match(documentsCenterView, /function matchesRiskLevelTab\(row, tab\) \{[\s\S]*tab === '高风险'[\s\S]*row\.riskTone === 'high'/)
|
||||
assert.match(documentListSharedStyles, /\.risk-level-tags\s*\{[\s\S]*display:\s*inline-flex;/)
|
||||
|
||||
@@ -1543,7 +1543,7 @@ test('application preview merges rule center travel estimate into highlighted ro
|
||||
assert.equal(buildApplicationPreviewRows(estimatedPreview).find((row) => row.key === 'policyEstimate')?.highlight, true)
|
||||
})
|
||||
|
||||
test('application preview blocks policy estimate when transport mode is missing', () => {
|
||||
test('application preview calculates base policy estimate when transport mode is missing', () => {
|
||||
const currentUser = { name: '李文静', grade: 'P5', location: '武汉' }
|
||||
const preview = buildLocalApplicationPreview(
|
||||
'我要申请2月20日-23日去上海出差,辅助国网仿生产项目部署',
|
||||
@@ -1551,9 +1551,15 @@ test('application preview blocks policy estimate when transport mode is missing'
|
||||
{ today: '2026-06-09' }
|
||||
)
|
||||
const request = buildApplicationPolicyEstimateRequest(preview, currentUser)
|
||||
assert.equal(request.canCalculate, false)
|
||||
assert.equal(request.reason, '缺少出行方式')
|
||||
assert.equal(request.payload, null)
|
||||
assert.equal(request.canCalculate, true)
|
||||
assert.deepEqual(request.payload, {
|
||||
days: 4,
|
||||
location: '上海',
|
||||
grade: 'P5',
|
||||
transport_mode: null,
|
||||
origin_location: '武汉',
|
||||
travel_date: '2026-02-20'
|
||||
})
|
||||
assert.equal(preview.missingFields.includes('出行方式'), true)
|
||||
assert.equal(preview.readyToSubmit, false)
|
||||
|
||||
@@ -1586,14 +1592,17 @@ test('application preview blocks policy estimate when transport mode is missing'
|
||||
|
||||
assert.equal(blockedEstimatePreview.fields.transportMode, '')
|
||||
assert.equal(blockedEstimatePreview.fields.transportEstimatedAmount, '')
|
||||
assert.equal(blockedEstimatePreview.fields.policyEstimate, '填写地点和天数后自动测算')
|
||||
assert.equal(blockedEstimatePreview.fields.lodgingDailyCap, '250元/天')
|
||||
assert.equal(blockedEstimatePreview.fields.subsidyDailyCap, '100元/天')
|
||||
assert.equal(blockedEstimatePreview.fields.policyEstimate, '交通待补充 + 住宿 1,000元 + 补贴 400元 = 1,400元(4天,不含交通)')
|
||||
assert.equal(blockedEstimatePreview.fields.amount, '1,400元(不含交通)')
|
||||
assert.equal(blockedEstimatePreview.missingFields.includes('出行方式'), true)
|
||||
assert.equal(staleEstimatePreview.fields.reason, '辅助国网仿生产项目部署')
|
||||
assert.equal(staleEstimatePreview.fields.transportMode, '火车')
|
||||
assert.equal(staleEstimatePreview.missingFields.includes('出行方式'), false)
|
||||
assert.equal(staleEstimatePreview.fields.transportPolicy, '当前尚未接通实时票务价格查询 API,无法获取当前实际票价;先按《交通费用预估表》武汉-上海火车往返(二等座预估)暂估 720元用于申请阶段预算占用,最终报销以实际票据金额为准')
|
||||
assert.match(staleEstimatePreview.fields.policyEstimate, /交通 720元/)
|
||||
assert.equal(staleEstimatePreview.fields.amount, '2,120元')
|
||||
assert.equal(staleEstimatePreview.fields.transportMode, '')
|
||||
assert.equal(staleEstimatePreview.missingFields.includes('出行方式'), true)
|
||||
assert.equal(staleEstimatePreview.fields.transportPolicy, '选择火车、飞机或轮船后自动预估交通费用')
|
||||
assert.equal(staleEstimatePreview.fields.policyEstimate, '交通待补充 + 住宿 1,000元 + 补贴 400元 = 1,400元(4天,不含交通)')
|
||||
assert.equal(staleEstimatePreview.fields.amount, '1,400元(不含交通)')
|
||||
})
|
||||
|
||||
test('application preview editor refreshes transport estimate after mode change', async () => {
|
||||
|
||||
@@ -190,3 +190,87 @@ test('legacy risk text falls back to semantic visibility defaults', () => {
|
||||
assert.equal(resolveRiskActionability(legacyFlag, { businessStage: 'reimbursement' }), 'fixable_by_submitter')
|
||||
assert.equal(resolveRiskVisibilityScope(legacyFlag, { businessStage: 'reimbursement' }), 'submitter')
|
||||
})
|
||||
|
||||
test('application submitter can see fixable policy/trip risks in detail', () => {
|
||||
// 申请单申请人在详情页可见可自行整改的风险(信息完整性、差旅、金额),
|
||||
// 以便申请时知晓风险及原因并补充修正。
|
||||
const cards = [
|
||||
{
|
||||
id: 'application-fields-missing',
|
||||
businessStage: 'expense_application',
|
||||
tone: 'low',
|
||||
risk: '差旅申请基础信息不完整,请补充地点、事由、起止时间和预计金额。',
|
||||
risk_domain: 'policy',
|
||||
visibility_scope: 'submitter',
|
||||
actionability: 'fixable_by_submitter'
|
||||
},
|
||||
{
|
||||
id: 'budget-detail',
|
||||
businessStage: 'expense_application',
|
||||
tone: 'high',
|
||||
risk: '预算可用余额不足。',
|
||||
risk_domain: 'budget',
|
||||
visibility_scope: 'budget_manager',
|
||||
actionability: 'budget_governance'
|
||||
},
|
||||
{
|
||||
id: 'profile-detail',
|
||||
businessStage: 'expense_application',
|
||||
tone: 'medium',
|
||||
risk: '历史差旅画像异常。',
|
||||
risk_domain: 'profile',
|
||||
visibility_scope: 'leader',
|
||||
actionability: 'review_decision'
|
||||
}
|
||||
]
|
||||
|
||||
const visibleCards = filterRiskCardsForVisibility(cards, { request: applicationRequest, currentUser: submitter })
|
||||
// 申请人只可见 fixable_by_submitter 的信息完整性类风险,
|
||||
// budget 走预算审批人、profile 走领导,申请人均不可见。
|
||||
assert.deepEqual(visibleCards.map((card) => card.id), ['application-fields-missing'])
|
||||
})
|
||||
|
||||
test('application leader can see review_decision risks that submitter cannot', () => {
|
||||
// 审批人可见 review_decision 类风险(画像、审批流程等),
|
||||
// 满足诉求2:提交后领导能看到风险点。
|
||||
const cards = [
|
||||
{
|
||||
id: 'profile-detail',
|
||||
businessStage: 'expense_application',
|
||||
tone: 'medium',
|
||||
risk: '历史差旅画像异常。',
|
||||
risk_domain: 'profile',
|
||||
visibility_scope: 'leader',
|
||||
actionability: 'review_decision'
|
||||
},
|
||||
{
|
||||
id: 'application-fields-missing',
|
||||
businessStage: 'expense_application',
|
||||
tone: 'low',
|
||||
risk: '差旅申请基础信息不完整。',
|
||||
risk_domain: 'policy',
|
||||
visibility_scope: 'submitter',
|
||||
actionability: 'fixable_by_submitter'
|
||||
}
|
||||
]
|
||||
|
||||
const visibleCards = filterRiskCardsForVisibility(cards, {
|
||||
request: applicationRequest,
|
||||
currentUser: { id: 'EMP-P7', name: '直属领导' },
|
||||
canViewApprovalRiskAdvice: true
|
||||
})
|
||||
assert.deepEqual(visibleCards.map((card) => card.id), ['profile-detail', 'application-fields-missing'])
|
||||
})
|
||||
|
||||
test('application fixable risks derive submitter semantics without hardcoded leader fallback', () => {
|
||||
// 验证申请单阶段 policy/trip/amount 域不再被硬编码为 leader/review_decision,
|
||||
// 而是沿用与报销单一致的 fixable_by_submitter 语义。
|
||||
const policyFlag = {
|
||||
source: 'submission_review',
|
||||
severity: 'low',
|
||||
message: '差旅申请基础信息不完整。',
|
||||
business_stage: 'expense_application'
|
||||
}
|
||||
assert.equal(resolveRiskActionability(policyFlag, { businessStage: 'expense_application' }), 'fixable_by_submitter')
|
||||
assert.equal(resolveRiskVisibilityScope(policyFlag, { businessStage: 'expense_application' }), 'submitter')
|
||||
})
|
||||
|
||||
@@ -645,8 +645,8 @@ test('AI advice template renders grouped section titles with completion before r
|
||||
assert.match(detailViewScript, /const hasVisibleRiskCards = computed/)
|
||||
assert.match(detailViewScript, /const showCompactSafeAdvice = computed/)
|
||||
assert.match(detailViewScript, /const showAiAdvicePanel = computed\(\(\) => \(/)
|
||||
assert.match(detailViewScript, /isCurrentApplicant\.value && !isApplicationDocument\.value && hasVisibleRiskCards\.value/)
|
||||
assert.match(detailViewScript, /return '风险提示'/)
|
||||
assert.match(detailViewScript, /isCurrentApplicant\.value && hasVisibleRiskCards\.value/)
|
||||
assert.match(detailViewScript, /isApplicationDocument\.value \? '申请风险提示' : '风险提示'/)
|
||||
assert.match(detailViewScript, /return isEditableRequest\.value \? 'AI建议' : '风险提示'/)
|
||||
assert.doesNotMatch(detailViewScript, /return '报销风险提示'/)
|
||||
assert.match(detailViewScript, /canViewApprovalRiskAdvice\.value && aiAdvice\.value\.riskCards\.length > 0/)
|
||||
|
||||
@@ -48,7 +48,13 @@ test('AI mode offers an inline application shortcut when no candidate applicatio
|
||||
assert.match(aiMode, /!candidates\.length/)
|
||||
assert.match(aiMode, /ai_application_start_inline/)
|
||||
assert.match(aiMode, /buildRequiredApplicationMissingText/)
|
||||
assert.match(aiMode, /function startAiApplicationDraft/)
|
||||
assert.match(aiMode, /function startAiApplicationPreview/)
|
||||
assert.match(aiMode, /buildLocalApplicationPreview/)
|
||||
assert.match(aiMode, /buildLocalApplicationPreviewMessage/)
|
||||
assert.match(aiMode, /refreshApplicationPreviewEstimate/)
|
||||
assert.match(aiMode, /applicationPreview:\s*preview/)
|
||||
assert.doesNotMatch(aiMode, /function startAiApplicationDraft/)
|
||||
assert.doesNotMatch(aiMode, /buildAiApplicationStepPrompt/)
|
||||
})
|
||||
|
||||
test('AI mode steward reimbursement action opens expense scene selection locally', () => {
|
||||
@@ -72,9 +78,28 @@ test('AI mode attaches required application lookup result before steward plannin
|
||||
assert.match(aiMode, /await attachAiRequiredApplicationGate\(planRequest, prompt\)/)
|
||||
})
|
||||
|
||||
test('AI mode automatically continues required application gate decisions from steward plan', () => {
|
||||
assert.match(aiMode, /function continueAiRequiredApplicationGateFromPlan\(normalizedPlan\)/)
|
||||
assert.match(aiMode, /flow\.flowId === 'travel_application'[\s\S]*startAiApplicationDraft\('travel', '差旅费'/)
|
||||
assert.match(aiMode, /flow\.flowId === 'travel_reimbursement'[\s\S]*startAiExpenseDraft\('travel', '差旅费', true/)
|
||||
assert.match(aiMode, /continueAiRequiredApplicationGateFromPlan\(normalizedPlan\)/)
|
||||
test('AI mode handles document query prompts locally before steward planning', () => {
|
||||
assert.match(aiMode, /resolveAiDocumentQueryIntent\(prompt/)
|
||||
assert.match(aiMode, /async function handleAiDocumentQueryIntent/)
|
||||
assert.match(aiMode, /buildAiDocumentQueryConditionSummary/)
|
||||
assert.match(aiMode, /filterAiDocumentQueryRecords\(payload, intent\)/)
|
||||
assert.match(aiMode, /fetchApprovalExpenseClaims/)
|
||||
assert.match(aiMode, /buildAiDocumentQueryMessage/)
|
||||
assert.match(aiMode, /AI_DOCUMENT_QUERY_STEP_DELAY_MS/)
|
||||
assert.match(aiMode, /async function updateAiDocumentQueryThinking/)
|
||||
assert.match(aiMode, /解析自然语言筛选条件/)
|
||||
assert.match(aiMode, /查询业务单据接口/)
|
||||
assert.match(aiMode, /组合筛选单据/)
|
||||
assert.match(aiMode, /if \(await handleAiDocumentQueryIntent\(prompt, pendingMessage\)\) \{[\s\S]*return[\s\S]*\}/)
|
||||
assert.match(aiMode, /emit\('open-document', buildAiDocumentDetailRequest\(detailReference\)\)/)
|
||||
})
|
||||
|
||||
test('AI mode continues required application gate decisions into table preview from steward plan', () => {
|
||||
assert.match(aiMode, /function continueAiRequiredApplicationGateFromPlan\(normalizedPlan, prompt = ''\)/)
|
||||
assert.match(aiMode, /flow\.flowId === 'travel_application'[\s\S]*void startAiApplicationPreview\('travel', '差旅费', prompt/)
|
||||
assert.match(aiMode, /flow\.flowId === 'travel_reimbursement'[\s\S]*startAiExpenseDraft\('travel', '差旅费', true/)
|
||||
assert.match(aiMode, /continueAiRequiredApplicationGateFromPlan\(normalizedPlan, prompt\)/)
|
||||
assert.match(aiMode, /class="workbench-ai-application-preview application-preview-shell"/)
|
||||
assert.match(aiMode, /resolveInlineApplicationPreviewRows\(message\)/)
|
||||
assert.match(aiMode, /commitInlineApplicationPreviewEditor\(message\)/)
|
||||
})
|
||||
|
||||
@@ -194,6 +194,7 @@ test('personal workbench view swaps the traditional dashboard with the AI mode s
|
||||
assert.match(workbenchView, /:sidebar-command="aiSidebarCommand"/)
|
||||
assert.match(workbenchView, /@conversation-change="emit\('ai-conversation-change', \$event\)"/)
|
||||
assert.match(workbenchView, /@conversation-history-change="emit\('ai-conversation-history-change', \$event\)"/)
|
||||
assert.match(workbenchView, /@open-document="emit\('open-document', \$event\)"/)
|
||||
assert.match(workbenchView, /<PersonalWorkbench[\s\S]*v-else[\s\S]*key="traditional"/)
|
||||
assert.match(workbenchView, /workbenchMode:\s*\{[\s\S]*type:\s*String,[\s\S]*default:\s*'traditional'/)
|
||||
assert.match(workbenchView, /aiSidebarCommand:\s*\{[\s\S]*type:\s*Object/)
|
||||
@@ -233,7 +234,7 @@ test('AI mode screen follows the approved reference structure', () => {
|
||||
assert.match(aiMode, /class="workbench-ai-thread"[\s\S]*@scroll\.passive="handleInlineConversationScroll"/)
|
||||
assert.match(aiMode, /workbench-ai-answer-card/)
|
||||
assert.match(aiMode, /workbench-ai-answer-markdown/)
|
||||
assert.match(aiMode, /v-html="renderInlineMarkdown\(message\.content\)"/)
|
||||
assert.match(aiMode, /v-html="renderInlineConversationHtml\(message\.content\)"/)
|
||||
assert.match(aiMode, /workbench-ai-message-actions/)
|
||||
assert.match(aiMode, /workbench-ai-conversation-actions/)
|
||||
assert.match(aiMode, /scrollInlineConversationToTop/)
|
||||
@@ -257,17 +258,28 @@ test('AI mode screen follows the approved reference structure', () => {
|
||||
assert.doesNotMatch(aiMode, /思考过程/)
|
||||
assert.doesNotMatch(aiMode, /message\.pending \?/)
|
||||
assert.match(aiMode, /placeholder="继续和小财管家对话\.\.\."/)
|
||||
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-card\)/)
|
||||
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-card__head\)/)
|
||||
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-card__body\)/)
|
||||
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-card__foot\)/)
|
||||
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-card__amount\)/)
|
||||
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-card__meta\)/)
|
||||
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-html-action-link\)/)
|
||||
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-html-table-wrap\)/)
|
||||
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-html-image-frame\)/)
|
||||
assert.match(aiMode, /import \{ fetchSettings \} from '\.\.\/\.\.\/services\/settings\.js'/)
|
||||
assert.match(aiMode, /import \{ fetchStewardPlan, fetchStewardPlanStream \} from '\.\.\/\.\.\/services\/steward\.js'/)
|
||||
assert.match(aiMode, /import \{ useWorkbenchComposerDate \} from '\.\.\/\.\.\/composables\/useWorkbenchComposerDate\.js'/)
|
||||
assert.match(aiMode, /loadAiWorkbenchConversationHistory/)
|
||||
assert.match(aiMode, /saveAiWorkbenchConversation/)
|
||||
assert.match(aiMode, /deleteAiWorkbenchConversation/)
|
||||
assert.match(aiMode, /import \{ renderMarkdown \} from '\.\.\/\.\.\/utils\/markdown\.js'/)
|
||||
assert.match(aiMode, /import \{ renderAiConversationHtml \} from '\.\.\/\.\.\/utils\/aiConversationHtmlRenderer\.js'/)
|
||||
assert.match(aiMode, /function renderInlineConversationHtml\(content\) \{[\s\S]*return renderAiConversationHtml\(content\)[\s\S]*\}/)
|
||||
assert.doesNotMatch(aiMode, /import \{ renderMarkdown \} from '\.\.\/\.\.\/utils\/markdown\.js'/)
|
||||
assert.match(aiMode, /buildStewardPlanRequest/)
|
||||
assert.match(aiMode, /buildStewardPlanMessageText/)
|
||||
assert.match(aiMode, /buildStewardSuggestedActions/)
|
||||
assert.match(aiMode, /const emit = defineEmits\(\['conversation-change', 'conversation-history-change'\]\)/)
|
||||
assert.match(aiMode, /const emit = defineEmits\(\['conversation-change', 'conversation-history-change', 'open-document'\]\)/)
|
||||
assert.match(aiMode, /function startInlineConversation\(prompt, entry = \{\}, files = \[\]\)/)
|
||||
assert.match(aiMode, /activateInlineConversation\(\{[\s\S]*title:[\s\S]*\}\)[\s\S]*conversationMessages\.value\.push\(createInlineMessage\('user'/)
|
||||
assert.match(aiMode, /persistCurrentConversation\(\)/)
|
||||
@@ -355,6 +367,9 @@ test('AI mode screen follows the approved reference structure', () => {
|
||||
assert.match(aiModeStyles, /\.workbench-ai-answer-card\s*\{[\s\S]*box-shadow:\s*none;[\s\S]*backdrop-filter:\s*none;/)
|
||||
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown\s*\{[\s\S]*line-height:\s*1\.86;/)
|
||||
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(h3\)\s*\{[\s\S]*font-size:\s*21px;/)
|
||||
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-html-focus-grid\)\s*\{[\s\S]*border-left:\s*3px solid/)
|
||||
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-html-focus-card\)\s*\{[\s\S]*background:\s*transparent;/)
|
||||
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-html-step-index\)\s*\{[\s\S]*background:\s*transparent;[\s\S]*font-size:\s*17px;/)
|
||||
assert.match(aiModeStyles, /\.workbench-ai-date-popover\s*\{[\s\S]*animation:\s*workbenchAiPopoverIn/)
|
||||
assert.match(aiModeStyles, /\.workbench-ai-send-btn:not\(:disabled\)\s*\{[\s\S]*linear-gradient\(135deg,[\s\S]*#1d4ed8/)
|
||||
assert.match(aiModeStyles, /\.workbench-ai-composer--inline\s*\{[\s\S]*min-height:\s*126px;[\s\S]*box-shadow:\s*none;/)
|
||||
|
||||
Reference in New Issue
Block a user