|
- import type { APIRequestContext, TestInfo } from '@playwright/test'
-
- import { attachJson, captureScreenshot, expect, runId, runPrefix, test } from './fixtures'
- import { authHeaders, confirmMessageBox, envelopeData, loginAsAdmin, visibleDialog } from './helpers'
-
- type JsonRecord = Record<string, unknown>
- type Headers = Record<string, string>
-
- interface Activity {
- module: string
- action: string
- result: 'PASS' | 'FAIL' | 'CLEANED' | 'CLEANUP_FAILED'
- httpStatus?: number
- resourceId?: string
- detail?: string
- }
-
- interface ResponseLike {
- status(): number
- json(): Promise<unknown>
- }
-
- const asRecord = (value: unknown): JsonRecord => value && typeof value === 'object' && !Array.isArray(value)
- ? value as JsonRecord
- : {}
-
- const asRecords = (value: unknown): JsonRecord[] => Array.isArray(value) ? value.map(asRecord) : []
-
- async function responseData(response: ResponseLike, expected: number | number[], label: string) {
- const statuses = Array.isArray(expected) ? expected : [expected]
- expect(statuses, `${label}:HTTP ${response.status()}`).toContain(response.status())
- return asRecord(envelopeData(await response.json()))
- }
-
- function addPass(
- activities: Activity[],
- action: string,
- response?: ResponseLike,
- resourceId?: string,
- detail?: string,
- ) {
- activities.push({ module: 'AI模型', action, result: 'PASS', httpStatus: response?.status(), resourceId, detail })
- }
-
- async function listTestModels(request: APIRequestContext, headers: Headers) {
- const response = await request.get('/api/v1/capabilities', {
- headers,
- params: { capabilityType: 'LLM', keyword: runPrefix, page: '1', pageSize: '200' },
- })
- const payload = await responseData(response, 200, '读取测试 AI 模型')
- return asRecords(payload.items ?? payload.records)
- }
-
- async function cleanupModels(
- request: APIRequestContext,
- headers: Headers,
- activities: Activity[],
- cleanupErrors: string[],
- ) {
- try {
- for (const item of await listTestModels(request, headers)) {
- if (!String(item.name ?? '').startsWith(runPrefix)) continue
- const id = String(item.id ?? '')
- if (!id) continue
- const detailResponse = await request.get(`/api/v1/capabilities/${encodeURIComponent(id)}`, { headers })
- if (detailResponse.status() === 404) continue
- const detail = await responseData(detailResponse, 200, '刷新待清理 AI 模型版本')
- const removed = await request.delete(`/api/v1/capabilities/${encodeURIComponent(id)}`, {
- headers,
- params: { dataVersion: String(Number(detail.dataVersion ?? 0)) },
- })
- if (![200, 404].includes(removed.status())) throw new Error(`AI 模型 ${id} 删除失败:HTTP ${removed.status()}`)
- activities.push({ module: 'AI模型', action: '失败兜底清理', result: 'CLEANED', httpStatus: removed.status(), resourceId: id })
- }
- } catch (error) {
- const message = `AI 模型兜底清理异常:${error instanceof Error ? error.message : String(error)}`
- cleanupErrors.push(message)
- activities.push({ module: 'AI模型', action: '失败兜底清理', result: 'CLEANUP_FAILED', detail: message })
- }
- }
-
- async function attachLifecycleReport(
- testInfo: TestInfo,
- activities: Activity[],
- cleanupErrors: string[],
- ) {
- await attachJson(testInfo, 'AI模型UI-页面生命周期与清理', {
- runId,
- runPrefix,
- activities,
- cleanupComplete: cleanupErrors.length === 0,
- cleanupErrors,
- security: '报告未记录管理员密码、访问令牌或外部模型服务密钥。测试模型只使用不可解析的本地配置标识。',
- })
- }
-
- test('AI模型页面:禁用安全模型新增、参数编辑、可解释预览、校验、启停、筛选与删除', async ({ page, request }, testInfo) => {
- test.setTimeout(120_000)
- const headers = await authHeaders(request)
- const activities: Activity[] = []
- const cleanupErrors: string[] = []
- const modelName = `${runPrefix}-离线内容安全模型`.slice(0, 50)
- const initialDescription = `${runPrefix} 维修问答输入输出安全审查`.slice(0, 200)
- const editedDescription = `${runPrefix} 维修问答输入输出安全审查,返回命中规则与处置依据`.slice(0, 200)
- const initialReference = `${runPrefix.toLowerCase()}-safety-guard:v1`
- const editedReference = `${runPrefix.toLowerCase()}-safety-guard:v2?threshold=0.12&explain=true`
- let modelId = ''
- let primaryError: unknown
-
- await cleanupModels(request, headers, activities, cleanupErrors)
- activities.length = 0
- cleanupErrors.length = 0
-
- try {
- await loginAsAdmin(page)
- await page.goto('/assets/ai-models')
- await expect(page.getByText('AI模型', { exact: true }).first()).toBeVisible()
- await expect(page.getByText('正在加载配置')).toHaveCount(0)
-
- await page.getByRole('button', { name: /新增AI模型/ }).click()
- let dialog = visibleDialog(page)
- await dialog.locator('.capability-form > label').filter({ hasText: '名称' }).locator('input').fill(modelName)
- await dialog.locator('.capability-form > label').filter({ hasText: '分类' }).locator('select').selectOption({ label: '离线问答引擎' })
- await dialog.locator('.capability-form > label').filter({ hasText: '说明' }).locator('textarea').fill(initialDescription)
- await dialog.locator('.capability-form-technical summary').click()
- await dialog.locator('.capability-form-technical input').fill(initialReference)
- const enabledSwitch = dialog.locator('.switch-field .el-switch')
- if ((await enabledSwitch.getAttribute('class'))?.includes('is-checked')) await enabledSwitch.click()
-
- const createResponsePromise = page.waitForResponse((response) => response.request().method() === 'POST' && /\/api\/v1\/capabilities(?:\?|$)/.test(response.url()))
- await dialog.getByRole('button', { name: '保存', exact: true }).click()
- const createResponse = await createResponsePromise
- const created = await responseData(createResponse, 201, '页面新增禁用安全模型')
- modelId = String(created.id ?? '')
- expect(modelId).not.toBe('')
-
- let card = page.locator('.capability-card').filter({ hasText: modelName })
- await expect(card).toBeVisible()
- await expect(card.locator('.status-pill')).toHaveText('已停用')
- const createdDetailResponse = await request.get(`/api/v1/capabilities/${encodeURIComponent(modelId)}`, { headers })
- const createdDetail = await responseData(createdDetailResponse, 200, 'API 后验新增安全模型')
- expect(createdDetail).toMatchObject({
- name: modelName,
- type: '离线问答引擎',
- description: initialDescription,
- reference: initialReference,
- enabled: false,
- })
- addPass(activities, '通过页面新增禁用状态的离线内容安全模型并完成 API 后验', createResponse, modelId)
- await captureScreenshot(page, testInfo, 'AI模型-01-禁用安全模型页面新增')
-
- await card.getByRole('button', { name: '编辑', exact: true }).click()
- dialog = visibleDialog(page)
- await dialog.locator('.capability-form > label').filter({ hasText: '说明' }).locator('textarea').fill(editedDescription)
- await dialog.locator('.capability-form-technical summary').click()
- await dialog.locator('.capability-form-technical input').fill(editedReference)
- const editResponsePromise = page.waitForResponse((response) => response.request().method() === 'PUT'
- && response.url().includes(`/api/v1/capabilities/${modelId}`)
- && !response.url().includes('/status'))
- await dialog.getByRole('button', { name: '保存', exact: true }).click()
- const editResponse = await editResponsePromise
- await responseData(editResponse, 200, '页面编辑安全模型参数')
-
- await page.reload()
- const keyword = page.getByPlaceholder('搜索AI模型名称、分类或说明')
- await keyword.fill(modelName)
- card = page.locator('.capability-card').filter({ hasText: modelName })
- await expect(card).toContainText(editedDescription)
- await card.locator('.capability-technical-info summary').click()
- await expect(card).toContainText(editedReference)
- const editedDetailResponse = await request.get(`/api/v1/capabilities/${encodeURIComponent(modelId)}`, { headers })
- const editedDetail = await responseData(editedDetailResponse, 200, 'API 后验编辑安全模型参数')
- expect(editedDetail).toMatchObject({ description: editedDescription, reference: editedReference, enabled: false })
- addPass(activities, '页面编辑模型用途与配置参数,刷新和 API 均保持', editResponse, modelId)
- await captureScreenshot(page, testInfo, 'AI模型-02-参数编辑刷新持久化')
-
- await card.getByRole('button', { name: '预览', exact: true }).click()
- const previewMessage = page.locator('.el-message:visible').last()
- await expect(previewMessage).toContainText(modelName)
- await expect(previewMessage).toContainText('离线问答引擎')
- await expect(previewMessage).toContainText('用途')
- await expect(previewMessage).toContainText(editedDescription)
- await expect(previewMessage).toContainText(editedReference)
- addPass(activities, '页面预览展示模型分类、用途和配置标识的可解释结果', undefined, modelId)
- await captureScreenshot(page, testInfo, 'AI模型-03-可解释预览结果')
-
- const verifyResponsePromise = page.waitForResponse((response) => response.request().method() === 'POST'
- && response.url().includes(`/api/v1/capabilities/${modelId}/verify`))
- await card.getByRole('button', { name: '校验本地配置' }).click()
- const verifyResponse = await verifyResponsePromise
- const verified = await responseData(verifyResponse, 200, '页面校验安全模型配置')
- expect(verified).toMatchObject({ valid: true, message: '配置可用' })
- await expect(page.getByText('配置可用').last()).toBeVisible()
- const verifiedDetailResponse = await request.get(`/api/v1/capabilities/${encodeURIComponent(modelId)}`, { headers })
- const verifiedDetail = await responseData(verifiedDetailResponse, 200, 'API 后验模型校验状态')
- expect(verifiedDetail.validationStatus).toBe('AVAILABLE')
- addPass(activities, '页面校验配置并以 API 确认 AVAILABLE 状态', verifyResponse, modelId)
- await captureScreenshot(page, testInfo, 'AI模型-04-本地配置校验通过')
-
- const enableResponsePromise = page.waitForResponse((response) => response.request().method() === 'PUT'
- && response.url().includes(`/api/v1/capabilities/${modelId}/status`))
- await card.getByRole('button', { name: '启用', exact: true }).click()
- const enableResponse = await enableResponsePromise
- await responseData(enableResponse, 200, '页面启用安全模型')
- await expect(card.locator('.status-pill')).toHaveText('可用')
-
- const disableResponsePromise = page.waitForResponse((response) => response.request().method() === 'PUT'
- && response.url().includes(`/api/v1/capabilities/${modelId}/status`))
- await card.getByRole('button', { name: '停用', exact: true }).click()
- const disableResponse = await disableResponsePromise
- await responseData(disableResponse, 200, '页面停用安全模型')
- await expect(card.locator('.status-pill')).toHaveText('已停用')
-
- await page.reload()
- await keyword.fill(modelName)
- card = page.locator('.capability-card').filter({ hasText: modelName })
- await expect(card.locator('.status-pill')).toHaveText('已停用')
- const statusFilter = page.getByLabel('状态筛选')
- await statusFilter.selectOption('enabled')
- await expect(card).toHaveCount(0)
- await expect(page.getByText('没有匹配的配置')).toBeVisible()
- await statusFilter.selectOption('disabled')
- await expect(page.locator('.capability-card').filter({ hasText: modelName })).toBeVisible()
- const disabledDetailResponse = await request.get(`/api/v1/capabilities/${encodeURIComponent(modelId)}`, { headers })
- const disabledDetail = await responseData(disabledDetailResponse, 200, 'API 后验最终停用状态')
- expect(disabledDetail.enabled).toBe(false)
- addPass(activities, '页面启用后再停用,刷新、状态筛选和 API 状态一致', disableResponse, modelId)
- await captureScreenshot(page, testInfo, 'AI模型-05-停用筛选刷新持久化')
-
- await statusFilter.selectOption('all')
- card = page.locator('.capability-card').filter({ hasText: modelName })
- await card.getByRole('button', { name: '删除', exact: true }).click()
- const deleteResponsePromise = page.waitForResponse((response) => response.request().method() === 'DELETE'
- && response.url().includes(`/api/v1/capabilities/${modelId}`))
- await confirmMessageBox(page, /确定/)
- const deleteResponse = await deleteResponsePromise
- await responseData(deleteResponse, 200, '页面删除安全模型')
- const deletedModelId = modelId
- modelId = ''
- await expect(page.locator('.capability-card').filter({ hasText: modelName })).toHaveCount(0)
- const absent = await request.get(`/api/v1/capabilities/${encodeURIComponent(deletedModelId)}`, { headers })
- expect(absent.status()).toBe(404)
- addPass(activities, '页面删除并确认 API 不可读取', deleteResponse, deletedModelId)
- await captureScreenshot(page, testInfo, 'AI模型-06-删除后无残留')
- } catch (error) {
- primaryError = error
- activities.push({ module: 'AI模型', action: '页面完整闭环', result: 'FAIL', detail: error instanceof Error ? error.message : String(error) })
- } finally {
- if (modelId) await cleanupModels(request, headers, activities, cleanupErrors)
- await attachLifecycleReport(testInfo, activities, cleanupErrors)
- }
-
- if (primaryError) throw primaryError
- expect(cleanupErrors, 'AI 模型测试资源必须清理干净').toEqual([])
- })
|