Files
X-Financial/web/tests/risk-visibility.test.mjs
caoxiaozhu 304bbe1fd4 feat(web): 工作台 AI 模式报销预审与文档查询模型拆分
- 新增 aiApplicationPrecheckModel/aiDocumentQueryModel/aiApplicationPreviewActions/aiConversationHtmlRenderer 四个独立模型与服务,按职责从主组件拆出
- PersonalWorkbenchAiMode 接入拆分后的预审、文档查询与 HTML 渲染逻辑,配合 markdown 工具增强结构化展示
- 文档中心与归档筛选、风险可见性、申请预览等工具同步适配,补充对应单元测试
- 新增 AI 文档卡片背景资源
2026-06-20 10:17:37 +08:00

277 lines
8.5 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 { buildDetailAlerts } from '../src/utils/detailAlerts.js'
import {
canViewRiskForContext,
filterRiskCardsForVisibility,
resolveRiskActionability,
resolveRiskDomain,
resolveRiskVisibilityScope
} from '../src/utils/riskVisibility.js'
const submitter = {
id: 'EMP-001',
employeeId: 'EMP-001',
name: '张三',
roleCodes: []
}
const budgetManager = {
id: 'EMP-P8',
name: '预算管理员',
grade: 'P8',
departmentName: '研发部',
roleCodes: ['budget_monitor']
}
const financeUser = {
id: 'FIN-001',
name: '财务',
roleCodes: ['finance']
}
const applicationRequest = {
id: 'AP-202606010001',
claimId: 'application-claim-id',
documentTypeCode: 'application',
approvalKey: 'in_progress',
node: '预算管理者审批',
employeeId: 'EMP-001',
departmentName: '研发部',
typeCode: 'travel_application',
typeLabel: '差旅费用申请',
reason: '北京出差申请',
location: '北京',
occurredDisplay: '2026-06-01 至 2026-06-03',
amountValue: 10000,
riskFlags: [
{
source: 'budget_control',
severity: 'high',
label: '预算可用余额不足',
message: '当前部门预算余额不足。',
business_stage: 'expense_application',
risk_domain: 'budget',
visibility_scope: 'budget_manager',
actionability: 'budget_governance'
}
],
expenseItems: []
}
test('application submitter cannot see budget governance alerts in detail topbar', () => {
const alerts = buildDetailAlerts(applicationRequest, { currentUser: submitter })
assert.deepEqual(alerts.map((item) => item.label), ['SLA 催单次数 0'])
})
test('budget manager can see application budget governance alerts', () => {
const alerts = buildDetailAlerts(applicationRequest, { currentUser: budgetManager })
assert.equal(alerts[0].label, '高风险 1 项')
assert.equal(alerts[0].tone, 'danger')
})
test('reimbursement submitter sees only fixable claim risks', () => {
const request = {
id: 'RE-202606010001',
claimId: 'reimbursement-claim-id',
documentTypeCode: 'claim',
approvalKey: 'draft',
node: '待提交',
employeeId: 'EMP-001',
typeCode: 'travel',
expenseItems: []
}
const cards = [
{
id: 'ticket-date',
businessStage: 'reimbursement',
tone: 'high',
risk: '票据日期早于申请行程。',
risk_domain: 'trip',
visibility_scope: 'submitter',
actionability: 'fixable_by_submitter'
},
{
id: 'budget-detail',
businessStage: 'reimbursement',
tone: 'medium',
risk: '预算占用率超过 90%。',
risk_domain: 'budget',
visibility_scope: 'budget_manager',
actionability: 'budget_governance'
}
]
const visibleCards = filterRiskCardsForVisibility(cards, { request, currentUser: submitter })
assert.deepEqual(visibleCards.map((card) => card.id), ['ticket-date'])
})
test('reimbursement detail still shows submitter-fixable attachment risks when viewer identity is incomplete', () => {
const request = {
id: 'RE-20260603083825-876B85XW',
claimId: '2ad80b25-b153-407e-91be-ed2651045fb1',
documentTypeCode: 'claim',
approvalKey: 'draft',
node: 'pending-submit',
employeeId: 'EMP-CLAIM-OWNER',
typeCode: 'travel',
expenseItems: []
}
const currentUserWithoutEmployeeMatch = {
id: 'FRONTEND-AUTH-SNAPSHOT',
employeeId: '',
name: '',
roleCodes: []
}
const cards = [
{
id: 'hotel-limit-risk',
source: 'attachment_analysis',
businessStage: 'reimbursement',
tone: 'high',
risk: 'hotel limit exceeded',
risk_domain: 'invoice',
visibility_scope: 'submitter',
actionability: 'fixable_by_submitter'
}
]
const visibleCards = filterRiskCardsForVisibility(cards, {
request,
currentUser: currentUserWithoutEmployeeMatch
})
assert.deepEqual(visibleCards.map((card) => card.id), ['hotel-limit-risk'])
})
test('finance can see reimbursement compliance risks but not budget governance detail', () => {
const request = {
id: 'RE-202606010002',
documentTypeCode: 'claim',
approvalKey: 'in_progress',
node: '财务审批',
employeeId: 'EMP-001',
typeCode: 'travel',
expenseItems: []
}
assert.equal(
canViewRiskForContext(
{
businessStage: 'reimbursement',
risk: '发票抬头与报销主体不一致。',
risk_domain: 'invoice',
actionability: 'fixable_by_submitter'
},
{ request, currentUser: financeUser }
),
true
)
assert.equal(
canViewRiskForContext(
{
businessStage: 'reimbursement',
risk: '预算余额不足。',
risk_domain: 'budget',
actionability: 'budget_governance'
},
{ request, currentUser: financeUser }
),
false
)
})
test('legacy risk text falls back to semantic visibility defaults', () => {
const legacyFlag = {
source: 'submission_review',
severity: 'high',
message: '住宿发票城市与行程城市不一致。',
business_stage: 'reimbursement'
}
assert.equal(resolveRiskDomain(legacyFlag), 'trip')
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')
})