2026-06-20 10:17:37 +08:00
|
|
|
import assert from 'node:assert/strict'
|
|
|
|
|
|
|
|
|
|
import {
|
|
|
|
|
AI_APPLICATION_ACTION_SAVE_DRAFT,
|
|
|
|
|
AI_APPLICATION_ACTION_SUBMIT,
|
2026-07-14 17:16:23 +08:00
|
|
|
forgetAiApplicationMemory,
|
2026-07-14 14:37:53 +08:00
|
|
|
registerAiApplicationPreviewDecision,
|
2026-07-14 17:16:23 +08:00
|
|
|
resolveAiApplicationLearningReceipts,
|
2026-06-20 14:42:04 +08:00
|
|
|
runAiApplicationPreviewAction
|
2026-06-20 10:17:37 +08:00
|
|
|
} from '../src/services/aiApplicationPreviewActions.js'
|
|
|
|
|
|
2026-06-20 14:42:04 +08:00
|
|
|
async function testSubmitActionUsesFastPreviewEndpoint() {
|
|
|
|
|
let capturedUrl = ''
|
|
|
|
|
let capturedOptions = null
|
2026-06-20 10:17:37 +08:00
|
|
|
|
2026-06-20 14:42:04 +08:00
|
|
|
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: '直属领导审批'
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-06-20 10:17:37 +08:00
|
|
|
|
2026-06-20 14:42:04 +08:00
|
|
|
await runAiApplicationPreviewAction({
|
2026-06-20 10:17:37 +08:00
|
|
|
actionType: AI_APPLICATION_ACTION_SUBMIT,
|
2026-06-20 14:42:04 +08:00
|
|
|
applicationPreview: {
|
2026-07-14 14:37:53 +08:00
|
|
|
decisionId: 'decision-fast-submit',
|
2026-06-20 14:42:04 +08:00
|
|
|
fields: {
|
|
|
|
|
applicationType: '差旅费用申请',
|
|
|
|
|
time: '2026-07-01 至 2026-07-03',
|
|
|
|
|
location: '北京',
|
|
|
|
|
reason: '项目实施',
|
|
|
|
|
days: '3天',
|
|
|
|
|
transportMode: '火车',
|
|
|
|
|
amount: '1000元'
|
2026-07-14 11:10:55 +08:00
|
|
|
},
|
|
|
|
|
aiDecisionFeedback: {
|
|
|
|
|
schemaVersion: 1,
|
|
|
|
|
changedFields: [
|
|
|
|
|
{
|
|
|
|
|
fieldKey: 'transportMode',
|
|
|
|
|
suggestedValue: '飞机',
|
|
|
|
|
finalValue: '火车'
|
|
|
|
|
}
|
|
|
|
|
]
|
2026-06-20 14:42:04 +08:00
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
currentUser: { username: 'zhangsan@example.com', name: '张三' },
|
2026-07-14 14:37:53 +08:00
|
|
|
conversationId: 'conversation-fast-submit',
|
|
|
|
|
requestId: 'action-fast-submit'
|
2026-06-20 10:17:37 +08:00
|
|
|
})
|
|
|
|
|
|
2026-06-20 14:42:04 +08:00
|
|
|
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')
|
2026-07-14 14:37:53 +08:00
|
|
|
assert.equal(body.action_type, 'submit')
|
|
|
|
|
assert.equal(body.decision_id, 'decision-fast-submit')
|
|
|
|
|
assert.equal(body.request_id, 'action-fast-submit')
|
2026-06-20 14:42:04 +08:00
|
|
|
assert.equal(body.context_json.application_preview.fields.transportMode, '火车')
|
2026-07-14 11:10:55 +08:00
|
|
|
assert.deepEqual(body.context_json.application_preview.aiDecisionFeedback.changedFields, [
|
|
|
|
|
{
|
|
|
|
|
fieldKey: 'transportMode',
|
|
|
|
|
suggestedValue: '飞机',
|
|
|
|
|
finalValue: '火车'
|
|
|
|
|
}
|
|
|
|
|
])
|
2026-06-20 14:42:04 +08:00
|
|
|
}
|
2026-06-20 10:17:37 +08:00
|
|
|
|
2026-06-20 21:44:16 +08:00
|
|
|
async function testSaveDraftActionUsesFastPreviewEndpoint() {
|
2026-06-20 14:42:04 +08:00
|
|
|
let capturedUrl = ''
|
2026-06-20 21:44:16 +08:00
|
|
|
let capturedOptions = null
|
2026-06-20 14:42:04 +08:00
|
|
|
|
2026-06-20 21:44:16 +08:00
|
|
|
global.fetch = async (url, options) => {
|
2026-06-20 14:42:04 +08:00
|
|
|
capturedUrl = String(url)
|
2026-06-20 21:44:16 +08:00
|
|
|
capturedOptions = options
|
2026-06-20 14:42:04 +08:00
|
|
|
return {
|
|
|
|
|
ok: true,
|
|
|
|
|
async json() {
|
2026-06-20 21:44:16 +08:00
|
|
|
return {
|
|
|
|
|
status: 'succeeded',
|
|
|
|
|
result: {
|
|
|
|
|
draft_payload: {
|
|
|
|
|
claim_id: 'claim-fast-draft',
|
|
|
|
|
claim_no: 'AP-20260620-DRAFT',
|
|
|
|
|
status: 'draft',
|
|
|
|
|
approval_stage: '待提交'
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-06-20 14:42:04 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-06-20 10:17:37 +08:00
|
|
|
|
2026-06-20 14:42:04 +08:00
|
|
|
await runAiApplicationPreviewAction({
|
|
|
|
|
actionType: AI_APPLICATION_ACTION_SAVE_DRAFT,
|
|
|
|
|
applicationPreview: { fields: { reason: '项目实施' } },
|
|
|
|
|
currentUser: { username: 'zhangsan@example.com', name: '张三' }
|
2026-06-20 10:17:37 +08:00
|
|
|
})
|
|
|
|
|
|
2026-06-20 21:44:16 +08:00
|
|
|
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')
|
2026-07-14 14:37:53 +08:00
|
|
|
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元'
|
2026-07-14 17:16:23 +08:00
|
|
|
},
|
|
|
|
|
memoryApplications: [{
|
|
|
|
|
memoryId: 'memory-transport-1',
|
|
|
|
|
fieldKey: 'transport_mode',
|
|
|
|
|
value: '火车',
|
|
|
|
|
status: 'active',
|
|
|
|
|
evidenceCount: 4,
|
|
|
|
|
approvedEvidenceCount: 3,
|
|
|
|
|
message: '根据历史申请偏好填入'
|
|
|
|
|
}]
|
2026-07-14 14:37:53 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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')
|
2026-07-14 17:16:23 +08:00
|
|
|
assert.deepEqual(preview.memoryApplications, [{
|
|
|
|
|
memoryId: 'memory-transport-1',
|
|
|
|
|
fieldKey: 'transport_mode',
|
|
|
|
|
fieldLabel: '出行方式',
|
|
|
|
|
value: '火车',
|
|
|
|
|
status: 'applied',
|
|
|
|
|
evidenceCount: 4,
|
|
|
|
|
approvedEvidenceCount: 3,
|
|
|
|
|
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,
|
|
|
|
|
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')
|
2026-06-20 14:42:04 +08:00
|
|
|
}
|
|
|
|
|
|
2026-06-26 22:42:23 +08:00
|
|
|
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'])
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-14 16:03:05 +08:00
|
|
|
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')
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-20 14:42:04 +08:00
|
|
|
async function run() {
|
|
|
|
|
await testSubmitActionUsesFastPreviewEndpoint()
|
2026-06-20 21:44:16 +08:00
|
|
|
await testSaveDraftActionUsesFastPreviewEndpoint()
|
2026-07-14 14:37:53 +08:00
|
|
|
await testRegistrationUsesServerCanonicalPreview()
|
2026-07-14 17:16:23 +08:00
|
|
|
await testLearningReceiptNormalizationAndForgetEndpoint()
|
2026-06-26 22:42:23 +08:00
|
|
|
await testEditDraftActionCarriesClaimAndEditableFields()
|
2026-07-14 16:03:05 +08:00
|
|
|
await testApplicationActionSourceCanBeConfiguredWithoutChangingWorkbenchDefaults()
|
2026-06-20 14:42:04 +08:00
|
|
|
console.log('ai-application-preview-actions tests passed')
|
|
|
|
|
}
|
2026-06-20 10:17:37 +08:00
|
|
|
|
2026-06-20 14:42:04 +08:00
|
|
|
run().catch((error) => {
|
|
|
|
|
console.error(error)
|
|
|
|
|
process.exit(1)
|
2026-06-20 10:17:37 +08:00
|
|
|
})
|