/** @jest-environment node */ import { aiModelCapabilityFingerprint, getAiModelCapabilities, mergeAiModelCapabilityMetadata, parseAiModelCapabilityMetadata, parseAiModelPublicCapabilityCatalog } from '@/lib/ai-model-capabilities' 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: { user: { findUnique: jest.fn() } } })) const base = 'https://api.vip98k.com/v1' const generic = 'https://example.com/v1' const metadata = (input: string[], output: string[], endpoints: string[]) => parseAiModelCapabilityMetadata({ input_modalities: input, output_modalities: output, endpoints })! const invocation = (path = '/v1/chat/completions', headers = {}) => ({ method: 'POST', path, content_type: path.includes('/images/') ? 'multipart/form-data' : 'application/json', headers }) const model = (overrides = {}) => ({ id: 'catalog-model', type: 'model', modality: 'text', available: true, status: 'available', default_service_scope_id: 'scope-default', selection_required: false, invocation: invocation(), ...overrides }) const publicMetadata = (entry: ReturnType, section = 'chat') => parseAiModelPublicCapabilityCatalog({ code: 0, data: { schema_version: '2.2', sections: [{ id: section, models: [entry] }] } }).get(entry.id)! test('a plain conversation model cannot analyze product images or generate images', () => { const capability = getAiModelCapabilities(generic, 'nano-banana-image-sounding-name', metadata(['text'], ['text'], ['/chat/completions'])) expect(capability).toMatchObject({ kind: 'text', allowedStages: ['copy'], source: 'upstream' }) }) test('image understanding and image output require separate interfaces', () => { expect(getAiModelCapabilities(generic, 'vision', metadata(['text', 'image'], ['text'], ['/chat/completions'])).allowedStages).toEqual(['analysis', 'copy']) expect(getAiModelCapabilities(generic, 'generator', metadata(['text'], ['image'], ['/images/generations']))).toMatchObject({ kind: 'image', allowedStages: [] }) expect(getAiModelCapabilities(generic, 'editor', metadata(['text', 'image'], ['image'], ['/images/edits']))).toMatchObject({ kind: 'image', allowedStages: ['image'] }) }) test('image models explain pending adaptation without treating missing metadata as proof of no reference support', () => { expect(getAiModelCapabilities(generic, 'text-image', metadata(['text'], ['image'], ['/images/generations']))).toMatchObject({ allowedStages: [], reason: '参考图待适配:当前目录尚未确认可用的参考图传入方式' }) expect(getAiModelCapabilities(generic, 'reference-image', metadata(['text', 'image'], ['image'], ['/images/generations']))).toMatchObject({ allowedStages: [], reason: '参考图待适配:目录已声明图片输入,当前传入方式尚未接入' }) const undeclaredReference = metadata(['text'], ['image'], ['/images/generations']) undeclaredReference.inputDeclared = false undeclaredReference.protocols = [{ id: 'openai-image-generate', path: '/v1/images/generations', headers: {}, supportsImageInput: false }] expect(getAiModelCapabilities(generic, 'undeclared-reference', undeclaredReference)).toMatchObject({ allowedStages: [], reason: '参考图待适配:当前目录尚未确认可用的参考图传入方式' }) const unavailable = { ...metadata(['text', 'image'], ['image'], ['/images/edits']), unavailable: true } expect(getAiModelCapabilities(generic, 'unavailable', unavailable)).toMatchObject({ allowedStages: [], reason: '上游当前不可用,暂不能测试或开放' }) }) test('selected reference route declarations override the default offer summary without claiming a real test passed', () => { const referenceRoute = { id: 'reference-route', input_modes: ['text', 'image_url'], output_modes: ['image'], invocation: { ...invocation('/v1/images/edits', { 'X-Relay-Route-Option': 'reference-route' }), input_schema: { properties: { 'image[]': { type: 'array' } } }, } } const entry = model({ type: 'image', modality: 'image', capabilities: ['text_to_image', 'image_to_image'], input_modes: ['text'], output_modes: ['text'], service_offers: [ { is_default: true, service_scope: { id: 'scope-default' }, input_modes: ['text'], output_modes: ['text'], route_options: [ { id: 'default-route', invocation: { ...invocation('/v1/images/generations', { 'X-Relay-Route-Option': 'default-route' }), content_type: 'application/json' } }, referenceRoute, ] }, ] }) const evidence = publicMetadata(entry, 'tool') expect(evidence).toMatchObject({ inputModalities: ['image', 'text'], outputModalities: ['image'], inputDeclared: true, outputDeclared: true }) expect(getAiModelCapabilities(base, entry.id, evidence)).toMatchObject({ kind: 'image', allowedStages: ['image'], reason: '接口已适配;通过对应用途的实际测试后可开放', protocols: { image: { routeId: 'reference-route' } } }) }) test('a reference adapter remains testable when the directory summary only lists text input', () => { const entry = model({ type: 'image', modality: 'image', capabilities: ['image_to_image'], input_modes: ['text'], invocation: { ...invocation('/v1/images/edits'), input_schema: { properties: { image: { type: 'string', format: 'binary' } } }, } }) const evidence = publicMetadata(entry, 'tool') expect(evidence).toMatchObject({ inputModalities: ['image', 'text'], outputModalities: ['image'] }) expect(getAiModelCapabilities(base, entry.id, evidence)).toMatchObject({ kind: 'image', allowedStages: ['image'] }) }) test('generic URL input does not become image input until a concrete reference adapter is available', () => { const entry = model({ type: 'image', modality: 'image', capabilities: ['text_to_image', 'image_to_image'], input_modes: ['text', 'url'], invocation: { ...invocation('/v1/images/generations'), content_type: 'application/json', } }) const evidence = publicMetadata(entry, 'tool') expect(evidence.inputModalities).toEqual(['text', 'url']) expect(getAiModelCapabilities(base, entry.id, evidence)).toMatchObject({ allowedStages: [], reason: '参考图待适配:当前目录尚未确认可用的参考图传入方式' }) }) test('an explicit non-image output declaration keeps an image route closed with a precise adaptation reason', () => { const evidence = { ...metadata(['text', 'image'], ['text'], ['/images/edits']), kindHint: 'image' as const } expect(getAiModelCapabilities(base, 'any-image-model', evidence)).toMatchObject({ allowedStages: [], reason: '参考图待适配:当前路线的输出声明与生图要求不一致' }) }) test('a standard reference probe is testable without claiming a declared or already verified reference contract', () => { const evidence = { ...metadata(['text'], ['image'], ['/images/generations']), protocols: [{ id: 'openai-image-generate', path: '/v1/images/generations', headers: { 'X-Relay-Route-Option': 'candidate-route' }, supportsImageInput: true, inputField: 'image', referenceTransport: 'json-inline', referenceContract: 'probe-required', requestParameters: { n: 1, response_format: 'b64_json' }, maxImages: 8, scopeId: 'candidate-scope', routeId: 'candidate-route', }] } expect(getAiModelCapabilities(base, 'standard-reference-candidate', evidence)).toMatchObject({ kind: 'image', allowedStages: ['image'], reason: '参考图采用标准图片参数;通过实际测试后可开放', protocols: { image: { maxImages: 8, referenceLimitSource: 'verification' } }, }) expect(getAiModelCapabilities(base, 'unavailable-candidate', { ...evidence, unavailable: true })).toMatchObject({ allowedStages: [], reason: '上游当前不可用,暂不能测试或开放', }) expect(aiModelCapabilityFingerprint(evidence)).not.toBe(aiModelCapabilityFingerprint({ ...evidence, protocols: evidence.protocols.map(({ referenceContract: _candidate, ...protocol }) => protocol) })) }) test('audio transcription is not a copy-writing model just because it returns text over chat', () => { expect(getAiModelCapabilities(generic, 'asr', metadata(['audio'], ['text'], ['/chat/completions']))).toMatchObject({ kind: 'asr', allowedStages: [] }) }) test('model names and arbitrary providers cannot inherit verified abilities', () => { expect(getAiModelCapabilities(generic, 'nano-banana-2')).toMatchObject({ source: 'unknown', allowedStages: [] }) expect(getAiModelCapabilities(base, 'nano-banana-unknown')).toMatchObject({ source: 'unknown', allowedStages: [] }) expect(getAiModelCapabilities(base, 'nano-banana-2').allowedStages).toEqual([]) }) test('public catalog classifies models dynamically without guessing names', () => { expect(getAiModelCapabilities(base, 'catalog-model', publicMetadata(model()))).toMatchObject({ kind: 'text', allowedStages: ['analysis', 'copy'], source: 'upstream' }) expect(getAiModelCapabilities(base, 'catalog-model', publicMetadata(model({ modality: 'multimodal' })))).toMatchObject({ kind: 'vision', allowedStages: ['analysis', 'copy'] }) }) test('same-route image-and-text models offer a combined test while explicitly text-only models do not', () => { const multimodal = publicMetadata(model({ modality: 'multimodal', input_modes: ['text', 'image'], output_modes: ['text'] })) expect(getAiModelCapabilities(base, 'catalog-model', multimodal)).toMatchObject({ allowedStages: ['analysis', 'copy'], combinedTextTest: true }) const textOnly = publicMetadata(model({ input_modes: ['text'], output_modes: ['text'] })) expect(getAiModelCapabilities(base, 'catalog-model', textOnly)).toMatchObject({ allowedStages: ['copy'], combinedTextTest: false }) }) test('public image processing tools do not appear as generative image models', () => { const entry = model({ type: 'image', modality: 'image', capabilities: ['media.tasks'], input_modes: ['file'], output_modes: ['image'] }) expect(getAiModelCapabilities(base, entry.id, publicMetadata(entry, 'task'))).toMatchObject({ kind: 'image-tool', allowedStages: [] }) }) test('a declared specialist tool is not opened merely because it shares a chat or image protocol', () => { const evidence = { ...metadata(['text', 'image'], ['text', 'image'], ['/chat/completions', '/images/edits']), kindHint: 'other' as const } expect(getAiModelCapabilities(generic, 'tool', evidence)).toMatchObject({ kind: 'other', allowedStages: [] }) expect(getAiModelCapabilities(base, 'nano-banana-2', evidence).allowedStages).toEqual([]) }) test.each(['header', 'selection', 'non-default'])('route-specific or non-default capability cannot be opened with the default adapter: %s', reason => { const offer = { is_default: reason !== 'non-default', service_scope: { id: 'scope-default' }, invocation: invocation('/v1/chat/completions', reason === 'header' ? { 'X-Relay-Route-Option': 'route' } : {}) } const entry = model({ selection_required: reason === 'selection', invocation: invocation(), service_offers: [offer] }) expect(getAiModelCapabilities(base, entry.id, publicMetadata(entry)).allowedStages).toEqual([]) }) test('non-default image editing offers do not override the selected image generation route', () => { const entry = model({ type: 'image', modality: 'image', capabilities: ['text_to_image', 'openai.images.edits'], input_modes: ['text', 'image'], output_modes: ['image'], service_offers: [ { is_default: true, service_scope: { id: 'scope-default' }, invocation: invocation('/v1/images/generations') }, { is_default: false, service_scope: { id: 'scope-other' }, invocation: invocation('/v1/images/edits') }, ] }) expect(getAiModelCapabilities(base, entry.id, publicMetadata(entry, 'tool'))).toMatchObject({ kind: 'image', allowedStages: [] }) }) test('missing public modality details permit a real image probe without claiming historical verification', () => { const entry = model({ id: 'gemini-2.5-flash' }) expect(getAiModelCapabilities(base, entry.id, publicMetadata(entry))).toMatchObject({ kind: 'text', allowedStages: ['analysis', 'copy'], source: 'upstream' }) }) test.each(['input', 'output', 'endpoint', 'unavailable'])('explicit withdrawal overrides exact verified evidence: %s', reason => { const current = metadata(reason === 'input' ? ['text'] : ['text', 'image'], reason === 'output' ? ['text'] : ['image'], reason === 'endpoint' ? ['/images/generations'] : ['/images/edits']) if (reason === 'unavailable') current.unavailable = true expect(getAiModelCapabilities(base, 'nano-banana-2', current).allowedStages).toEqual([]) }) test('a disabled selected offer and public negative override positive private catalog metadata', () => { const entry = model({ id: 'gemini-2.5-flash', service_offers: [{ is_default: true, available: false, service_scope: { id: 'scope-default' }, invocation: invocation() }] }) const merged = mergeAiModelCapabilityMetadata(metadata(['text', 'image'], ['text'], ['/chat/completions']), publicMetadata(entry)) expect(getAiModelCapabilities(base, entry.id, merged).allowedStages).toEqual([]) }) test('metadata fingerprint is stable after PostgreSQL JSONB reorders properties', () => { const value = metadata(['image', 'text'], ['text'], ['/chat/completions']) expect(aiModelCapabilityFingerprint(value)).toBe(aiModelCapabilityFingerprint(Object.fromEntries(Object.entries(value).reverse()))) }) test('partial capability declarations and unexpected public schemas fail before synchronization writes', () => { expect(() => parseAiModelCapabilityMetadata({ input_modalities: ['text'] })).toThrow('能力字段不完整') expect(() => parseAiModelPublicCapabilityCatalog({ code: 0, data: { schema_version: '3', sections: [] } })).toThrow('能力目录格式不兼容') }) test('standard catalog unavailable status cannot be overridden by positive protocol metadata', () => { const unavailable = parseAiModelCapabilityMetadata({ available: false, input_modalities: ['text', 'image'], output_modalities: ['image'], endpoints: ['/images/edits'] }) expect(getAiModelCapabilities(base, 'nano-banana-2', unavailable).allowedStages).toEqual([]) }) test('an ID-only private model entry can still withdraw a publicly available capability', () => { const unavailable = parseAiModelCapabilityMetadata({ id: 'gemini-2.5-flash', object: 'model', available: false }) const merged = mergeAiModelCapabilityMetadata(unavailable, publicMetadata(model({ id: 'gemini-2.5-flash' }))) expect(getAiModelCapabilities(base, 'gemini-2.5-flash', merged)).toMatchObject({ kind: 'text', allowedStages: [] }) })