import test from 'node:test' import assert from 'node:assert/strict' import prisma from '../../utils/prisma' import { approveDisposal, createDisposal } from '../disposalController' const now = new Date('2026-07-14T03:00:00.000Z') const rootEquipment = { id: 10, equipment_code: 'JS-PC-0010', equipment_name: '整机', parent_id: null, department_id: 3, storage_location_id: 1, status: '可用', mount_status: 'none', total_quantity: 1, remaining_quantity: 1, unit_price: 1000, updated_at: now } const childEquipment = { id: 11, equipment_code: 'JS-RAM-0011', equipment_name: '内存', parent_id: 10, department_id: 3, storage_location_id: 1, status: '可用', mount_status: 'mounted', total_quantity: 1, remaining_quantity: 1, unit_price: 200, updated_at: now } const createResponse = (role = 'it') => { const state = { statusCode: 200, body: null as any } const res = { locals: { user: { id: 6, role, name: '测试用户', username: 'tester' } }, status(code: number) { state.statusCode = code return this }, json(payload: any) { state.body = payload return this } } return { res, state } } test('主资产仍挂载未勾选配件时申请报废返回 409 且不写状态', async () => { const originalTransaction = prisma.$transaction let stateWriteCount = 0 let disposalCreateCount = 0 ;(prisma.$transaction as any) = async (callback: any) => callback({ $queryRaw: async (query: any) => { if (query.sql.includes('WHERE equipment.id = ?')) return [rootEquipment] if (query.sql.includes('WHERE equipment.parent_id IN')) { return query.values.includes(rootEquipment.id) ? [childEquipment] : [] } return [] }, equipment: { findUnique: async () => rootEquipment, findMany: async (args: any) => { const parentIds = args.where.parent_id.in as number[] return parentIds.includes(10) ? [childEquipment] : [] }, updateMany: async () => { stateWriteCount += 1 return { count: 1 } } }, disposalRecord: { create: async () => { disposalCreateCount += 1 return { id: 1 } } } }) try { const { res, state } = createResponse() await createDisposal( { params: {}, body: { equipment_id: 10, item_ids: [10], disposal_reason: '设备老化', disposal_method: '报废', estimated_value: 0 } } as any, res as any ) assert.equal(state.statusCode, 409) assert.match(state.body.message, /请先卸载未勾选配件/) assert.equal(disposalCreateCount, 0) assert.equal(stateWriteCount, 0) } finally { ;(prisma.$transaction as any) = originalTransaction } }) test('初读资产树后并发挂载新配件时报废申请中止且不写数据', async () => { const originalTransaction = prisma.$transaction let mountedAfterInitialRead = false let stateWriteCount = 0 let disposalCreateCount = 0 const lockQueries: any[] = [] ;(prisma.$transaction as any) = async (callback: any) => callback({ $queryRaw: async (query: any) => { if (query.sql.startsWith('SELECT id FROM equipment')) { lockQueries.push(query) mountedAfterInitialRead = true return [] } if (query.sql.includes('WHERE equipment.id = ?')) return [rootEquipment] if (query.sql.includes('WHERE equipment.parent_id IN')) { return mountedAfterInitialRead && query.values.includes(rootEquipment.id) ? [childEquipment] : [] } return [] }, equipment: { findUnique: async () => rootEquipment, // 模拟 RR 旧快照:锁后再做 ORM 普通读仍会漏掉并发新配件。 findMany: async () => [], updateMany: async () => { stateWriteCount += 1 return { count: 1 } } }, disposalRecord: { create: async () => { disposalCreateCount += 1 return { id: 1 } } } }) try { const { res, state } = createResponse() await createDisposal( { params: {}, body: { equipment_id: rootEquipment.id, item_ids: [rootEquipment.id], disposal_reason: '设备老化', disposal_method: '报废', estimated_value: 0 } } as any, res as any ) assert.equal(state.statusCode, 409) assert.match(state.body.message, /资产树状态或挂载关系已变化/) assert.match(lockQueries[0].sql, /IN \(\?\) ORDER BY id FOR UPDATE/) assert.deepEqual(lockQueries[0].values, [10]) assert.equal(disposalCreateCount, 0) assert.equal(stateWriteCount, 0) } finally { ;(prisma.$transaction as any) = originalTransaction } }) test('并发审批 CAS 失败时返回 409,不追加动作也不改资产', async () => { const originalTransaction = prisma.$transaction let actionCount = 0 let equipmentWriteCount = 0 const disposal = { id: 91, equipment_id: 10, equipment_code: rootEquipment.equipment_code, applied_by: 4, status: 'pending', items: [{ equipment_id: 10, equipment_code: rootEquipment.equipment_code, parent_equipment_id: null, pre_status: '可用', equipment: { ...rootEquipment, status: '待报废' } }] } ;(prisma.$transaction as any) = async (callback: any) => callback({ disposalRecord: { findUnique: async () => disposal, updateMany: async () => ({ count: 0 }) }, disposalAction: { create: async () => { actionCount += 1 } }, equipment: { updateMany: async () => { equipmentWriteCount += 1 return { count: 1 } } } }) try { const { res, state } = createResponse('administrative') await approveDisposal( { params: { id: '91' }, body: { approved: true } } as any, res as any ) assert.equal(state.statusCode, 409) assert.equal(actionCount, 0) assert.equal(equipmentWriteCount, 0) } finally { ;(prisma.$transaction as any) = originalTransaction } }) test('驳回报废时按每个 item.pre_status 分别恢复', async () => { const originalTransaction = prisma.$transaction const originalLogCreate = prisma.operationLog.create const restoredStatuses: string[] = [] const disposal = { id: 92, equipment_id: 10, equipment_code: rootEquipment.equipment_code, applied_by: 4, status: 'pending', items: [ { equipment_id: 10, equipment_code: rootEquipment.equipment_code, parent_equipment_id: null, pre_status: '可用', equipment: { ...rootEquipment, status: '待报废' } }, { equipment_id: 11, equipment_code: childEquipment.equipment_code, parent_equipment_id: 10, pre_status: '已损坏', equipment: { ...childEquipment, status: '待报废' } } ] } ;(prisma.$transaction as any) = async (callback: any) => callback({ disposalRecord: { findUnique: async () => disposal, updateMany: async () => ({ count: 1 }) }, equipment: { updateMany: async (args: any) => { restoredStatuses.push(args.data.status) return { count: 1 } } }, disposalAction: { create: async () => ({ id: 1 }) }, user: { findMany: async () => [] } }) ;(prisma.operationLog.create as any) = async () => ({ id: 1 }) try { const { res, state } = createResponse('administrative') await approveDisposal( { params: { id: '92' }, body: { approved: false, reject_reason: '尚可继续使用' } } as any, res as any ) assert.equal(state.statusCode, 200) assert.deepEqual(restoredStatuses, ['可用', '已损坏']) } finally { ;(prisma.$transaction as any) = originalTransaction ;(prisma.operationLog.create as any) = originalLogCreate } })