import assert from 'node:assert/strict' import { readFileSync } from 'node:fs' import test from 'node:test' import { fileURLToPath } from 'node:url' import { executeRiskDispositionAction, normalizeRiskDisposition, normalizeRiskDispositionError, normalizeRiskObservation } from '../src/services/riskObservations.js' const evidenceCard = readFileSync( fileURLToPath(new URL('../src/components/travel/RiskObservationEvidenceCard.vue', import.meta.url)), 'utf8' ) const waiverDialog = readFileSync( fileURLToPath(new URL('../src/components/travel/RiskWaiverActionDialog.vue', import.meta.url)), 'utf8' ) const waiverRecord = readFileSync( fileURLToPath(new URL('../src/components/travel/RiskWaiverRecord.vue', import.meta.url)), 'utf8' ) function jsonResponse(payload) { return { ok: true, status: 200, async json() { return payload } } } test('risk disposition normalizes server-authorized actions and complete waiver audit fields', () => { const disposition = normalizeRiskDisposition({ id: 'disposition-1', observation_id: 'risk-1', lifecycle_status: 'waived', version: 3, available_actions: ['approve_waiver', 'reject_waiver', 'approve_waiver'], read_only_reason: null, waiver_requester_id: 'employee-requester', waiver_requester_name: '申请人甲', waiver_requested_at: '2026-07-16T08:00:00Z', waiver_reason: '客户现场暂时无法取得原件', waiver_scope: '仅限本次单据的行程确认材料', waiver_expires_at: '2026-07-20T08:00:00Z', waiver_conditions_json: ['三日内补交原件', '到期前复核', '三日内补交原件'], waiver_decision: 'approved', waiver_decider_id: 'employee-decider', waiver_decider_name: '决定人乙', waiver_decided_at: '2026-07-16T09:00:00Z', waiver_decision_reason: '补偿控制充分,同意限时豁免', events: [{ id: 'waiver-event-1', action: 'request_waiver', version: 2, actor_id: 'employee-requester', actor_name: '申请人甲', request_id: 'waiver-request-001', comment: '客户现场暂时无法取得原件', after_json: { waiver_scope: '仅限本次单据的行程确认材料' }, created_at: '2026-07-16T08:00:00Z' }] }) assert.deepEqual(disposition.availableActions, ['approve_waiver', 'reject_waiver']) assert.equal(disposition.readOnlyReason, '') assert.equal(disposition.waiverRequesterName, '申请人甲') assert.equal(disposition.waiverReason, '客户现场暂时无法取得原件') assert.equal(disposition.waiverScope, '仅限本次单据的行程确认材料') assert.deepEqual(disposition.waiverConditions, ['三日内补交原件', '到期前复核']) assert.equal(disposition.waiverDeciderName, '决定人乙') assert.equal(disposition.waiverDecisionReason, '补偿控制充分,同意限时豁免') assert.equal(disposition.events[0].actorId, 'employee-requester') assert.equal(disposition.events[0].requestId, 'waiver-request-001') assert.equal( disposition.events[0].afterState.waiver_scope, '仅限本次单据的行程确认材料' ) }) test('observation keeps server actions when the first disposition has not been created', () => { const observation = normalizeRiskDisposition(null) assert.equal(observation, null) const normalized = normalizeRiskObservation({ id: 'risk-first-action', disposition: null, available_actions: ['confirm', 'false_positive'], read_only_reason: '' }) assert.equal(normalized.disposition, null) assert.deepEqual(normalized.availableActions, ['confirm', 'false_positive']) assert.equal(normalized.readOnlyReason, '') }) test('waiver request and decision payloads carry idempotency key and optimistic version', async (t) => { const originalFetch = globalThis.fetch const calls = [] t.after(() => { globalThis.fetch = originalFetch }) globalThis.fetch = async (url, options) => { calls.push({ url: String(url), body: JSON.parse(options.body) }) return jsonResponse({ disposition: { id: 'disposition-1', observation_id: 'risk/1', version: calls.length, available_actions: [] }, event: { id: `event-${calls.length}` }, replayed: false }) } await executeRiskDispositionAction('risk/1', { action: 'request_waiver', expectedVersion: 2, requestId: 'risk-request-waiver-001', waiverReason: '材料暂缺', waiverScope: '仅限当前单据', waiverExpiresAt: '2026-07-20T08:00:00.000Z', waiverConditions: ['三日内补件', '三日内补件', '', '财务复核'] }) await executeRiskDispositionAction('risk/1', { action: 'approve_waiver', expectedVersion: 3, requestId: 'risk-approve-waiver-001', comment: '补偿控制充分,同意限时豁免' }) await executeRiskDispositionAction('risk/1', { action: 'reject_waiver', expectedVersion: 3, requestId: 'risk-reject-waiver-001', comment: '补偿控制不足,拒绝豁免' }) assert.equal(calls[0].url, '/api/v1/risk-observations/risk%2F1/disposition/actions') assert.deepEqual(calls[0].body, { action: 'request_waiver', expected_version: 2, request_id: 'risk-request-waiver-001', waiver_reason: '材料暂缺', waiver_scope: '仅限当前单据', waiver_expires_at: '2026-07-20T08:00:00.000Z', waiver_conditions: ['三日内补件', '财务复核'] }) assert.deepEqual(calls[1].body, { action: 'approve_waiver', expected_version: 3, request_id: 'risk-approve-waiver-001', comment: '补偿控制充分,同意限时豁免' }) assert.deepEqual(calls[2].body, { action: 'reject_waiver', expected_version: 3, request_id: 'risk-reject-waiver-001', comment: '补偿控制不足,拒绝豁免' }) }) test('waiver errors are presented by conflict, permission, validation and timeout type', () => { assert.deepEqual( normalizeRiskDispositionError({ code: 'RISK_DISPOSITION_VERSION_CONFLICT', message: '状态已变化' }), { code: 'RISK_DISPOSITION_VERSION_CONFLICT', title: '风险状态已更新', message: '状态已变化', shouldRefresh: true } ) assert.equal( normalizeRiskDispositionError({ code: 'RISK_WAIVER_DECISION_FORBIDDEN' }).title, '当前账号不能执行此操作' ) assert.equal(normalizeRiskDispositionError({ status: 422 }).code, 'RISK_DISPOSITION_VALIDATION_FAILED') assert.equal(normalizeRiskDispositionError({ code: 'REQUEST_TIMEOUT' }).shouldRefresh, true) }) test('waiver UI is server-action driven and makes all three confirmations explicit', () => { assert.match(evidenceCard, /currentDisposition\.value\.availableActions/) assert.match(evidenceCard, /currentDisposition\.readOnlyReason/) assert.match(evidenceCard, /批准豁免/) assert.match(evidenceCard, /拒绝豁免/) assert.match(evidenceCard, /风险豁免已拒绝,仍会阻断审批/) assert.match(evidenceCard, /waiverExpiresAt/) assert.match(evidenceCard, /scheduleWaiverExpiryRefresh/) assert.match(evidenceCard, /onBeforeUnmount\(clearWaiverExpiryTimer\)/) assert.doesNotMatch(evidenceCard, /roleCodes|role_codes|isAdmin|is_admin/) assert.match(waiverDialog, /确认提交豁免申请/) assert.match(waiverDialog, /确认批准豁免/) assert.match(waiverDialog, /确认拒绝豁免/) assert.match(waiverDialog, /请填写拒绝理由。/) assert.match(waiverDialog, /补偿条件最多填写 20 项。/) assert.match(waiverDialog, /拒绝后该风险仍会阻断审批/) assert.match(waiverDialog, /:disabled="busy"/) assert.match(waiverRecord, /申请人/) assert.match(waiverRecord, /申请理由/) assert.match(waiverRecord, /豁免范围/) assert.match(waiverRecord, /有效期至/) assert.match(waiverRecord, /补偿条件/) assert.match(waiverRecord, /决定记录/) assert.match(waiverRecord, /waiverDecisionReason/) assert.match(waiverRecord, /豁免审计轨迹/) assert.match(waiverRecord, /event\.requestId/) })