gongxiangp2/lib/__tests__/ai-model-execution-release.test.ts

26 lines
4.0 KiB
TypeScript

/** @jest-environment node */
import { randomUUID } from 'crypto'
import { releaseAiModelVerificationExecution } from '@/lib/ai-model-execution-release'
import { aiModelExecutionOccupiesSlot } from '@/lib/ai-model-execution-slot'
import { aiModelAudit } from '@/lib/ai-model-repository'
import { assertAiModelAdmin, assertAiModelAdminForWrite } from '@/lib/ai-model-access'
jest.mock('next-auth',()=>({getServerSession:jest.fn()}))
jest.mock('@/lib/auth',()=>({authOptions:{}}))
jest.mock('@/lib/prisma',()=>({prisma:{}}))
jest.mock('@/lib/department-permission',()=>({isSuperAdmin:jest.fn()}))
jest.mock('@/lib/ai-model-access',()=>({...jest.requireActual('@/lib/ai-model-access'),assertAiModelAdmin:jest.fn(),assertAiModelAdminForWrite:jest.fn()}))
jest.mock('@/lib/ai-model-service',()=>({assertAiModelRecoveryTextSafe:jest.fn(),getAiModelAdminState:jest.fn(async()=>({providers:[]}))}))
jest.mock('@/lib/detail-generator/repository',()=>({detailTransactionLock:jest.fn()}))
const mockDb={$queryRawUnsafe:jest.fn(),$executeRawUnsafe:jest.fn()}
jest.mock('@/lib/ai-model-repository',()=>({aiModelTransaction:jest.fn(async cb=>cb(mockDb)),aiModelAudit:jest.fn()}))
let row:any,duplicate=false
const provider=randomUUID(),requestId=randomUUID(),proof={requestId,executionEnded:true,billingFinalized:true,reference:requestId,note:'服务商已核查:只派发一次,执行已结束,扣费已结算为零,原结果无法恢复。',confirmed:true}
beforeEach(()=>{jest.clearAllMocks();duplicate=false;row={id:randomUUID(),modelKey:randomUUID(),stage:'analysis',status:'unknown',requestId,taskId:null,evidence:{},idempotencyKey:randomUUID()};mockDb.$queryRawUnsafe.mockImplementation(async sql=>sql.includes('FROM ai_model_audit')?duplicate?[{id:'audit'}]:[]:[row])})
test('ending execution writes only audit; original result, IDs, keys and retry fence are preserved',async()=>{const before=JSON.stringify(row);await releaseAiModelVerificationExecution('admin',provider,row.id,proof);expect(JSON.stringify(row)).toBe(before);expect(mockDb.$executeRawUnsafe).not.toHaveBeenCalled();expect(aiModelAudit).toHaveBeenCalledWith(mockDb,'admin','model.verification-execution-ended',provider,expect.objectContaining({resultStillUnknown:true,retryAllowed:false,originalRequestId:requestId,verificationIds:[row.id]}))})
test.each([{requestId:randomUUID()},{executionEnded:false},{billingFinalized:false},{confirmed:false},{status:'passed'},{note:'已结束'},{model:'override'}])('insufficient or altered proof cannot release execution: %j',async change=>{await expect(releaseAiModelVerificationExecution('admin',provider,row.id,{...proof,...change})).rejects.toThrow();expect(aiModelAudit).not.toHaveBeenCalled()})
test.each(['running','passed','failed'])('a %s result cannot be released as unknown',async status=>{row.status=status;await expect(releaseAiModelVerificationExecution('admin',provider,row.id,proof)).rejects.toMatchObject({code:'execution_proof_conflict'});expect(aiModelAudit).not.toHaveBeenCalled()})
test('asynchronous tasks cannot skip polling',async()=>{row.taskId='task-1';await expect(releaseAiModelVerificationExecution('admin',provider,row.id,proof)).rejects.toMatchObject({code:'execution_proof_conflict'})})
test('duplicate evidence cannot overwrite the first audit',async()=>{duplicate=true;await releaseAiModelVerificationExecution('admin',provider,row.id,proof);expect(aiModelAudit).not.toHaveBeenCalled()})
test('fresh database revocation blocks audit writes',async()=>{(assertAiModelAdminForWrite as jest.Mock).mockRejectedValueOnce(new Error('revoked'));await expect(releaseAiModelVerificationExecution('admin',provider,row.id,proof)).rejects.toThrow('revoked');expect(aiModelAudit).not.toHaveBeenCalled()})
test('slot predicate requires the exact original execution, request and verification membership',()=>{const sql=aiModelExecutionOccupiesSlot('v');expect(sql).toContain("v.status='running'");expect(sql).toContain("ended.details->>'originalRequestId'=v.request_id");expect(sql).toContain("ended.details->'verificationIds' ? v.id");expect(()=>aiModelExecutionOccupiesSlot('x;DROP')).toThrow()})