gongxiangp2/components/admin/AiModelRecovery.test.tsx

95 lines
8.6 KiB
TypeScript

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(<AiModelManagementPanel />)
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(<AiModelManagementPanel />)
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(<AiModelManagementPanel />)
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(<AiModelManagementPanel />)
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(<AiModelManagementPanel />)
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(<AiModelManagementPanel />)
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)
})