import type { APIRequestContext, TestInfo } from '@playwright/test' import { attachJson, captureScreenshot, expect, runId, runPrefix, test } from './fixtures' import { authHeaders, confirmMessageBox, envelopeData, loginAsAdmin, visibleDialog } from './helpers' test.describe.configure({ mode: 'serial' }) test.use({ trace: 'off', video: 'off' }) type JsonRecord = Record type Headers = Record interface Activity { module: string action: string result: 'PASS' | 'FAIL' | 'CLEANED' | 'CLEANUP_FAILED' httpStatus?: number resourceId?: string detail?: string } interface ResponseLike { status(): number json(): Promise } interface DictionaryItem { id?: string kind: 'term' | 'homophone' term?: string wrongText?: string rightText?: string enabled: boolean builtIn?: boolean } 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[], module: string, action: string, response?: ResponseLike, resourceId?: string, detail?: string, ) { activities.push({ module, action, result: 'PASS', httpStatus: response?.status(), resourceId, detail }) } async function attachLifecycleReport( testInfo: TestInfo, title: string, activities: Activity[], cleanupErrors: string[], ) { await attachJson(testInfo, `${title}-页面生命周期与清理`, { runId, runPrefix, title, activities, cleanupComplete: cleanupErrors.length === 0, cleanupErrors, security: '报告未记录管理员密码、访问令牌或外部服务密钥。', }) } async function waitForDocumentReady( request: APIRequestContext, headers: Headers, documentId: string, timeoutMs = 60_000, ) { const deadline = Date.now() + timeoutMs let latest: JsonRecord = {} while (Date.now() < deadline) { const response = await request.get(`/api/v1/knowledge/documents/${encodeURIComponent(documentId)}`, { headers }) latest = await responseData(response, 200, '等待知识资料建立索引') const status = String(latest.status ?? '').toLowerCase() if (status === 'ready') return latest if (status === 'failed') throw new Error(`知识资料建立索引失败:${String(latest.errorMessage ?? '未返回原因')}`) await new Promise((resolve) => setTimeout(resolve, 500)) } throw new Error(`知识资料在 ${timeoutMs}ms 内未进入 ready,最后状态:${String(latest.status ?? 'unknown')}`) } async function cleanupKnowledgeByPrefix( request: APIRequestContext, headers: Headers, activities: Activity[], cleanupErrors: string[], ) { try { const listed = await request.get('/api/v1/knowledge/bases', { headers, params: { keyword: runPrefix, page: '1', pageSize: '200' }, }) const page = await responseData(listed, 200, '清理前读取知识库') for (const base of asRecords(page.items)) { const baseId = String(base.id ?? '') if (!baseId || !String(base.name ?? '').startsWith(runPrefix)) continue const documentsResponse = await request.get('/api/v1/knowledge/documents', { headers, params: { knowledgeBaseId: baseId, page: '1', pageSize: '200' }, }) if (documentsResponse.status() === 200) { const documents = asRecord(envelopeData(await documentsResponse.json())) for (const item of asRecords(documents.items)) { const documentId = String(item.id ?? '') if (!documentId) continue const detailResponse = await request.get(`/api/v1/knowledge/documents/${encodeURIComponent(documentId)}`, { headers }) if (detailResponse.status() !== 200) continue const detail = asRecord(envelopeData(await detailResponse.json())) const removed = await request.delete(`/api/v1/knowledge/documents/${encodeURIComponent(documentId)}`, { headers, params: { dataVersion: String(Number(detail.dataVersion ?? 0)) }, }) if (![200, 404].includes(removed.status())) throw new Error(`资料 ${documentId} 删除失败:HTTP ${removed.status()}`) activities.push({ module: '维修知识', action: '资料失败兜底清理', result: 'CLEANED', httpStatus: removed.status(), resourceId: documentId }) } } const currentList = await request.get('/api/v1/knowledge/bases', { headers, params: { keyword: String(base.name ?? runPrefix), page: '1', pageSize: '200' }, }) const currentPage = asRecord(envelopeData(await currentList.json())) const current = asRecords(currentPage.items).find((item) => String(item.id) === baseId) if (!current) continue const removedBase = await request.delete(`/api/v1/knowledge/bases/${encodeURIComponent(baseId)}`, { headers, params: { dataVersion: String(Number(current.dataVersion ?? 0)) }, }) if (![200, 404].includes(removedBase.status())) throw new Error(`知识库 ${baseId} 删除失败:HTTP ${removedBase.status()}`) activities.push({ module: '维修知识', action: '知识库失败兜底清理', result: 'CLEANED', httpStatus: removedBase.status(), resourceId: baseId }) } } catch (error) { const message = `知识库兜底清理异常:${error instanceof Error ? error.message : String(error)}` cleanupErrors.push(message) activities.push({ module: '维修知识', action: '失败兜底清理', result: 'CLEANUP_FAILED', detail: message }) } } const dictionaryItem = (value: JsonRecord): DictionaryItem => ({ id: String(value.id ?? '') || undefined, kind: String(value.kind) === 'homophone' ? 'homophone' : 'term', term: value.term == null ? undefined : String(value.term), wrongText: value.wrongText == null ? undefined : String(value.wrongText), rightText: value.rightText == null ? undefined : String(value.rightText), enabled: value.enabled !== false, builtIn: value.builtIn == null ? undefined : Boolean(value.builtIn), }) const dictionaryKey = (item: DictionaryItem) => item.kind === 'term' ? `term:${item.term ?? ''}` : `homophone:${item.wrongText ?? ''}=>${item.rightText ?? ''}` const dictionarySummary = (items: DictionaryItem[]) => items .map((item) => ({ kind: item.kind, term: item.term ?? '', wrongText: item.wrongText ?? '', rightText: item.rightText ?? '', enabled: item.enabled, builtIn: Boolean(item.builtIn), })) .sort((left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right))) async function getDictionary(request: APIRequestContext, headers: Headers) { const response = await request.get('/api/v1/asr-dictionaries', { headers }) const payload = await responseData(response, 200, '读取 ASR 词典') return { items: asRecords(payload.items).map(dictionaryItem), dataVersion: Number(payload.dataVersion ?? 0), } } async function saveDictionarySnapshot( request: APIRequestContext, headers: Headers, desired: DictionaryItem[], current: { items: DictionaryItem[]; dataVersion: number }, ) { const ids = new Map(current.items.map((item) => [dictionaryKey(item), item.id])) return request.put('/api/v1/asr-dictionaries', { headers, data: { items: desired.map((item) => ({ ...(ids.get(dictionaryKey(item)) ? { id: ids.get(dictionaryKey(item)) } : {}), kind: item.kind, ...(item.kind === 'term' ? { term: item.term } : { wrongText: item.wrongText, rightText: item.rightText }), enabled: item.enabled, builtIn: item.builtIn, })), dataVersion: current.dataVersion, }, }) } async function restoreDictionary( request: APIRequestContext, headers: Headers, snapshot: DictionaryItem[], ) { let current = await getDictionary(request, headers) let response = await saveDictionarySnapshot(request, headers, snapshot, current) await responseData(response, 200, '恢复 ASR 词典原始内容') // 页面保存会重新生成词典行。若原快照含内置标记,先让服务恢复标记,再按快照做最后一次精确裁剪。 if (snapshot.some((item) => item.builtIn)) { response = await request.post('/api/v1/asr-dictionaries/restore-built-in', { headers, data: {} }) await responseData(response, 200, '恢复 ASR 内置词条标记') current = await getDictionary(request, headers) response = await saveDictionarySnapshot(request, headers, snapshot, current) await responseData(response, 200, '按原快照精确裁剪 ASR 词典') } return response } async function cleanupCapabilityByPrefix( request: APIRequestContext, headers: Headers, activities: Activity[], cleanupErrors: string[], ) { try { const response = await request.get('/api/v1/capabilities', { headers, params: { capabilityType: 'ASR', keyword: runPrefix, page: '1', pageSize: '200' }, }) const page = await responseData(response, 200, '清理前读取 ASR 能力') for (const item of asRecords(page.items ?? page.records)) { const id = String(item.id ?? '') if (!id || !String(item.name ?? '').startsWith(runPrefix)) continue const detailResponse = await request.get(`/api/v1/capabilities/${encodeURIComponent(id)}`, { headers }) if (detailResponse.status() === 404) continue const detail = await responseData(detailResponse, 200, '清理前刷新 ASR 能力版本') 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(`ASR 能力 ${id} 删除失败:HTTP ${removed.status()}`) activities.push({ module: 'ASR模型', action: '失败兜底清理', result: 'CLEANED', httpStatus: removed.status(), resourceId: id }) } } catch (error) { const message = `ASR 能力兜底清理异常:${error instanceof Error ? error.message : String(error)}` cleanupErrors.push(message) activities.push({ module: 'ASR模型', action: '失败兜底清理', result: 'CLEANUP_FAILED', detail: message }) } } test('维修知识页面:个人库、资料重建、启停、问答引用、下载与删除完整闭环', async ({ page, request }, testInfo) => { test.setTimeout(150_000) const headers = await authHeaders(request) const activities: Activity[] = [] const cleanupErrors: string[] = [] const baseName = `${runPrefix}-液压安全知识库`.slice(0, 80) const documentName = `${runPrefix}-液压安全规程.txt`.slice(0, 150) const editedDocumentName = `${runPrefix}-液压安全规程-已重建.txt`.slice(0, 150) const uniqueRule = `${runPrefix}液压安全口令:拆卸测试液压管路前必须先停机、断电、验电、完全卸压、挂牌上锁,并确认压力表归零。` let baseId = '' let documentId = '' let primaryError: unknown await cleanupKnowledgeByPrefix(request, headers, activities, cleanupErrors) cleanupErrors.length = 0 activities.length = 0 try { await loginAsAdmin(page) await page.goto('/knowledge/library') await page.getByRole('tab', { name: '用户自建' }).click() await expect(page.getByRole('button', { name: /新建知识库/ })).toBeVisible() await page.getByRole('button', { name: /新建知识库/ }).click() const createDialog = visibleDialog(page) await createDialog.locator('label').filter({ hasText: '知识库名称' }).locator('input').fill(baseName) await createDialog.locator('label').filter({ hasText: '知识库说明' }).locator('textarea').fill(`${runPrefix} 页面提交与持久化验证`) await createDialog.locator('label').filter({ hasText: '专业分类' }).locator('select').selectOption({ label: '液压气动' }) await createDialog.locator('label').filter({ hasText: '标签' }).locator('input').fill('液压,安全,APE2E') const createResponsePromise = page.waitForResponse((response) => response.request().method() === 'POST' && /\/api\/v1\/knowledge\/bases(?:\?|$)/.test(response.url())) await createDialog.getByRole('button', { name: '创建并进入' }).click() const createResponse = await createResponsePromise const createdBase = await responseData(createResponse, 201, '页面创建个人知识库') baseId = String(createdBase.id ?? '') expect(baseId).not.toBe('') await expect(page.getByRole('heading', { name: baseName })).toBeVisible() addPass(activities, '维修知识', '页面创建个人知识库并进入详情', createResponse, baseId) await captureScreenshot(page, testInfo, '01-personal-knowledge-base-created') const uploadResponsePromise = page.waitForResponse((response) => response.request().method() === 'POST' && /\/api\/v1\/knowledge\/documents(?:\?|$)/.test(response.url())) await page.locator('input[type="file"]').setInputFiles({ name: documentName, mimeType: 'text/plain', buffer: Buffer.from(`${uniqueRule}\n复核要求:作业完成后空载试运行并记录压力、温升和异常声响。`, 'utf8'), }) const uploadResponse = await uploadResponsePromise const uploadPayload = await responseData(uploadResponse, 201, '页面上传知识资料') documentId = String(asRecord(uploadPayload.document).id ?? '') expect(documentId).not.toBe('') await waitForDocumentReady(request, headers, documentId) await page.reload() const uploadedRow = page.locator('.knowledge-document-list article').filter({ hasText: documentName }) await expect(uploadedRow).toContainText(/个片段/) addPass(activities, '维修知识', 'TXT 上传、后台索引并刷新为可用', uploadResponse, documentId) await captureScreenshot(page, testInfo, '02-document-uploaded-ready') await uploadedRow.getByRole('button', { name: '编辑资料' }).click() let editor = visibleDialog(page) await expect(editor.getByText('正在读取资料正文…')).toHaveCount(0) await editor.locator('label').filter({ hasText: '资料名称' }).locator('input').fill(editedDocumentName) await editor.locator('label').filter({ hasText: '资料状态' }).locator('select').selectOption({ label: '禁用' }) await editor.locator('label').filter({ hasText: '参考资料正文' }).locator('textarea').fill(`${uniqueRule}\n编辑确认:禁用资料不参与问答,重新启用后使用最新正文。`) let updateResponsePromise = page.waitForResponse((response) => response.request().method() === 'PUT' && response.url().includes(`/api/v1/knowledge/documents/${documentId}`)) await editor.getByRole('button', { name: '保存并重建索引' }).click() let updateResponse = await updateResponsePromise await responseData(updateResponse, 200, '页面编辑并禁用知识资料') await waitForDocumentReady(request, headers, documentId) await page.reload() let documentRow = page.locator('.knowledge-document-list article').filter({ hasText: editedDocumentName }) await expect(documentRow).toContainText('已禁用') addPass(activities, '维修知识', '编辑正文、重建索引并禁用', updateResponse, documentId) await captureScreenshot(page, testInfo, '03-document-edited-disabled') await documentRow.getByRole('button', { name: '编辑资料' }).click() editor = visibleDialog(page) await expect(editor.getByText('正在读取资料正文…')).toHaveCount(0) await editor.locator('label').filter({ hasText: '资料状态' }).locator('select').selectOption({ label: '启用并参与问答' }) updateResponsePromise = page.waitForResponse((response) => response.request().method() === 'PUT' && response.url().includes(`/api/v1/knowledge/documents/${documentId}`)) await editor.getByRole('button', { name: '保存并重建索引' }).click() updateResponse = await updateResponsePromise await responseData(updateResponse, 200, '页面重新启用知识资料') await waitForDocumentReady(request, headers, documentId) await page.reload() documentRow = page.locator('.knowledge-document-list article').filter({ hasText: editedDocumentName }) await expect(documentRow).toContainText(/个片段/) await expect(documentRow).not.toContainText('已禁用') addPass(activities, '维修知识', '重新启用并刷新验证持久化', updateResponse, documentId) await captureScreenshot(page, testInfo, '04-document-enabled-persisted') const question = page.getByLabel('知识库问题') await question.fill(`${runPrefix}液压安全口令要求完成哪些步骤?`) const answerResponsePromise = page.waitForResponse((response) => response.request().method() === 'POST' && response.url().includes('/api/v1/knowledge/answer')) await page.getByRole('button', { name: '生成有依据回答' }).click() const answerResponse = await answerResponsePromise const answer = await responseData(answerResponse, 200, '页面知识问答') expect(String(answer.answer ?? '')).toContain(runPrefix) const citations = page.locator('.knowledge-citations button:not(:disabled)') await expect(page.locator('.knowledge-answer')).toBeVisible() await expect(citations.first()).toBeVisible() addPass(activities, '维修知识', '真实问答命中最新正文并显示引用', answerResponse, documentId, `引用 ${asRecords(answer.results).length} 条`) await captureScreenshot(page, testInfo, '05-answer-with-citation') const downloadPromise = page.waitForEvent('download') await citations.first().click() const download = await downloadPromise expect(download.suggestedFilename()).toContain('.txt') addPass(activities, '维修知识', '从引用下载受保护原文', undefined, documentId) await captureScreenshot(page, testInfo, '06-citation-download-complete') await documentRow.getByRole('button', { name: '编辑资料' }).click() editor = visibleDialog(page) await expect(editor.getByText('正在读取资料正文…')).toHaveCount(0) await editor.getByRole('button', { name: '删除资料' }).click() const deleteDocumentResponsePromise = page.waitForResponse((response) => response.request().method() === 'DELETE' && response.url().includes(`/api/v1/knowledge/documents/${documentId}`)) await confirmMessageBox(page, '删除') const deleteDocumentResponse = await deleteDocumentResponsePromise await responseData(deleteDocumentResponse, 200, '页面删除知识资料') const deletedDocumentId = documentId documentId = '' await expect(page.locator('.knowledge-document-list article').filter({ hasText: editedDocumentName })).toHaveCount(0) addPass(activities, '维修知识', '页面删除知识资料', deleteDocumentResponse, deletedDocumentId) await captureScreenshot(page, testInfo, '07-document-deleted') await page.getByRole('button', { name: /删除知识库/ }).click() const deleteBaseResponsePromise = page.waitForResponse((response) => response.request().method() === 'DELETE' && response.url().includes(`/api/v1/knowledge/bases/${baseId}`)) await confirmMessageBox(page, '确认删除') const deleteBaseResponse = await deleteBaseResponsePromise await responseData(deleteBaseResponse, 200, '页面删除个人知识库') const deletedBaseId = baseId baseId = '' await expect(page).toHaveURL(/\/knowledge\/library(?:\?|$)/) await page.getByRole('tab', { name: '用户自建' }).click() await expect(page.locator('.knowledge-base-card').filter({ hasText: baseName })).toHaveCount(0) addPass(activities, '维修知识', '页面删除个人知识库并从列表消失', deleteBaseResponse, deletedBaseId) await captureScreenshot(page, testInfo, '08-knowledge-base-deleted') } catch (error) { primaryError = error activities.push({ module: '维修知识', action: '页面完整闭环', result: 'FAIL', detail: error instanceof Error ? error.message : String(error) }) } finally { if (baseId || documentId) await cleanupKnowledgeByPrefix(request, headers, activities, cleanupErrors) await attachLifecycleReport(testInfo, '维修知识UI', activities, cleanupErrors) } if (primaryError) throw primaryError expect(cleanupErrors, '知识库测试资源必须清理干净').toEqual([]) }) test('ASR能力页面:词典保存刷新恢复与模型创建、编辑、启停、筛选、删除', async ({ page, request }, testInfo) => { test.setTimeout(120_000) const headers = await authHeaders(request) const activities: Activity[] = [] const cleanupErrors: string[] = [] const capabilityName = `${runPrefix}-本地维修ASR`.slice(0, 90) const testTerm = `${runPrefix}支腿阀组`.slice(0, 180) const wrongText = `${runPrefix}之腿阀组`.slice(0, 180) const rightText = `${runPrefix}支腿阀组`.slice(0, 180) const dictionarySnapshot = await getDictionary(request, headers) let capabilityId = '' let primaryError: unknown let dictionaryRestored = false await cleanupCapabilityByPrefix(request, headers, activities, cleanupErrors) cleanupErrors.length = 0 activities.length = 0 try { await loginAsAdmin(page) await page.goto('/assets/asr-models') const dictionary = page.locator('.asr-dictionary') await dictionary.locator('summary').click() await expect(dictionary.getByRole('button', { name: '保存词典' })).toBeEnabled() const termInput = dictionary.locator('label').filter({ hasText: '专有名词' }).locator('textarea') const homophoneInput = dictionary.locator('label').filter({ hasText: '同音词纠正' }).locator('textarea') const originalTerms = await termInput.inputValue() const originalHomophones = await homophoneInput.inputValue() await termInput.fill([originalTerms, testTerm].filter(Boolean).join('\n')) await homophoneInput.fill([originalHomophones, `${wrongText}=${rightText}`].filter(Boolean).join('\n')) const dictionarySaveResponsePromise = page.waitForResponse((response) => response.request().method() === 'PUT' && /\/api\/v1\/asr-dictionaries(?:\?|$)/.test(response.url())) await dictionary.getByRole('button', { name: '保存词典' }).click() const dictionarySaveResponse = await dictionarySaveResponsePromise await responseData(dictionarySaveResponse, 200, '页面保存 ASR 词典') await expect(page.getByText(/词典已保存/).last()).toBeVisible() addPass(activities, 'ASR词典', '页面追加专有名词与同音词', dictionarySaveResponse) await captureScreenshot(page, testInfo, '09-asr-dictionary-saved') await page.reload() await dictionary.locator('summary').click() await expect(termInput).toHaveValue(new RegExp(testTerm)) await expect(homophoneInput).toHaveValue(new RegExp(wrongText)) const persistedDictionary = await getDictionary(request, headers) expect(persistedDictionary.items.some((item) => item.kind === 'term' && item.term === testTerm)).toBeTruthy() expect(persistedDictionary.items.some((item) => item.kind === 'homophone' && item.wrongText === wrongText && item.rightText === rightText)).toBeTruthy() addPass(activities, 'ASR词典', '页面刷新与 API 均保留词典改动') await captureScreenshot(page, testInfo, '10-asr-dictionary-refresh-persisted') await page.getByRole('button', { name: /新增ASR模型/ }).click() let dialog = visibleDialog(page) await dialog.locator('.capability-form > label').filter({ hasText: '名称' }).locator('input').fill(capabilityName) await dialog.locator('.capability-form > label').filter({ hasText: '说明' }).locator('textarea').fill(`${runPrefix} 页面创建的本地识别能力`) await dialog.locator('label.file-field').filter({ hasText: '识别热词' }).locator('textarea').fill(`${runPrefix}液压缸\n${runPrefix}连接销`) await dialog.locator('.capability-form-technical summary').click() await dialog.locator('.capability-form-technical input').fill(`${runPrefix.toLowerCase()}-local-asr`) 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, '页面创建 ASR 能力') capabilityId = String(created.id ?? '') expect(capabilityId).not.toBe('') let card = page.locator('.capability-card').filter({ hasText: capabilityName }) await expect(card).toBeVisible() await expect(card.locator('.status-pill')).toHaveText('已停用') addPass(activities, 'ASR模型', '页面创建禁用状态的本地 ASR', createResponse, capabilityId) await captureScreenshot(page, testInfo, '11-asr-model-created-disabled') await card.getByRole('button', { name: '编辑', exact: true }).click() dialog = visibleDialog(page) await dialog.locator('.capability-form > label').filter({ hasText: '说明' }).locator('textarea').fill(`${runPrefix} 已编辑、刷新后应保持`) await dialog.locator('label.file-field').filter({ hasText: '识别热词' }).locator('textarea').fill(`${runPrefix}液压缸\n${runPrefix}连接销\n${runPrefix}蓄能器`) const editResponsePromise = page.waitForResponse((response) => response.request().method() === 'PUT' && response.url().includes(`/api/v1/capabilities/${capabilityId}`) && !response.url().includes('/status')) await dialog.getByRole('button', { name: '保存', exact: true }).click() const editResponse = await editResponsePromise const edited = await responseData(editResponse, 200, '页面编辑 ASR 能力') expect(asRecords(edited.hotWords).length || (Array.isArray(edited.hotWords) ? edited.hotWords.length : 0)).toBe(3) addPass(activities, 'ASR模型', '页面编辑说明与识别热词', editResponse, capabilityId) await page.reload() const search = page.getByPlaceholder(/搜索ASR模型名称/) await search.fill(capabilityName) card = page.locator('.capability-card').filter({ hasText: capabilityName }) await expect(card).toContainText('已编辑、刷新后应保持') await expect(card).toContainText(`${runPrefix}蓄能器`) addPass(activities, 'ASR模型', '刷新后编辑内容保持', undefined, capabilityId) await captureScreenshot(page, testInfo, '12-asr-model-edit-persisted') const enableResponsePromise = page.waitForResponse((response) => response.request().method() === 'PUT' && response.url().includes(`/api/v1/capabilities/${capabilityId}/status`)) await card.getByRole('button', { name: '启用', exact: true }).click() const enableResponse = await enableResponsePromise await responseData(enableResponse, 200, '页面启用 ASR 能力') await expect(card.locator('.status-pill')).toHaveText('可用') addPass(activities, 'ASR模型', '页面启用能力', enableResponse, capabilityId) await page.reload() await search.fill(capabilityName) card = page.locator('.capability-card').filter({ hasText: capabilityName }) await expect(card.locator('.status-pill')).toHaveText('可用') const statusFilter = page.getByLabel('状态筛选') await statusFilter.selectOption('enabled') await expect(card).toBeVisible() await statusFilter.selectOption('disabled') await expect(card).toHaveCount(0) await expect(page.getByText('没有匹配的配置')).toBeVisible() addPass(activities, 'ASR模型', '刷新验证启用状态并完成状态筛选', undefined, capabilityId) await captureScreenshot(page, testInfo, '13-asr-status-filter') await statusFilter.selectOption('all') card = page.locator('.capability-card').filter({ hasText: capabilityName }) await card.getByRole('button', { name: '删除', exact: true }).click() const deleteResponsePromise = page.waitForResponse((response) => response.request().method() === 'DELETE' && response.url().includes(`/api/v1/capabilities/${capabilityId}`)) await confirmMessageBox(page, /确定/) const deleteResponse = await deleteResponsePromise await responseData(deleteResponse, 200, '页面删除 ASR 能力') const deletedCapabilityId = capabilityId capabilityId = '' await expect(page.locator('.capability-card').filter({ hasText: capabilityName })).toHaveCount(0) const absent = await request.get(`/api/v1/capabilities/${encodeURIComponent(deletedCapabilityId)}`, { headers }) expect(absent.status()).toBe(404) addPass(activities, 'ASR模型', '页面删除并确认 API 不可读', deleteResponse, deletedCapabilityId) await captureScreenshot(page, testInfo, '14-asr-model-deleted') } catch (error) { primaryError = error activities.push({ module: 'ASR能力与词典', action: '页面完整闭环', result: 'FAIL', detail: error instanceof Error ? error.message : String(error) }) } finally { if (capabilityId) await cleanupCapabilityByPrefix(request, headers, activities, cleanupErrors) try { await restoreDictionary(request, headers, dictionarySnapshot.items) const restored = await getDictionary(request, headers) expect(dictionarySummary(restored.items)).toEqual(dictionarySummary(dictionarySnapshot.items)) dictionaryRestored = true activities.push({ module: 'ASR词典', action: '精确恢复运行前语义快照', result: 'CLEANED', detail: `恢复 ${restored.items.length} 条词典数据` }) } catch (error) { const message = `ASR 词典恢复异常:${error instanceof Error ? error.message : String(error)}` cleanupErrors.push(message) activities.push({ module: 'ASR词典', action: '恢复原快照', result: 'CLEANUP_FAILED', detail: message }) } await attachLifecycleReport(testInfo, 'ASR能力与词典UI', activities, cleanupErrors) } if (primaryError) throw primaryError expect(dictionaryRestored, 'ASR 词典必须恢复为运行前快照').toBeTruthy() expect(cleanupErrors, 'ASR 测试能力与词典必须清理恢复').toEqual([]) })