import assert from 'node:assert/strict' import test from 'node:test' import { addSignApprovalTask, approveApprovalTask, batchApproveApprovalTasks, countersignApprovalTask, delegateApprovalTask, escalateApprovalTask, fetchApprovalTaskCandidates, fetchApprovalTaskDetail, fetchApprovalTasks, returnApprovalTask, revokeApprovalTaskDelegation, transferApprovalTask } from '../src/services/approvalTasks.js' function jsonResponse(payload) { return { ok: true, status: 200, async json() { return payload } } } function pendingTask(overrides = {}) { return { id: 'task-001', claim_id: 'claim-001', status: 'pending', version: 3, can_act: true, available_actions: ['approve', 'delegate'], batch_eligible: true, claim_status_snapshot: 'submitted', claim_stage_snapshot: '直属领导审批', priority_reasons_json: [{ code: 'sla_near_due', label: '即将超时', weight: 16 }], ...overrides } } test('审批任务列表发送服务端分页筛选参数并规范化任务', async () => { let capturedUrl = '' global.fetch = async (url) => { capturedUrl = String(url) return jsonResponse({ items: [{ task: pendingTask(), claim: { id: 'claim-001', claim_no: 'BX-001' } }], total: 41, page: 2, page_size: 25, total_pages: 2, generated_at: '2026-07-16T10:00:00Z' }) } const result = await fetchApprovalTasks({ page: 2, pageSize: 25, status: 'pending', riskLevel: 'high', slaState: 'overdue', nodeKey: 'direct_manager', keyword: '张 三', batchEligible: true, sort: 'priority_desc' }) const url = new URL(capturedUrl, 'http://local.test') assert.equal(url.pathname, '/api/v1/approval-tasks') assert.equal(url.searchParams.get('page'), '2') assert.equal(url.searchParams.get('page_size'), '25') assert.equal(url.searchParams.get('status'), 'pending') assert.equal(url.searchParams.get('risk_level'), 'high') assert.equal(url.searchParams.get('sla_state'), 'overdue') assert.equal(url.searchParams.get('node_key'), 'direct_manager') assert.equal(url.searchParams.get('keyword'), '张 三') assert.equal(url.searchParams.get('batch_eligible'), 'true') assert.equal(url.searchParams.get('sort'), 'priority_desc') assert.equal(result.total, 41) assert.equal(result.pageSize, 25) assert.equal(result.items[0].task.claimId, 'claim-001') assert.equal(result.items[0].task.priorityReasons[0].code, 'sla_near_due') assert.equal(result.items[0].claim.claim_no, 'BX-001') }) test('审批任务详情和候选人使用任务作用域接口', async () => { const calls = [] global.fetch = async (url) => { calls.push(String(url)) if (String(url).includes('/candidates?')) { return jsonResponse({ items: [{ employee_id: 'employee-002', employee_no: 'E002', name: '李审批', email: 'li@example.com', qualified: false, reason: '不属于当前部门' }], total: 1 }) } return jsonResponse({ task: pendingTask(), claim: { id: 'claim-001' } }) } const detail = await fetchApprovalTaskDetail('task/001') const candidates = await fetchApprovalTaskCandidates('task/001', { action: 'delegate', keyword: '李 审批', limit: 30 }) assert.equal(calls[0], '/api/v1/approval-tasks/task%2F001') const candidateUrl = new URL(calls[1], 'http://local.test') assert.equal(candidateUrl.pathname, '/api/v1/approval-tasks/task%2F001/candidates') assert.equal(candidateUrl.searchParams.get('action'), 'delegate') assert.equal(candidateUrl.searchParams.get('keyword'), '李 审批') assert.equal(candidateUrl.searchParams.get('limit'), '30') assert.equal(detail.task.id, 'task-001') assert.equal(candidates.items[0].qualified, false) assert.equal(candidates.items[0].reason, '不属于当前部门') }) test('所有审批动作携带请求幂等键和任务版本', async () => { const calls = [] global.fetch = async (url, options) => { calls.push({ url: String(url), body: JSON.parse(options.body) }) return jsonResponse({ task: pendingTask({ version: 4 }), related_tasks: [], replayed: false }) } const base = { requestId: 'request-action-001', expectedTaskVersion: 3, reason: '业务审批处理' } await delegateApprovalTask('task-001', { ...base, targetEmployeeId: 'employee-002', expiresAt: '2026-07-18T10:00:00Z' }) await revokeApprovalTaskDelegation('task-001', base) await transferApprovalTask('task-001', { ...base, targetEmployeeId: 'employee-003' }) await addSignApprovalTask('task-001', { ...base, participantEmployeeIds: ['employee-004', 'employee-005', 'employee-004'] }) await countersignApprovalTask('task-001', { ...base, participantEmployeeIds: ['employee-006', 'employee-007'] }) await escalateApprovalTask('task-001', base) await approveApprovalTask('task-001', { ...base, opinion: '同意通过' }) await returnApprovalTask('task-001', { ...base, reasonCodes: ['missing_invoice', 'missing_invoice', 'amount_mismatch'] }) assert.deepEqual(calls.map((call) => call.url), [ '/api/v1/approval-tasks/task-001/delegate', '/api/v1/approval-tasks/task-001/delegation/revoke', '/api/v1/approval-tasks/task-001/transfer', '/api/v1/approval-tasks/task-001/add-sign', '/api/v1/approval-tasks/task-001/countersign', '/api/v1/approval-tasks/task-001/escalate', '/api/v1/approval-tasks/task-001/approve', '/api/v1/approval-tasks/task-001/return' ]) calls.forEach((call) => { assert.equal(call.body.request_id, 'request-action-001') assert.equal(call.body.expected_task_version, 3) assert.equal(call.body.reason, '业务审批处理') }) assert.equal(calls[0].body.target_employee_id, 'employee-002') assert.equal(calls[0].body.expires_at, '2026-07-18T10:00:00Z') assert.deepEqual(calls[3].body.participant_employee_ids, ['employee-004', 'employee-005']) assert.equal(calls[6].body.opinion, '同意通过') assert.deepEqual(calls[7].body.reason_codes, ['missing_invoice', 'amount_mismatch']) }) test('批量审批发送逐项快照并规范化部分成功结果', async () => { let capturedUrl = '' let capturedBody = null global.fetch = async (url, options) => { capturedUrl = String(url) capturedBody = JSON.parse(options.body) return jsonResponse({ batch_request_id: 'batch-request-001', status: 'partial', succeeded_count: 1, blocked_count: 1, conflict_count: 1, items: [ { task_id: 'task-001', claim_id: 'claim-001', status: 'succeeded', code: 'OK', message: '已通过' }, { task_id: 'task-002', claim_id: 'claim-002', status: 'blocked', code: 'OPEN_RISK', message: '存在开放高风险' }, { task_id: 'task-003', claim_id: 'claim-003', status: 'conflict', code: 'STALE_TASK', message: '任务已更新' } ] }) } const result = await batchApproveApprovalTasks({ batchRequestId: 'batch-request-001', items: [ pendingTask({ id: 'task-001' }), pendingTask({ id: 'task-002', version: 5 }), { taskId: 'task-003', expectedTaskVersion: 2, expectedStatus: 'submitted', expectedApprovalStage: '财务审批', opinion: '票据已核对' } ] }) assert.equal(capturedUrl, '/api/v1/approval-tasks/batch-approve') assert.equal(capturedBody.batch_request_id, 'batch-request-001') assert.deepEqual(capturedBody.items[0], { task_id: 'task-001', expected_task_version: 3, expected_status: 'submitted', expected_approval_stage: '直属领导审批' }) assert.equal(capturedBody.items[1].expected_task_version, 5) assert.equal(capturedBody.items[2].opinion, '票据已核对') assert.equal(result.status, 'partial') assert.equal(result.succeededCount, 1) assert.equal(result.blockedCount, 1) assert.equal(result.conflictCount, 1) assert.equal(result.items[1].code, 'OPEN_RISK') })