/** @jest-environment node */ import { randomUUID } from 'node:crypto' import { AiModelError, assertAiModelAdmin } from '@/lib/ai-model-access' import { verifyAiModel, refreshAiModelVerification, validateAiModelVerificationOutput } from '@/lib/ai-model-verification' import { validateAiModelCombinedVerificationOutput, verificationReferenceImage } from '@/lib/ai-model-verification-output' import { callAiModelProtocol, AiModelProtocolError } from '@/lib/ai-model-protocol' import { resolveAiModelVerification } from '@/lib/ai-model-service' import { prisma } from '@/lib/prisma' import { detailTransactionLock } from '@/lib/detail-generator/repository' import { aiModelAudit } from '@/lib/ai-model-repository' import sharp from 'sharp' import { multiReferenceGrid, multiReferenceImages } from '@/lib/ai-model-multireference' jest.mock('@/lib/ai-model-access', () => { const actual = jest.requireActual('@/lib/ai-model-access'); return { ...actual, assertAiModelAdmin: jest.fn(), assertAiModelAdminForWrite: jest.fn() } }) jest.mock('next-auth', () => ({ getServerSession: jest.fn() })) jest.mock('@/lib/auth', () => ({ authOptions: {} })) jest.mock('@/lib/department-permission', () => ({ isSuperAdmin: jest.fn() })) jest.mock('@/lib/prisma', () => ({ prisma: { $executeRawUnsafe: jest.fn(), $queryRawUnsafe: jest.fn() } })) jest.mock('@/lib/ai-model-service', () => ({ getAiModelAdminState: jest.fn(async () => ({ providers: [] })), resolveAiModelVerification: jest.fn() })) jest.mock('@/lib/ai-model-protocol', () => ({ ...jest.requireActual('@/lib/ai-model-protocol'), callAiModelProtocol: jest.fn() })) 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 callback => callback(mockDb)), aiModelAudit: jest.fn() })) const providerId = '11111111-1111-4111-8111-111111111111', modelKey = '22222222-2222-4222-8222-222222222222' const input = () => ({ stage: 'analysis' as const, revision: 1, idempotencyKey: randomUUID() }) let row: any, executions: any[], duplicate = false, unresolved = 0, active = 0 const validCopy = { sellingPoints: ['蓝色外观'], warnings: [], screens: [{ title: '蓝色产品', body: '型号TEST,蓝色外观', imagePrompt: '无文字的蓝色产品展示场景' }] } beforeEach(() => { jest.clearAllMocks(); duplicate = false; unresolved = 0; active = 0; executions = [] row = { id: randomUUID(), modelKey, stage: 'analysis', contextHash: 'a'.repeat(64), idempotencyKey: randomUUID(), status: 'running', taskId: null, deadlineAt: new Date(Date.now() - 1) } ;(assertAiModelAdmin as jest.Mock).mockResolvedValue('admin') ;(resolveAiModelVerification as jest.Mock).mockResolvedValue({ provider: { baseUrl: 'https://models.example.com/v1' }, model: { id: 'model' }, protocol: { id: 'openai-chat', path: '/v1/chat/completions', headers: {}, supportsImageInput: true }, apiKey: 'synthetic-test-secret', contextHash: row.contextHash }) mockDb.$queryRawUnsafe.mockImplementation(async (sql: string, ...args: any[]) => { if (sql.startsWith('INSERT')) { row = { ...row, id: args[0], stage: args[2], contextHash: args[3], idempotencyKey: args[4], evidence: JSON.parse(args[6]) }; executions.push(row); return [row] } if (sql.includes("evidence->>'executionGroupId'=$3")) return [...executions].sort((left, right) => left.stage.localeCompare(right.stage)) if (sql.includes('idempotency_key=$2')) return duplicate ? [row] : [] if (sql.includes('FOR UPDATE')) return [row] if (sql.includes('active_executions')) return [{ count: active, own: 0 }] if (sql.includes('COUNT(*)')) return [{ count: unresolved }] return [] }) mockDb.$executeRawUnsafe.mockResolvedValue(1) ;(prisma.$executeRawUnsafe as jest.Mock).mockResolvedValue(1) ;(prisma.$queryRawUnsafe as jest.Mock).mockResolvedValue([{ id: row.id }]) ;(callAiModelProtocol as jest.Mock).mockImplementation(async request => { await request.beforeRequest(); const analysis = row.evidence?.challenge || { topLeft: 'red', bottomRight: 'blue' }; return { text: JSON.stringify(row.evidence?.purpose === 'analysis-copy' ? { analysis, copy: validCopy } : analysis), requestId: 'request-1', summary: {} } }) }) test.each([2, 4, 8])('native image acceptance verifies every one of %i persisted references', async count => { const original = await (resolveAiModelVerification as jest.Mock)() ;(resolveAiModelVerification as jest.Mock).mockResolvedValue({ ...original, protocol: { id: 'openai-image-generate', path: '/v1/images/generations', supportsImageInput: true, headers: {}, referenceContract: 'probe-required', maxImages: 8 } }) ;(callAiModelProtocol as jest.Mock).mockImplementation(async request => { await request.beforeRequest() const challenge = row.evidence.multiReferenceChallenge, expected = await multiReferenceImages(challenge) expect(request.images.map((value: any) => value.buffer)).toEqual(expected.map(value => value.buffer)) expect(request.prompt).not.toMatch(/red|blue|green|yellow|红色|蓝色|绿色|黄色/i) const { columns, rows } = multiReferenceGrid(count) const image = await sharp({ create: { width: columns * 512, height: rows * 512, channels: 3, background: '#fff' } }).composite(expected.map((value, i) => ({ input: value.buffer, left: i % columns * 512, top: Math.floor(i / columns) * 512 }))).png().toBuffer() return { image, requestId: 'request-multi', summary: {} } }) await verifyAiModel('admin', providerId, modelKey, { ...input(), stage: 'image', ...(count === 8 ? {} : { referenceImageCount: count }) }) expect(row.evidence.multiReferenceChallenge.references).toHaveLength(count) const passed = mockDb.$executeRawUnsafe.mock.calls.find(([sql]) => sql.includes("status='passed'"))! expect(JSON.parse(passed[3])).toMatchObject({ referenceImageCount: count, allReferenceImagesMatched: true, referenceMatches: Array(count).fill(true) }) expect(callAiModelProtocol).toHaveBeenCalledTimes(1) }) test('changing a persisted test quantity cannot reuse its UUID or dispatch another paid call', async () => { duplicate = true; row.evidence = { purpose: 'image', referenceImageCount: 2 } await expect(verifyAiModel('admin', providerId, modelKey, { ...input(), stage: 'image', referenceImageCount: 4 })).rejects.toMatchObject({ code: 'verification_idempotency_conflict' }) expect(callAiModelProtocol).not.toHaveBeenCalled() }) test('a real image-understanding response persists passed evidence without enabling the model', async () => { await verifyAiModel('admin', providerId, modelKey, input()) expect(callAiModelProtocol).toHaveBeenCalledTimes(1) expect((callAiModelProtocol as jest.Mock).mock.calls[0][0].images[0].buffer).toBeInstanceOf(Buffer) expect(mockDb.$executeRawUnsafe.mock.calls.some(([sql]) => sql.includes("status='passed'"))).toBe(true) expect(mockDb.$executeRawUnsafe.mock.calls.some(([sql]) => /UPDATE ai_model_catalog|ai_model_configuration/.test(sql))).toBe(false) expect(detailTransactionLock).toHaveBeenCalledWith(mockDb, 'detail-generator-execution') }) test('reference-image acceptance requests an unchanged flat redraw without leaking the random color answers', async () => { ;(callAiModelProtocol as jest.Mock).mockImplementation(async request => { await request.beforeRequest() return { image: await verificationReferenceImage(row.evidence.challenge), requestId: 'flat-reference-check', summary: {} } }) await verifyAiModel('admin', providerId, modelKey, { ...input(), stage: 'image' }) const request = (callAiModelProtocol as jest.Mock).mock.calls[0][0] expect(request.prompt).toBe('将参考图重新绘制为干净的平面图,严格保留两个方块各自的原始颜色、形状和相对位置,纯白背景;不要添加文字、标志、阴影、纹理、渐变、光照或其它内容。') expect(request.prompt).not.toMatch(/red|blue|green|yellow|红色|蓝色|绿色|黄色/i) expect(request.prompt).not.toContain(row.evidence.challenge.topLeft) expect(request.prompt).not.toContain(row.evidence.challenge.bottomRight) expect(request.images).toHaveLength(1) expect(request.images[0].buffer).toEqual(await verificationReferenceImage(row.evidence.challenge)) expect(callAiModelProtocol).toHaveBeenCalledTimes(1) expect(mockDb.$executeRawUnsafe.mock.calls.some(([sql]) => sql.includes("status='passed'"))).toBe(true) }) test.each(['multipart', 'json-inline'])('image acceptance preserves the bounded %s request and original versus stored dimensions', async referenceTransport => { const parameters = { n: 1, response_format: 'b64_json', size: '4096x4096', quality: 'high' } const context = await (resolveAiModelVerification as jest.Mock)() ;(resolveAiModelVerification as jest.Mock).mockResolvedValue({ ...context, protocol: { id: 'openai-image-generate', path: '/v1/images/generations', headers: {}, supportsImageInput: true, inputField: 'image', referenceTransport, requestParameters: parameters } }) ;(callAiModelProtocol as jest.Mock).mockImplementation(async request => { await request.beforeRequest() const image = await sharp(await verificationReferenceImage(row.evidence.challenge)).resize(1440, 1440).png().toBuffer() return { image, requestId: 'actual-4k-request', summary: { referenceTransport, requestParameters: parameters, generatedWidth: 4096, generatedHeight: 4096, storedWidth: 1440, storedHeight: 1440, referenceImageAccepted: false, referenceLayoutMatched: false, imageDecoded: false, width: 9999, ruleVersion: -1, adapterVersion: -1, prompt: 'must not persist', secret: 'synthetic-test-secret' } } }) await verifyAiModel('admin', providerId, modelKey, { ...input(), stage: 'image' }) const passed = mockDb.$executeRawUnsafe.mock.calls.find(([sql]) => sql.includes("status='passed'"))! expect(passed[2]).toBe('actual-4k-request') const evidence = JSON.parse(passed[3]) expect(evidence).toMatchObject({ referenceTransport, requestParameters: parameters, generatedWidth: 4096, generatedHeight: 4096, storedWidth: 1440, storedHeight: 1440, width: 1440, height: 1440, referenceImageAccepted: true, referenceLayoutMatched: true, imageDecoded: true }) expect(evidence.ruleVersion).toBeGreaterThan(0); expect(evidence.adapterVersion).toBeGreaterThan(0) expect(evidence).not.toHaveProperty('prompt'); expect(evidence).not.toHaveProperty('secret') expect(passed[3]).not.toContain('synthetic-test-secret') expect((aiModelAudit as jest.Mock).mock.calls.find(([, , action]) => action === 'model.verification-passed')?.[4].evidence).toMatchObject({ generatedWidth: 4096, storedWidth: 1440 }) }) test('an unexpected reference transport cannot replace the locked native route evidence', async () => { const context = await (resolveAiModelVerification as jest.Mock)() const parameters = { n: 1, response_format: 'b64_json' } ;(resolveAiModelVerification as jest.Mock).mockResolvedValue({ ...context, protocol: { id: 'openai-image-generate', path: '/v1/images/generations', headers: {}, supportsImageInput: true, inputField: 'image', referenceTransport: 'json-inline', requestParameters: parameters } }) ;(callAiModelProtocol as jest.Mock).mockImplementation(async request => { await request.beforeRequest() return { image: await verificationReferenceImage(row.evidence.challenge), requestId: 'safe-result', summary: { referenceTransport: 'multipart', requestParameters: parameters } } }) await verifyAiModel('admin', providerId, modelKey, { ...input(), stage: 'image' }) const evidence = JSON.parse(mockDb.$executeRawUnsafe.mock.calls.find(([sql]) => sql.includes("status='passed'"))![3]) expect(evidence.referenceLayoutMatched).toBe(true) expect(evidence).not.toHaveProperty('referenceTransport') expect(evidence).not.toHaveProperty('requestParameters') }) test.each([ { n: 1, response_format: 'b64_json', callback_url: 'https://example.com/private' }, { n: 2, response_format: 'b64_json', size: '4096x4096' }, { n: 1, response_format: 'b64_json', size: '8192x8192' }, { n: 1, response_format: 'b64_json', size: '2048x2048' }, ])('adapter evidence rejects unknown, unbounded or changed request parameters: %j', async requestParameters => { const context = await (resolveAiModelVerification as jest.Mock)() ;(resolveAiModelVerification as jest.Mock).mockResolvedValue({ ...context, protocol: { id: 'openai-image-generate', path: '/v1/images/generations', headers: {}, supportsImageInput: true, inputField: 'image', referenceTransport: 'multipart', requestParameters: { n: 1, response_format: 'b64_json', size: '4096x4096' } } }) ;(callAiModelProtocol as jest.Mock).mockImplementation(async request => { await request.beforeRequest() return { image: await verificationReferenceImage(row.evidence.challenge), requestId: 'safe-result', summary: { referenceTransport: 'multipart', requestParameters, generatedWidth: Infinity, generatedHeight: 1, storedWidth: 512, storedHeight: -1, referenceLayoutMatched: false, } } }) await verifyAiModel('admin', providerId, modelKey, { ...input(), stage: 'image' }) const evidence = JSON.parse(mockDb.$executeRawUnsafe.mock.calls.find(([sql]) => sql.includes("status='passed'"))![3]) expect(evidence.referenceLayoutMatched).toBe(true) expect(evidence.referenceTransport).toBe('multipart') expect(evidence).not.toHaveProperty('requestParameters') expect(evidence).not.toHaveProperty('generatedWidth'); expect(evidence).not.toHaveProperty('generatedHeight') expect(evidence).not.toHaveProperty('storedWidth'); expect(evidence).not.toHaveProperty('storedHeight') }) test('repeated idempotency keys and unresolved requests cannot submit another model call', async () => { duplicate = true await verifyAiModel('admin', providerId, modelKey, input()) expect(callAiModelProtocol).not.toHaveBeenCalled() duplicate = false; unresolved = 1 await expect(verifyAiModel('admin', providerId, modelKey, input())).rejects.toMatchObject({ code: 'verification_unresolved' }) expect(callAiModelProtocol).not.toHaveBeenCalled() }) test('verification respects active generation capacity', async () => { active = 2 await expect(verifyAiModel('admin', providerId, modelKey, input())).rejects.toMatchObject({ code: 'verification_busy' }) expect(callAiModelProtocol).not.toHaveBeenCalled() const capacity = mockDb.$queryRawUnsafe.mock.calls.find(([sql]) => sql.includes('active_executions'))! expect(capacity[0]).toContain("status='queued' AND checkpoint->>'dispatchPending'='true'") expect(capacity[0]).toContain("v.status='unknown'") expect(capacity[0]).toContain("model.verification-execution-ended") }) test('revocation before admission performs no call or insert', async () => { ;(assertAiModelAdmin as jest.Mock).mockRejectedValue(new AiModelError('forbidden', 403)) await expect(verifyAiModel('admin', providerId, modelKey, input())).rejects.toMatchObject({ status: 403 }) expect(callAiModelProtocol).not.toHaveBeenCalled() expect(mockDb.$queryRawUnsafe).not.toHaveBeenCalled() }) test.each([false, true])('protocol outcomeUnknown=%s is persisted accurately, never retried', async unknown => { ;(callAiModelProtocol as jest.Mock).mockImplementation(async request => { await request.beforeRequest(); throw new AiModelProtocolError('synthetic-test-secret must not escape', unknown, 'req-2', unknown ? 'task-2' : null) }) await verifyAiModel('admin', providerId, modelKey, input()) expect(callAiModelProtocol).toHaveBeenCalledTimes(1) const terminal = mockDb.$executeRawUnsafe.mock.calls.find(([sql]) => sql.includes('status=$2'))! expect(terminal[2]).toBe(unknown ? 'unknown' : 'failed') expect(JSON.stringify(terminal)).not.toContain('synthetic-test-secret') }) test.each([false, true])('safe protocol diagnostics are preserved for outcomeUnknown=%s', async unknown => { const message = unknown ? '上游返回异常响应(502),请核查请求结果;勿重复提交' : '上游拒绝模型请求(HTTP 403),测试未通过' ;(callAiModelProtocol as jest.Mock).mockImplementation(async request => { await request.beforeRequest(); throw new AiModelProtocolError(message, unknown, 'safe-request-id') }) await verifyAiModel('admin', providerId, modelKey, input()) const terminal = mockDb.$executeRawUnsafe.mock.calls.find(([sql]) => sql.includes('status=$2'))! expect(terminal).toContain(message) expect(terminal[2]).toBe(unknown ? 'unknown' : 'failed') expect(callAiModelProtocol).toHaveBeenCalledTimes(1) }) test('a persisted task can only be queried through resumeTaskId and its original idempotency key', async () => { row.status = 'unknown'; row.taskId = 'task-original' await refreshAiModelVerification('admin', providerId, row.id) expect(callAiModelProtocol).toHaveBeenCalledWith(expect.objectContaining({ resumeTaskId: 'task-original', idempotencyKey: row.idempotencyKey })) expect(mockDb.$queryRawUnsafe.mock.calls.some(([sql]) => sql.startsWith('INSERT'))).toBe(false) expect(mockDb.$queryRawUnsafe.mock.calls.some(([sql]) => sql.includes('active_executions'))).toBe(false) }) test('unknown without a checkpoint cannot be resumed or bypassed with manual confirmation', async () => { row.status = 'unknown'; row.taskId = null await expect(refreshAiModelVerification('admin', providerId, row.id)).rejects.toMatchObject({ code: 'verification_not_resumable' }) await expect(verifyAiModel('admin', providerId, modelKey, { ...input(), compatibility: 'confirmed' })).rejects.toThrow('参数无效') expect(callAiModelProtocol).not.toHaveBeenCalled() }) test('a recovered customer request reuses the persisted multi-image challenge and never creates another verification', async () => { const requestId = randomUUID(), original = await (resolveAiModelVerification as jest.Mock)() row.status = 'unknown'; row.stage = 'image'; row.evidence = { challenge: { topLeft: 'red', bottomRight: 'blue' }, referenceImageCount: 2, multiReferenceChallenge: { version: 1, references: [{ topLeft: 'red', bottomRight: 'blue' }, { topLeft: 'yellow', bottomRight: 'green' }] } } ;(resolveAiModelVerification as jest.Mock).mockResolvedValue({ ...original, protocol: { id: 'openai-image-generate', path: '/v1/images/generations', headers: {}, supportsImageInput: true, maxImages: 8 } }) ;(callAiModelProtocol as jest.Mock).mockImplementation(async request => { await request.beforeRequest() const refs = await multiReferenceImages(row.evidence.multiReferenceChallenge) return { image: await sharp({ create: { width: 1024, height: 512, channels: 3, background: '#fff' } }).composite(refs.map((value, i) => ({ input: value.buffer, left: i * 512, top: 0 }))).png().toBuffer(), requestId, summary: {} } }) await refreshAiModelVerification('admin', providerId, row.id, { requestId }) expect(callAiModelProtocol).toHaveBeenCalledTimes(1) expect(callAiModelProtocol).toHaveBeenCalledWith(expect.objectContaining({ resumeRequestId: requestId, idempotencyKey: row.idempotencyKey })) expect((callAiModelProtocol as jest.Mock).mock.calls[0][0]).not.toHaveProperty('resumeTaskId') expect(mockDb.$queryRawUnsafe.mock.calls.some(([sql]) => sql.startsWith('INSERT'))).toBe(false) const passed = mockDb.$executeRawUnsafe.mock.calls.find(([sql]) => sql.includes("status='passed'"))! expect(JSON.parse(passed[3])).toMatchObject({ retainedResponse: true, referenceImageCount: 2, referenceMatches: [true, true] }) expect(aiModelAudit).toHaveBeenCalledWith(mockDb, 'admin', 'model.verification-query', providerId, expect.objectContaining({ requestId, retainedResponse: true })) }) test('unavailable retained result keeps the original request id unknown and cannot be replaced', async () => { const requestId = randomUUID(), original = await (resolveAiModelVerification as jest.Mock)() row.status = 'unknown'; row.stage = 'image'; row.requestId = requestId; row.evidence = { challenge: { topLeft: 'red', bottomRight: 'blue' } } ;(resolveAiModelVerification as jest.Mock).mockResolvedValue({ ...original, protocol: { id: 'openai-image-generate', path: '/v1/images/generations', headers: {}, supportsImageInput: true } }) ;(callAiModelProtocol as jest.Mock).mockRejectedValue(new AiModelProtocolError('暂未取回', true, requestId)) await refreshAiModelVerification('admin', providerId, row.id) expect(mockDb.$executeRawUnsafe.mock.calls.find(([sql]) => sql.includes('status=$2'))?.slice(2, 4)).toEqual(['unknown', requestId]) await expect(refreshAiModelVerification('admin', providerId, row.id, { requestId: randomUUID() })).rejects.toMatchObject({ code: 'verification_request_conflict' }) expect(callAiModelProtocol).toHaveBeenCalledTimes(1) }) test('changed context before dispatch records known failure without pretending it passed', async () => { ;(resolveAiModelVerification as jest.Mock).mockResolvedValueOnce({ ...(await (resolveAiModelVerification as jest.Mock)()), contextHash: row.contextHash }) ;(callAiModelProtocol as jest.Mock).mockImplementation(async request => { (resolveAiModelVerification as jest.Mock).mockResolvedValue({ contextHash: 'b'.repeat(64) }); await request.beforeRequest(); throw new Error('must not send') }) await verifyAiModel('admin', providerId, modelKey, input()) const terminal = mockDb.$executeRawUnsafe.mock.calls.find(([sql]) => sql.includes('status=$2'))! expect(terminal[2]).toBe('failed') }) test('configuration changes immediately after admission persist known failure and make no model call', async () => { const original = await (resolveAiModelVerification as jest.Mock)() ;(resolveAiModelVerification as jest.Mock).mockResolvedValueOnce(original).mockRejectedValue(new AiModelError('连接已改变', 409, 'verification_context_changed')) await verifyAiModel('admin', providerId, modelKey, input()) expect(callAiModelProtocol).not.toHaveBeenCalled() expect(mockDb.$executeRawUnsafe.mock.calls.find(([sql]) => sql.includes('status=$2'))?.[2]).toBe('failed') }) test('revocation after an upstream task was accepted preserves unknown and the query checkpoint', async () => { ;(callAiModelProtocol as jest.Mock).mockImplementation(async request => { await request.beforeRequest() await request.onTaskSubmitted('task-accepted') throw new AiModelError('权限已撤销', 403) }) await verifyAiModel('admin', providerId, modelKey, input()) const terminal = mockDb.$executeRawUnsafe.mock.calls.find(([sql]) => sql.includes('status=$2'))! expect(terminal[2]).toBe('unknown') expect(terminal[4]).toBe('task-accepted') expect(callAiModelProtocol).toHaveBeenCalledTimes(1) }) test('a resumed task remains unknown when result lookup fails before the request guard', async () => { row.status = 'unknown'; row.taskId = 'task-original' ;(callAiModelProtocol as jest.Mock).mockRejectedValue(new AiModelProtocolError('DNS temporarily unavailable', true, null, 'task-original')) await refreshAiModelVerification('admin', providerId, row.id) const terminal = mockDb.$executeRawUnsafe.mock.calls.find(([sql]) => sql.includes('status=$2'))! expect(terminal[2]).toBe('unknown') expect(terminal[4]).toBe('task-original') expect(callAiModelProtocol).toHaveBeenCalledWith(expect.objectContaining({ resumeTaskId: 'task-original' })) }) test('output acceptance checks image interpretation and actual outline structure', async () => { await expect(validateAiModelVerificationOutput('analysis', { text: '{"topLeft":"blue","bottomRight":"red"}' })).rejects.toMatchObject({ code: 'verification_output_invalid' }) await expect(validateAiModelVerificationOutput('copy', { text: '{"text":"ok"}' })).rejects.toMatchObject({ code: 'verification_output_invalid' }) await expect(validateAiModelVerificationOutput('copy', { text: '{"sellingPoints":["蓝色"],"warnings":[],"screens":[{"title":"蓝色产品","body":"型号TEST","imagePrompt":"蓝色产品,无文字"}]}' })).resolves.toMatchObject({ outlineSchemaVerified: true }) }) test('a closed absent submission cannot arrive late and create a paid call', async () => { const original = mockDb.$queryRawUnsafe.getMockImplementation()! mockDb.$queryRawUnsafe.mockImplementation(async (sql: string, ...args: any[]) => sql.includes('verification-intake-cancelled') ? [{ id: 'closed-intake' }] : original(sql, ...args)) await expect(verifyAiModel('admin', providerId, modelKey, input())).rejects.toMatchObject({ code: 'verification_submission_closed' }) expect(callAiModelProtocol).not.toHaveBeenCalled() expect(mockDb.$queryRawUnsafe.mock.calls.some(([sql]) => sql.startsWith('INSERT'))).toBe(false) }) test('a closed verification stops dispatch and a late response cannot append a false success audit', async () => { ;(prisma.$queryRawUnsafe as jest.Mock).mockResolvedValue([]) await verifyAiModel('admin', providerId, modelKey, input()) expect(mockDb.$executeRawUnsafe.mock.calls.some(([sql]) => sql.includes("status='passed'"))).toBe(false) ;(prisma.$queryRawUnsafe as jest.Mock).mockResolvedValue([{ id: row.id }]) mockDb.$executeRawUnsafe.mockImplementation(async sql => sql.includes("status='passed'") ? 0 : 1) await verifyAiModel('admin', providerId, modelKey, input()) expect((aiModelAudit as jest.Mock).mock.calls.some(([, , action]) => action === 'model.verification-passed')).toBe(false) }) test('a terminal decoded image that fails reference validation is failed, not an endless unknown task', async () => { ;(callAiModelProtocol as jest.Mock).mockImplementation(async request => { await request.beforeRequest(); await request.onTaskSubmitted('task-ended') const challenge = row.evidence.challenge const image = await verificationReferenceImage({ topLeft: challenge.bottomRight, bottomRight: challenge.topLeft }) return { image, requestId: 'known-result', summary: {} } }) row.stage = 'image' await verifyAiModel('admin', providerId, modelKey, { ...input(), stage: 'image' }) const failed = mockDb.$executeRawUnsafe.mock.calls.find(([sql]) => sql.includes('status=$2'))! expect(failed[2]).toBe('failed') expect(failed[3]).toBe('known-result') expect(failed[4]).toBe('task-ended') expect(failed[5]).toContain('颜色与相对位置') expect(callAiModelProtocol).toHaveBeenCalledTimes(1) }) test('one multimodal request creates independent passed records and consumes one execution identity', async () => { const submission = { ...input(), stage: 'analysis-copy' } await verifyAiModel('admin', providerId, modelKey, submission) expect(callAiModelProtocol).toHaveBeenCalledTimes(1) expect(callAiModelProtocol).toHaveBeenCalledWith(expect.objectContaining({ idempotencyKey: submission.idempotencyKey, maxOutputTokens: 1800 })) expect(executions.map(value => value.stage)).toEqual(['analysis', 'copy']) expect(new Set(executions.map(value => value.evidence.executionGroupId)).size).toBe(1) expect(executions.every(value => value.evidence.purpose === 'analysis-copy' && value.idempotencyKey === submission.idempotencyKey)).toBe(true) expect(mockDb.$executeRawUnsafe.mock.calls.filter(([sql]) => sql.includes("status='passed'")).map(([, id]) => id)).toEqual(executions.map(value => value.id)) expect((aiModelAudit as jest.Mock).mock.calls.filter(([, , action]) => action === 'model.verification-start')).toHaveLength(1) expect(mockDb.$queryRawUnsafe.mock.calls.find(([sql]) => sql.includes('active_executions'))?.[0]).toContain("SELECT DISTINCT COALESCE(v.evidence->>'executionGroupId',v.id)") duplicate = true await verifyAiModel('admin', providerId, modelKey, submission) expect(callAiModelProtocol).toHaveBeenCalledTimes(1) }) test.each(['analysis', 'copy'] as const)('a failed %s section leaves the other purpose independently passed', async failedStage => { ;(callAiModelProtocol as jest.Mock).mockImplementation(async request => { await request.beforeRequest() const content = { analysis: row.evidence.challenge, copy: validCopy, [failedStage]: failedStage === 'analysis' ? { topLeft: 'pink', bottomRight: 'pink' } : { screens: [] } } return { text: JSON.stringify(content), requestId: 'partial-result', summary: {} } }) await verifyAiModel('admin', providerId, modelKey, { ...input(), stage: 'analysis-copy' }) const failed = executions.find(value => value.stage === failedStage), passed = executions.find(value => value.stage !== failedStage) expect(mockDb.$executeRawUnsafe.mock.calls.some(([sql, id]) => sql.includes("status='failed'") && id === failed.id)).toBe(true) expect(mockDb.$executeRawUnsafe.mock.calls.some(([sql, id]) => sql.includes("status='passed'") && id === passed.id)).toBe(true) expect(callAiModelProtocol).toHaveBeenCalledTimes(1) }) test('unsupported image input or a different copy route cannot dispatch a joint test', async () => { const original = await (resolveAiModelVerification as jest.Mock)() ;(resolveAiModelVerification as jest.Mock).mockResolvedValue({ ...original, protocol: { ...original.protocol, supportsImageInput: false } }) await expect(verifyAiModel('admin', providerId, modelKey, { ...input(), stage: 'analysis-copy' })).rejects.toMatchObject({ code: 'verification_combined_unsupported' }) ;(resolveAiModelVerification as jest.Mock).mockImplementation(async (_provider, _model, stage) => ({ ...original, protocol: { ...original.protocol, path: stage === 'copy' ? '/v1/messages' : original.protocol.path } })) await expect(verifyAiModel('admin', providerId, modelKey, { ...input(), stage: 'analysis-copy' })).rejects.toMatchObject({ code: 'verification_combined_unsupported' }) expect(callAiModelProtocol).not.toHaveBeenCalled() expect(executions).toHaveLength(0) }) test('unknown joint outcomes pause both purposes and query only the same original task', async () => { ;(callAiModelProtocol as jest.Mock).mockImplementation(async request => { await request.beforeRequest(); await request.onTaskSubmitted('task-joint') throw new AiModelProtocolError('interrupted', true, 'request-joint', 'task-joint') }) const submission = { ...input(), stage: 'analysis-copy' } await verifyAiModel('admin', providerId, modelKey, submission) expect(mockDb.$executeRawUnsafe.mock.calls.filter(([sql, , status]) => sql.includes('status=$2') && status === 'unknown')).toHaveLength(2) executions.forEach(value => { value.status = 'unknown'; value.taskId = 'task-joint' }) row = executions[1] ;(callAiModelProtocol as jest.Mock).mockImplementation(async request => { await request.beforeRequest(); return { text: JSON.stringify({ analysis: row.evidence.challenge, copy: validCopy }), requestId: 'request-joint', summary: {} } }) await refreshAiModelVerification('admin', providerId, row.id) expect(callAiModelProtocol).toHaveBeenLastCalledWith(expect.objectContaining({ resumeTaskId: 'task-joint', idempotencyKey: submission.idempotencyKey })) expect(executions).toHaveLength(2) expect(mockDb.$queryRawUnsafe.mock.calls.filter(([sql]) => sql.includes('active_executions'))).toHaveLength(1) }) test('malformed joint JSON fails both checks and null sections cannot pass', async () => { await expect(validateAiModelCombinedVerificationOutput({ text: 'not JSON' }, { topLeft: 'red', bottomRight: 'blue' })).resolves.toMatchObject({ analysis: { status: 'failed' }, copy: { status: 'failed' } }) await expect(validateAiModelCombinedVerificationOutput({ text: JSON.stringify({ analysis: null, copy: validCopy }) }, { topLeft: 'red', bottomRight: 'blue' })).resolves.toMatchObject({ analysis: { status: 'failed' }, copy: { status: 'passed' } }) }) test('a joint text recovery reads the original retained response once and records both purposes separately', async () => { await verifyAiModel('admin', providerId, modelKey, { ...input(), stage: 'analysis-copy' }) const requestId = randomUUID() executions.forEach(value => { value.status = 'unknown'; value.requestId = requestId; value.taskId = null }) row = executions[1] ;(callAiModelProtocol as jest.Mock).mockClear().mockImplementation(async request => { await request.beforeRequest() expect(request.resumeRequestId).toBe(requestId) expect(request.resumeTaskId).toBeUndefined() return { text: JSON.stringify({ analysis: row.evidence.challenge, copy: { screens: [] } }), requestId, summary: { retainedResponse: true } } }) mockDb.$executeRawUnsafe.mockClear() await refreshAiModelVerification('admin', providerId, row.id) expect(callAiModelProtocol).toHaveBeenCalledTimes(1) const saved = mockDb.$executeRawUnsafe.mock.calls.filter(([sql]) => sql.includes('finished_at=NOW()') && sql.includes('evidence=evidence')) expect(saved).toHaveLength(2) expect(saved.map(call => call[0])).toEqual([expect.stringContaining("status='passed'"), expect.stringContaining("status='failed'")]) expect(saved.every(call => JSON.parse(call[3]).retainedResponse === true)).toBe(true) expect(executions).toHaveLength(2) }) test('a terminal truncated retained text response closes the checks as failed without another submission', async () => { await verifyAiModel('admin', providerId, modelKey, { ...input(), stage: 'analysis-copy' }) const requestId = randomUUID() executions.forEach(value => { value.status = 'unknown'; value.requestId = requestId; value.taskId = null }) row = executions[0] ;(callAiModelProtocol as jest.Mock).mockClear().mockImplementation(async request => { await request.beforeRequest(); throw new AiModelProtocolError('模型输出不完整,本次调用已结束', false, row.requestId) }) mockDb.$executeRawUnsafe.mockClear() await refreshAiModelVerification('admin', providerId, row.id) expect(callAiModelProtocol).toHaveBeenCalledTimes(1) const failed = mockDb.$executeRawUnsafe.mock.calls.filter(([sql]) => sql.includes('status=$2')) expect(failed).toHaveLength(2) expect(failed.every(call => call[2] === 'failed')).toBe(true) })