import { act, fireEvent, render, screen, waitFor, within } from '@testing-library/react' import AiModelManagementPanel from './AiModelManagementPanel' import type { AiModelAdminState } from '@/lib/ai-model-types' const snapshot = (): AiModelAdminState => ({ providers: [{ id: 'provider', name: '平台模型服务', baseUrl: 'https://models.example.com', enabled: true, hasApiKey: true, credentialSource: 'environment', revision: 1, syncedAt: null, models: [{ key: 'model', id: 'vision-model', present: true, enabled: false, stages: [], capabilities: { kind: 'vision', allowedStages: ['analysis'], source: 'upstream', reason: 'vision' }, verification: { analysis: { status: 'untested', id: null, checkedAt: null, requestId: null, message: null }, copy: { status: 'untested', id: null, checkedAt: null, requestId: null, message: null }, image: { status: 'untested', id: null, checkedAt: null, requestId: null, message: null } } }] }], revision: 1, defaults: { analysis: null, copy: null, image: null }, encryptionConfigured: true, legacyAvailable: false }) const response = (body: unknown) => ({ ok: true, status: 200, json: async () => body } as Response) const testName = '测试 vision-model · 平台模型服务 的资料分析', intakeName = '核查 vision-model · 平台模型服务 的测试提交' beforeEach(() => sessionStorage.clear()) afterEach(() => { jest.restoreAllMocks(); jest.useRealTimers() }) test('lost intake can be closed without repeating a paid call, then a new explicit test is available', async () => { global.fetch = jest.fn(async (url: string, init?: RequestInit) => { if (url.endsWith('/verify')) throw new Error('响应丢失') if (url.endsWith('/submission')) return response({ ...snapshot(), submissionCheck: { modelKey: 'model', ...JSON.parse(init!.body as string), received: false } }) return response(snapshot()) }) as typeof fetch render() fireEvent.click(await screen.findByRole('button', { name: testName })) fireEvent.click(within(await screen.findByRole('dialog')).getByRole('button', { name: '开始测试' })) fireEvent.click(await screen.findByRole('button', { name: intakeName })) await screen.findByText('原提交未被受理,已关闭,可以重新测试') expect(screen.getByRole('button', { name: testName })).toBeEnabled() expect((global.fetch as jest.Mock).mock.calls.filter(([url]) => url.endsWith('/verify'))).toHaveLength(1) expect(sessionStorage.getItem('ai-model-pending-submissions-v1')).toBe('[]') }) test('reload restores lost intake, server-confirmed same submission clears it without creating a new call', async () => { const key = '11111111-1111-4111-8111-111111111111' sessionStorage.setItem('ai-model-pending-submissions-v1', JSON.stringify([['model', { stage: 'analysis', requestId: key, previousId: null }]])) const known = snapshot(); known.providers[0].models[0].verification.analysis = { status: 'passed', id: 'verification', checkedAt: null, requestId: 'upstream', message: null, submissionKey: key } global.fetch = jest.fn(async () => response(known)) as typeof fetch render() await screen.findByText('已通过', { selector: 'p' }) expect(screen.queryByRole('button', { name: intakeName })).not.toBeInTheDocument() expect(screen.getByRole('checkbox', { name: 'vision-model · 平台模型服务 用于资料分析' })).toBeEnabled() expect((global.fetch as jest.Mock).mock.calls.every(([, init]) => !init?.method)).toBe(true) }) test('evidence-based recovery never enables or approves a model, keeps pending configuration', async () => { const unknown = snapshot(); unknown.providers[0].models[0].verification.analysis = { status: 'unknown', id: 'verification', checkedAt: null, requestId: 'request', message: '结果未知', canReconcile: true } const closed = snapshot(); closed.providers[0].models[0].verification.analysis = { ...unknown.providers[0].models[0].verification.analysis, status: 'failed', canReconcile: false, reconciled: true } global.fetch = jest.fn(async (url: string) => response(url.endsWith('/reconcile') ? closed : unknown)) as typeof fetch render() fireEvent.click(await screen.findByRole('button', { name: '核查 vision-model · 平台模型服务 的资料分析结果' })) expect(screen.getByRole('button', { name: '记录核查' })).toBeDisabled() fireEvent.change(screen.getByLabelText('凭证编号'), { target: { value: 'upstream-123' } }) fireEvent.change(screen.getByLabelText('核查说明'), { target: { value: '已核查上游日志,原请求失败并已结束。' } }) fireEvent.click(screen.getByRole('checkbox', { name: '已核查,原请求已结束' })) fireEvent.click(screen.getByRole('button', { name: '记录核查' })) await screen.findByText('已核查,可重测') expect(screen.getByRole('checkbox', { name: 'vision-model · 平台模型服务 用于资料分析' })).toBeDisabled() expect(screen.getByRole('checkbox', { name: '开放 vision-model · 平台模型服务 给用户' })).not.toBeChecked() expect((global.fetch as jest.Mock).mock.calls.filter(([, init]) => init?.method === 'POST')).toHaveLength(1) expect((global.fetch as jest.Mock).mock.calls.some(([url]) => url.endsWith('/verify'))).toBe(false) }) test('a known customer image request is retrieved without generation or automatic enabling', async () => { const requestId = '1c8c2fa5-5ad0-436b-977f-a047a64fa845', unknown = snapshot(), model = unknown.providers[0].models[0] model.id = 'image-model'; model.capabilities = { kind: 'image', allowedStages: ['image'], source: 'upstream', reason: '', protocols: { image: { id: 'openai-image-generate', path: '/v1/images/generations' } } } model.verification.image = { status: 'unknown', id: 'verification', checkedAt: null, requestId: null, message: '连接中断', canReconcile: true } const passed = JSON.parse(JSON.stringify(unknown)); passed.providers[0].models[0].verification.image = { ...model.verification.image, status: 'passed', requestId, canReconcile: false, verifiedReferenceImageCount: 4 } global.fetch = jest.fn(async (url: string) => response(url.endsWith('/refresh') ? passed : unknown)) as typeof fetch render() fireEvent.click(await screen.findByRole('button', { name: '核查 image-model · 平台模型服务 的图片生成结果' })) expect(screen.getByRole('button', { name: '取回原结果' })).toBeDisabled() fireEvent.change(screen.getByLabelText('客户请求编号'), { target: { value: requestId } }) fireEvent.click(screen.getByRole('button', { name: '取回原结果' })) await screen.findByText('原请求查询已完成,请查看测试状态') const posts = (global.fetch as jest.Mock).mock.calls.filter(([, init]) => init?.method === 'POST') expect(posts).toHaveLength(1); expect(posts[0][0]).toMatch(/\/refresh$/) expect(JSON.parse(posts[0][1].body)).toEqual({ requestId }) expect(screen.getByRole('checkbox', { name: '开放 image-model · 平台模型服务 给用户' })).not.toBeChecked() }) test('a running request polls read-only state without resubmitting; unmount cancels polling', async () => { jest.useFakeTimers() const running = snapshot(); running.providers[0].models[0].verification.analysis.status = 'running' global.fetch = jest.fn(async () => response(running)) as typeof fetch const view = render() await screen.findByText('测试进行中,自动更新状态') await act(async () => { await jest.advanceTimersByTimeAsync(3100) }) expect(global.fetch).toHaveBeenCalledTimes(2) expect((global.fetch as jest.Mock).mock.calls.every(([, init]) => !init?.method)).toBe(true) view.unmount() await act(async () => { await jest.advanceTimersByTimeAsync(6000) }) expect(global.fetch).toHaveBeenCalledTimes(2) }) test('withdrawn capability and disabled upstream still expose the original unknown recovery action', async () => { const withdrawn = snapshot(); withdrawn.providers[0].enabled = false const model = withdrawn.providers[0].models[0]; model.present = false; model.capabilities.allowedStages = [] model.verification.analysis = { status: 'unknown', id: 'old-verification', taskId: 'old-task', checkedAt: null, requestId: 'old-request', message: '原请求仍需核查', canReconcile: true, canResume: false } global.fetch = jest.fn(async () => response(withdrawn)) as typeof fetch render() const recovery = await screen.findByRole('button', { name: '核查 vision-model · 平台模型服务 的资料分析结果' }) expect(recovery).toBeEnabled() fireEvent.click(recovery) expect(screen.getByLabelText('凭证编号')).toBeEnabled() expect((global.fetch as jest.Mock).mock.calls.every(([, init]) => !init?.method)).toBe(true) })