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) })