Files
X-Financial/web/tests/ai-application-preview-actions.test.mjs

411 lines
12 KiB
JavaScript
Raw Normal View History

import assert from 'node:assert/strict'
import {
AI_APPLICATION_ACTION_SAVE_DRAFT,
AI_APPLICATION_ACTION_SUBMIT,
forgetAiApplicationMemory,
normalizeAiApplicationMemory,
registerAiApplicationPreviewDecision,
resolveAiApplicationLearningReceipts,
runAiApplicationPreviewAction
} from '../src/services/aiApplicationPreviewActions.js'
async function testSubmitActionUsesFastPreviewEndpoint() {
let capturedUrl = ''
let capturedOptions = null
global.fetch = async (url, options) => {
capturedUrl = String(url)
capturedOptions = options
return {
ok: true,
async json() {
return {
status: 'succeeded',
result: {
draft_payload: {
claim_id: 'claim-fast-submit',
claim_no: 'AP-20260620-FAST',
status: 'submitted',
approval_stage: '直属领导审批'
}
}
}
}
}
}
await runAiApplicationPreviewAction({
actionType: AI_APPLICATION_ACTION_SUBMIT,
applicationPreview: {
decisionId: 'decision-fast-submit',
fields: {
applicationType: '差旅费用申请',
time: '2026-07-01 至 2026-07-03',
location: '北京',
reason: '项目实施',
days: '3天',
transportMode: '火车',
amount: '1000元'
},
aiDecisionFeedback: {
schemaVersion: 1,
changedFields: [
{
fieldKey: 'transportMode',
suggestedValue: '飞机',
finalValue: '火车'
}
]
}
},
currentUser: { username: 'zhangsan@example.com', name: '张三' },
conversationId: 'conversation-fast-submit',
requestId: 'action-fast-submit'
})
assert.equal(capturedUrl, '/api/v1/reimbursements/application-preview-action')
assert.equal(capturedOptions.method, 'POST')
const body = JSON.parse(capturedOptions.body)
assert.equal(body.context_json.session_type, 'application')
assert.equal(body.context_json.application_stage, 'expense_application')
assert.equal(body.action_type, 'submit')
assert.equal(body.decision_id, 'decision-fast-submit')
assert.equal(body.request_id, 'action-fast-submit')
assert.equal(body.context_json.application_preview.fields.transportMode, '火车')
assert.deepEqual(body.context_json.application_preview.aiDecisionFeedback.changedFields, [
{
fieldKey: 'transportMode',
suggestedValue: '飞机',
finalValue: '火车'
}
])
}
async function testSaveDraftActionUsesFastPreviewEndpoint() {
let capturedUrl = ''
let capturedOptions = null
global.fetch = async (url, options) => {
capturedUrl = String(url)
capturedOptions = options
return {
ok: true,
async json() {
return {
status: 'succeeded',
result: {
draft_payload: {
claim_id: 'claim-fast-draft',
claim_no: 'AP-20260620-DRAFT',
status: 'draft',
approval_stage: '待提交'
}
}
}
}
}
}
await runAiApplicationPreviewAction({
actionType: AI_APPLICATION_ACTION_SAVE_DRAFT,
applicationPreview: { fields: { reason: '项目实施' } },
currentUser: { username: 'zhangsan@example.com', name: '张三' }
})
assert.equal(capturedUrl, '/api/v1/reimbursements/application-preview-action')
assert.equal(capturedOptions.method, 'POST')
const body = JSON.parse(capturedOptions.body)
assert.equal(body.context_json.application_action, 'save_draft')
assert.equal(body.context_json.application_save_mode, true)
assert.equal(body.context_json.application_stage, 'expense_application')
assert.equal(body.action_type, 'save_draft')
assert.equal('decision_id' in body, false)
assert.equal('request_id' in body, false)
}
async function testRegistrationUsesServerCanonicalPreview() {
let capturedUrl = ''
let capturedOptions = null
global.fetch = async (url, options) => {
capturedUrl = String(url)
capturedOptions = options
return {
ok: true,
async json() {
return {
decision_id: 'decision-issued-1',
decision_source: 'rule',
expires_at: '2026-07-14T10:30:00Z',
application_preview: {
fields: {
reason: '服务端规范事由',
amount: '1800元'
},
memoryApplications: [{
memoryId: 'memory-transport-1',
fieldKey: 'transport_mode',
value: '火车',
status: 'active',
evidenceCount: 4,
approvedEvidenceCount: 3,
scope_type: 'department',
scope_id: 'department-shanghai-delivery',
scope_label: '上海交付部',
source: 'admin_managed_org',
priority: 200,
effective_confidence: 0.92,
conflicts: [{ scope_type: 'user', reason: 'lower_priority' }],
can_revoke: false,
message: '根据历史申请偏好填入'
}]
}
}
}
}
}
const preview = await registerAiApplicationPreviewDecision({
applicationPreview: {
fields: {
reason: '客户端事由',
location: '上海'
}
},
conversationId: 'conversation-issued-1',
message: '去上海做客户现场实施',
requestId: 'preview-request-1'
})
assert.equal(capturedUrl, '/api/v1/reimbursements/application-previews')
assert.equal(capturedOptions.method, 'POST')
assert.deepEqual(JSON.parse(capturedOptions.body), {
message: '去上海做客户现场实施',
conversation_id: 'conversation-issued-1',
request_id: 'preview-request-1'
})
assert.equal(preview.decisionId, 'decision-issued-1')
assert.equal(preview.decisionSource, 'rule')
assert.equal(preview.fields.reason, '服务端规范事由')
assert.equal(preview.fields.location, '上海')
assert.equal(preview.decisionTrackingStatus, 'registered')
assert.deepEqual(preview.memoryApplications, [{
memoryId: 'memory-transport-1',
fieldKey: 'transport_mode',
fieldLabel: '出行方式',
value: '火车',
status: 'applied',
evidenceCount: 4,
approvedEvidenceCount: 3,
scopeType: 'department',
scopeId: 'department-shanghai-delivery',
scopeLabel: '上海交付部',
source: 'admin_managed_org',
priority: 200,
effectiveConfidence: 0.92,
conflicts: [{ scope_type: 'user', reason: 'lower_priority' }],
canRevoke: false,
message: '根据历史申请偏好填入'
}])
}
async function testLearningReceiptNormalizationAndForgetEndpoint() {
const receipts = resolveAiApplicationLearningReceipts({
result: {
learning_receipts: [{
memory_id: 'memory-reason-1',
field_key: 'reason',
field_label: '申请事由',
value: '客户现场实施',
status: 'candidate',
evidence_count: 2,
approved_evidence_count: 1,
message: '再确认一次后可形成稳定偏好'
}]
}
})
assert.deepEqual(receipts, [{
memoryId: 'memory-reason-1',
fieldKey: 'reason',
fieldLabel: '申请事由',
value: '客户现场实施',
status: 'candidate',
evidenceCount: 2,
approvedEvidenceCount: 1,
scopeType: 'user',
scopeId: '',
scopeLabel: '个人偏好',
source: 'verified_user_history',
priority: 100,
effectiveConfidence: null,
conflicts: [],
canRevoke: true,
message: '再确认一次后可形成稳定偏好'
}])
let capturedUrl = ''
let capturedOptions = null
global.fetch = async (url, options) => {
capturedUrl = String(url)
capturedOptions = options
return {
ok: true,
async json() {
return { status: 'forgotten' }
}
}
}
await forgetAiApplicationMemory('memory/reason 1')
assert.equal(
capturedUrl,
'/api/v1/expense-application-memories/memory%2Freason%201'
)
assert.equal(capturedOptions.method, 'DELETE')
}
function testHierarchicalMemoryNormalizationKeepsCompatibilityAndPermissions() {
const enterpriseMemory = normalizeAiApplicationMemory({
memory_id: 'memory-enterprise-transport',
field_key: 'transport_mode',
value: '火车',
status: 'applied',
scope_type: 'enterprise',
scope_id: 'tenant-acme',
scope_label: '企业统一制度',
source: 'admin_managed_org',
priority: '300',
effective_confidence: '0.88',
conflicts: [{ scope_type: 'department', reason: 'lower_priority' }],
can_revoke: false
})
assert.deepEqual(enterpriseMemory, {
memoryId: 'memory-enterprise-transport',
fieldKey: 'transport_mode',
fieldLabel: '出行方式',
value: '火车',
status: 'applied',
evidenceCount: 0,
approvedEvidenceCount: 0,
scopeType: 'enterprise',
scopeId: 'tenant-acme',
scopeLabel: '企业统一制度',
source: 'admin_managed_org',
priority: 300,
effectiveConfidence: 0.88,
conflicts: [{ scope_type: 'department', reason: 'lower_priority' }],
canRevoke: false,
message: ''
})
const legacyPersonalMemory = normalizeAiApplicationMemory({
memoryId: 'memory-legacy-user',
fieldKey: 'transport_mode',
value: '飞机',
status: 'active'
})
assert.equal(legacyPersonalMemory.scopeType, 'user')
assert.equal(legacyPersonalMemory.source, 'verified_user_history')
assert.equal(legacyPersonalMemory.canRevoke, true)
assert.equal(legacyPersonalMemory.status, 'applied')
}
async function testEditDraftActionCarriesClaimAndEditableFields() {
let capturedOptions = null
global.fetch = async (_url, options) => {
capturedOptions = options
return {
ok: true,
async json() {
return {
status: 'succeeded',
result: {
draft_payload: {
claim_id: 'claim-edit-application',
claim_no: 'AP-20260620-EDIT',
status: 'draft',
approval_stage: '待提交'
}
}
}
}
}
}
await runAiApplicationPreviewAction({
actionType: AI_APPLICATION_ACTION_SAVE_DRAFT,
applicationPreview: {
applicationEditMode: true,
editableFields: ['reason', 'time', 'location', 'transportMode'],
fields: {
applicationType: '差旅费用申请',
time: '2026-07-01 至 2026-07-03',
location: '北京',
reason: '项目实施',
days: '3天',
transportMode: '火车',
amount: '1000元'
}
},
currentUser: { username: 'zhangsan@example.com', name: '张三' },
draftPayload: {
claim_id: 'claim-edit-application',
claim_no: 'AP-20260620-EDIT',
status: 'returned'
}
})
const body = JSON.parse(capturedOptions.body)
assert.equal(body.context_json.application_edit_claim_id, 'claim-edit-application')
assert.equal(body.context_json.application_edit_mode, true)
assert.deepEqual(body.context_json.application_editable_fields, ['reason', 'time', 'location', 'transportMode'])
}
async function testApplicationActionSourceCanBeConfiguredWithoutChangingWorkbenchDefaults() {
const requests = []
global.fetch = async (_url, options) => {
requests.push(JSON.parse(options.body))
return {
ok: true,
async json() {
return { status: 'succeeded', result: {} }
}
}
}
await runAiApplicationPreviewAction({
actionType: AI_APPLICATION_ACTION_SAVE_DRAFT,
applicationPreview: { fields: { reason: '默认工作台来源' } }
})
await runAiApplicationPreviewAction({
actionType: AI_APPLICATION_ACTION_SAVE_DRAFT,
applicationPreview: { fields: { reason: '小财管家来源' } },
entrySource: 'application',
source: 'financial_assistant',
requestSource: 'assistant_preview_action'
})
assert.equal(requests[0].source, 'user_message')
assert.equal(requests[0].context_json.entry_source, 'workbench_ai_inline')
assert.equal(requests[0].context_json.source, 'workbench')
assert.equal(requests[1].source, 'assistant_preview_action')
assert.equal(requests[1].context_json.entry_source, 'application')
assert.equal(requests[1].context_json.source, 'financial_assistant')
}
async function run() {
await testSubmitActionUsesFastPreviewEndpoint()
await testSaveDraftActionUsesFastPreviewEndpoint()
await testRegistrationUsesServerCanonicalPreview()
await testLearningReceiptNormalizationAndForgetEndpoint()
testHierarchicalMemoryNormalizationKeepsCompatibilityAndPermissions()
await testEditDraftActionCarriesClaimAndEditableFields()
await testApplicationActionSourceCanBeConfiguredWithoutChangingWorkbenchDefaults()
console.log('ai-application-preview-actions tests passed')
}
run().catch((error) => {
console.error(error)
process.exit(1)
})