95 lines
8.6 KiB
TypeScript
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)
|
|
})
|