|
- import fs from 'node:fs'
- import path from 'node:path'
-
- import type { APIRequestContext, APIResponse, TestInfo } from '@playwright/test'
-
- import { attachJson, expect, runId, runPrefix, test } from './fixtures'
- import { authHeaders, envelopeData } from './helpers'
-
- type JsonRecord = Record<string, unknown>
- type Headers = Record<string, string>
-
- interface VersionedRef {
- id: string
- dataVersion: number
- }
-
- interface Activity {
- module: string
- action: string
- result: 'PASS' | 'FAIL' | 'SKIP' | 'CLEANED' | 'CLEANUP_FAILED'
- httpStatus?: number
- resourceId?: string
- detail?: string
- }
-
- const asRecord = (value: unknown): JsonRecord => value && typeof value === 'object'
- ? value as JsonRecord
- : {}
-
- const asRecords = (value: unknown): JsonRecord[] => Array.isArray(value)
- ? value.map(asRecord)
- : []
-
- const versioned = (value: unknown): VersionedRef => {
- const item = asRecord(value)
- const id = String(item.id ?? '')
- const dataVersion = Number(item.dataVersion)
- if (!id || !Number.isInteger(dataVersion) || dataVersion < 0) {
- throw new Error('API 响应缺少有效的资源 id 或 dataVersion')
- }
- return { id, dataVersion }
- }
-
- const assertStatus = (response: APIResponse, expected: number | number[], label: string) => {
- const values = Array.isArray(expected) ? expected : [expected]
- expect(values, `${label}:HTTP ${response.status()}`).toContain(response.status())
- }
-
- async function responseData<T = JsonRecord>(
- response: APIResponse,
- expected: number | number[],
- label: string,
- ): Promise<T> {
- assertStatus(response, expected, label)
- let body: unknown
- try {
- body = await response.json()
- } catch {
- throw new Error(`${label}:HTTP ${response.status()} 响应不是合法 JSON`)
- }
- return envelopeData<T>(body)
- }
-
- const addPass = (
- activities: Activity[],
- module: string,
- action: string,
- response: APIResponse,
- resourceId?: string,
- detail?: string,
- ) => activities.push({
- module,
- action,
- result: 'PASS',
- httpStatus: response.status(),
- resourceId,
- detail,
- })
-
- async function attachCaseReport(
- testInfo: TestInfo,
- title: string,
- activities: Activity[],
- cleanupErrors: string[],
- ) {
- await attachJson(testInfo, `${title}-生命周期与清理`, {
- runId,
- runPrefix,
- title,
- activities,
- cleanupComplete: cleanupErrors.length === 0,
- cleanupErrors,
- security: '报告未记录登录密码、访问令牌、智能体一次性 API Key 或请求头。',
- })
- }
-
- async function cleanupVersioned(
- request: APIRequestContext,
- headers: Headers,
- reference: VersionedRef | null,
- module: string,
- detailPath: (id: string) => string,
- deletePath: (id: string) => string,
- activities: Activity[],
- cleanupErrors: string[],
- ) {
- if (!reference) return
- try {
- for (let attempt = 0; attempt < 2; attempt += 1) {
- const detail = await request.get(detailPath(reference.id), { headers })
- if (detail.status() === 404) {
- activities.push({ module, action: '失败兜底清理', result: 'CLEANED', resourceId: reference.id, detail: '资源已不存在' })
- return
- }
- if (detail.status() !== 200) {
- const message = `${module} 清理读取失败:HTTP ${detail.status()}`
- cleanupErrors.push(message)
- activities.push({ module, action: '失败兜底清理', result: 'CLEANUP_FAILED', httpStatus: detail.status(), resourceId: reference.id, detail: message })
- return
- }
- const current = versioned(envelopeData(await detail.json()))
- const removed = await request.delete(deletePath(reference.id), {
- headers,
- params: { dataVersion: String(current.dataVersion) },
- })
- if (removed.status() === 200 || removed.status() === 404) {
- const absent = await request.get(detailPath(reference.id), { headers })
- if (absent.status() === 404) {
- activities.push({ module, action: '失败兜底清理', result: 'CLEANED', httpStatus: removed.status(), resourceId: reference.id })
- return
- }
- }
- if (removed.status() !== 409 || attempt === 1) {
- const message = `${module} 清理删除失败:HTTP ${removed.status()}`
- cleanupErrors.push(message)
- activities.push({ module, action: '失败兜底清理', result: 'CLEANUP_FAILED', httpStatus: removed.status(), resourceId: reference.id, detail: message })
- return
- }
- }
- } catch (error) {
- const message = `${module} 清理异常:${error instanceof Error ? error.message : String(error)}`
- cleanupErrors.push(message)
- activities.push({ module, action: '失败兜底清理', result: 'CLEANUP_FAILED', resourceId: reference.id, detail: message })
- }
- }
-
- async function cleanupKnowledgeBase(
- request: APIRequestContext,
- headers: Headers,
- reference: VersionedRef | null,
- activities: Activity[],
- cleanupErrors: string[],
- ) {
- if (!reference) return
- try {
- for (let attempt = 0; attempt < 2; attempt += 1) {
- const listed = await request.get('/api/v1/knowledge/bases', {
- headers,
- params: { page: '1', pageSize: '200', keyword: runPrefix },
- })
- if (listed.status() !== 200) {
- const message = `知识库清理读取失败:HTTP ${listed.status()}`
- cleanupErrors.push(message)
- activities.push({ module: '知识库', action: '失败兜底清理', result: 'CLEANUP_FAILED', httpStatus: listed.status(), resourceId: reference.id, detail: message })
- return
- }
- const page = asRecord(envelopeData(await listed.json()))
- const current = asRecords(page.items).find((item) => String(item.id ?? '') === reference.id)
- if (!current) {
- activities.push({ module: '知识库', action: '失败兜底清理', result: 'CLEANED', resourceId: reference.id, detail: '资源已不存在' })
- return
- }
- const removed = await request.delete(`/api/v1/knowledge/bases/${reference.id}`, {
- headers,
- params: { dataVersion: String(Number(current.dataVersion)) },
- })
- if (removed.status() === 200 || removed.status() === 404) {
- activities.push({ module: '知识库', action: '失败兜底清理', result: 'CLEANED', httpStatus: removed.status(), resourceId: reference.id })
- return
- }
- if (removed.status() !== 409 || attempt === 1) {
- const message = `知识库清理删除失败:HTTP ${removed.status()}`
- cleanupErrors.push(message)
- activities.push({ module: '知识库', action: '失败兜底清理', result: 'CLEANUP_FAILED', httpStatus: removed.status(), resourceId: reference.id, detail: message })
- return
- }
- }
- } catch (error) {
- const message = `知识库清理异常:${error instanceof Error ? error.message : String(error)}`
- cleanupErrors.push(message)
- activities.push({ module: '知识库', action: '失败兜底清理', result: 'CLEANUP_FAILED', resourceId: reference.id, detail: message })
- }
- }
-
- async function waitForDocumentReady(
- request: APIRequestContext,
- headers: Headers,
- documentId: string,
- timeoutMs = 30_000,
- ) {
- const deadline = Date.now() + timeoutMs
- let latest: JsonRecord = {}
- while (Date.now() < deadline) {
- const response = await request.get(`/api/v1/knowledge/documents/${documentId}`, { headers })
- latest = await responseData<JsonRecord>(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, 300))
- }
- throw new Error(`知识资料在 ${timeoutMs}ms 内未进入 ready 状态,最后状态:${String(latest.status ?? 'unknown')}`)
- }
-
- test.describe('正式业务 API 数据生命周期', () => {
- test('能力资产:LLM 创建、读取、编辑、状态、校验与删除', async ({ request }, testInfo) => {
- const module = '能力资产'
- const activities: Activity[] = []
- const cleanupErrors: string[] = []
- const headers = await authHeaders(request)
- let resource: VersionedRef | null = null
- let primaryError: unknown
-
- try {
- const createdResponse = await request.post('/api/v1/capabilities', {
- headers,
- data: {
- capabilityType: 'LLM',
- name: `${runPrefix}-本地语言模型`,
- type: '本地模型',
- description: 'APE2E 生命周期验证,不调用外部模型。',
- reference: `${runPrefix.toLowerCase()}-local-llm`,
- embedUrl: '',
- hotWords: [],
- enabled: false,
- },
- })
- const created = await responseData<JsonRecord>(createdResponse, 201, '创建 LLM 能力')
- resource = versioned(created)
- expect(created.capabilityType).toBe('LLM')
- expect(created.enabled).toBe(false)
- addPass(activities, module, '创建禁用的本地 LLM 能力', createdResponse, resource.id)
-
- const detailResponse = await request.get(`/api/v1/capabilities/${resource.id}`, { headers })
- const detail = await responseData<JsonRecord>(detailResponse, 200, '读取 LLM 能力')
- expect(detail.name).toBe(`${runPrefix}-本地语言模型`)
- addPass(activities, module, '详情读取', detailResponse, resource.id)
-
- const listResponse = await request.get('/api/v1/capabilities', {
- headers,
- params: { capabilityType: 'LLM', keyword: runPrefix, page: '1', pageSize: '20' },
- })
- const page = asRecord(await responseData(listResponse, 200, '筛选 LLM 能力'))
- expect(asRecords(page.items).some((item) => String(item.id) === resource?.id)).toBeTruthy()
- addPass(activities, module, '列表筛选', listResponse, resource.id)
-
- const updateResponse = await request.put(`/api/v1/capabilities/${resource.id}`, {
- headers,
- data: {
- name: `${runPrefix}-本地语言模型-修订`,
- description: 'APE2E 已提交编辑。',
- dataVersion: resource.dataVersion,
- },
- })
- const updated = await responseData<JsonRecord>(updateResponse, 200, '编辑 LLM 能力')
- const staleVersion = resource.dataVersion
- resource = versioned(updated)
- expect(updated.description).toBe('APE2E 已提交编辑。')
- addPass(activities, module, '编辑并推进数据版本', updateResponse, resource.id)
-
- const staleResponse = await request.put(`/api/v1/capabilities/${resource.id}/status`, {
- headers,
- data: { enabled: true, dataVersion: staleVersion },
- })
- assertStatus(staleResponse, 409, '能力资产过期版本必须冲突')
- addPass(activities, module, 'CAS 过期写入拦截', staleResponse, resource.id)
-
- const enabledResponse = await request.put(`/api/v1/capabilities/${resource.id}/status`, {
- headers,
- data: { enabled: true, dataVersion: resource.dataVersion },
- })
- const enabled = await responseData<JsonRecord>(enabledResponse, 200, '启用 LLM 能力')
- resource = versioned(enabled)
- expect(enabled.enabled).toBe(true)
- addPass(activities, module, '启用状态提交', enabledResponse, resource.id)
-
- const verifyResponse = await request.post(`/api/v1/capabilities/${resource.id}/verify`, { headers })
- const verified = await responseData<JsonRecord>(verifyResponse, 200, '校验本地 LLM 配置')
- expect(verified.valid).toBe(true)
- addPass(activities, module, '本地配置校验(无外部请求)', verifyResponse, resource.id)
-
- const refreshedResponse = await request.get(`/api/v1/capabilities/${resource.id}`, { headers })
- const refreshed = await responseData<JsonRecord>(refreshedResponse, 200, '刷新能力版本')
- resource = versioned(refreshed)
- expect(refreshed.validationStatus).toBe('AVAILABLE')
-
- const id = resource.id
- const deleteResponse = await request.delete(`/api/v1/capabilities/${id}`, {
- headers,
- params: { dataVersion: String(resource.dataVersion) },
- })
- await responseData(deleteResponse, 200, '删除 LLM 能力')
- resource = null
- addPass(activities, module, '版本化删除', deleteResponse, id)
- const absent = await request.get(`/api/v1/capabilities/${id}`, { headers })
- assertStatus(absent, 404, '删除后的能力不可读取')
- addPass(activities, module, '删除后不可读取', absent, id)
- } catch (error) {
- primaryError = error
- } finally {
- await cleanupVersioned(
- request,
- headers,
- resource,
- module,
- (id) => `/api/v1/capabilities/${id}`,
- (id) => `/api/v1/capabilities/${id}`,
- activities,
- cleanupErrors,
- )
- await attachCaseReport(testInfo, module, activities, cleanupErrors)
- }
-
- if (primaryError) throw primaryError
- expect(cleanupErrors, '能力资产测试数据必须清理干净').toEqual([])
- })
-
- test('知识库:知识库与 TXT 文档写入、检索问答、编辑和删除', async ({ request }, testInfo) => {
- test.setTimeout(120_000)
- const module = '知识库'
- const activities: Activity[] = []
- const cleanupErrors: string[] = []
- const headers = await authHeaders(request)
- let knowledgeBase: VersionedRef | null = null
- let document: VersionedRef | null = null
- let primaryError: unknown
- const marker = `${runPrefix}-液压校验标记`
-
- try {
- const baseResponse = await request.post('/api/v1/knowledge/bases', {
- headers,
- data: {
- name: `${runPrefix}-设备检修知识库`,
- description: 'APE2E 真实知识库数据提交。',
- category: '端到端验证',
- tags: ['APE2E', '设备检修'],
- },
- })
- const base = await responseData<JsonRecord>(baseResponse, 201, '创建知识库')
- knowledgeBase = versioned(base)
- expect(base.scope).toBe('personal')
- addPass(activities, module, '创建知识库', baseResponse, knowledgeBase.id)
-
- const baseListResponse = await request.get('/api/v1/knowledge/bases', {
- headers,
- params: { keyword: runPrefix, page: '1', pageSize: '20' },
- })
- const basePage = asRecord(await responseData(baseListResponse, 200, '筛选知识库'))
- expect(asRecords(basePage.items).some((item) => String(item.id) === knowledgeBase?.id)).toBeTruthy()
- addPass(activities, module, '知识库列表筛选', baseListResponse, knowledgeBase.id)
-
- const baseUpdateResponse = await request.put(`/api/v1/knowledge/bases/${knowledgeBase.id}`, {
- headers,
- data: {
- name: `${runPrefix}-设备检修知识库-修订`,
- description: 'APE2E 已提交知识库编辑。',
- category: '端到端验证',
- tags: ['APE2E', '知识问答'],
- enabled: true,
- dataVersion: knowledgeBase.dataVersion,
- },
- })
- const baseUpdated = await responseData<JsonRecord>(baseUpdateResponse, 200, '编辑知识库')
- knowledgeBase = versioned(baseUpdated)
- addPass(activities, module, '编辑知识库', baseUpdateResponse, knowledgeBase.id)
-
- const initialContent = `设备检修知识:${marker}。拆卸液压管路前必须停机、断电并完成卸压。`
- const uploadResponse = await request.post('/api/v1/knowledge/documents', {
- headers,
- multipart: {
- knowledgeBaseId: knowledgeBase.id,
- file: {
- name: `${runPrefix}-检修规程.txt`,
- mimeType: 'text/plain',
- buffer: Buffer.from(initialContent, 'utf8'),
- },
- },
- })
- const uploaded = asRecord(await responseData(uploadResponse, 201, '上传 TXT 知识资料'))
- const uploadedDocument = asRecord(uploaded.document)
- document = versioned(uploadedDocument)
- expect(String(asRecord(uploaded.job).id ?? '')).not.toBe('')
- addPass(activities, module, '上传 TXT 并创建索引任务', uploadResponse, document.id)
-
- let current = await waitForDocumentReady(request, headers, document.id)
- document = versioned(current)
- expect(Number(current.chunkCount)).toBeGreaterThanOrEqual(1)
- activities.push({ module, action: '轮询等待 TXT 索引 ready', result: 'PASS', resourceId: document.id })
-
- const documentsResponse = await request.get('/api/v1/knowledge/documents', {
- headers,
- params: { knowledgeBaseId: knowledgeBase.id, page: '1', pageSize: '20' },
- })
- const documentsPage = asRecord(await responseData(documentsResponse, 200, '按知识库读取资料'))
- expect(asRecords(documentsPage.items).some((item) => String(item.id) === document?.id)).toBeTruthy()
- addPass(activities, module, '资料列表筛选', documentsResponse, document.id)
-
- const downloadResponse = await request.get(`/api/v1/knowledge/documents/${document.id}/file`, { headers })
- assertStatus(downloadResponse, 200, '下载 TXT 原始资料')
- expect((await downloadResponse.body()).toString('utf8')).toContain(marker)
- addPass(activities, module, '下载并核对 TXT 内容', downloadResponse, document.id)
-
- const searchResponse = await request.post('/api/v1/knowledge/search', {
- headers,
- data: { query: marker, knowledgeBaseId: knowledgeBase.id, documentIds: [document.id], topK: 5 },
- })
- const search = asRecord(await responseData(searchResponse, 200, '检索 TXT 知识'))
- expect(asRecords(search.results).some((item) => String(item.documentId) === document?.id)).toBeTruthy()
- addPass(activities, module, '本地知识检索', searchResponse, document.id)
-
- const policyResponse = await request.get('/api/v1/knowledge/provider-policy', { headers })
- const policy = await responseData<JsonRecord>(policyResponse, 200, '读取知识问答提供方策略')
- const mode = String(policy.mode ?? 'local').toLowerCase()
- if (policy.enabled === false || mode === 'local') {
- const answerResponse = await request.post('/api/v1/knowledge/answer', {
- headers,
- data: { query: marker, knowledgeBaseId: knowledgeBase.id, documentIds: [document.id], topK: 5 },
- })
- const answer = asRecord(await responseData(answerResponse, 200, '执行本地知识问答'))
- expect(String(answer.answer ?? '')).toContain(marker)
- expect(asRecords(answer.results).some((item) => String(item.documentId) === document?.id)).toBeTruthy()
- addPass(activities, module, '知识问答与引用核对', answerResponse, document.id, `providerMode=${mode}`)
- } else {
- activities.push({
- module,
- action: '第三方/混合知识问答',
- result: 'SKIP',
- detail: `能力探测到 providerMode=${mode};为避免真实调用外部上游,仅完成本地 search 契约验证。`,
- })
- }
-
- const updatedContent = `修订后的设备检修知识:${marker}-修订。复装后必须进行低压泄漏检测。`
- const documentUpdateResponse = await request.put(`/api/v1/knowledge/documents/${document.id}`, {
- headers,
- data: {
- name: `${runPrefix}-检修规程-修订.txt`,
- enabled: true,
- content: updatedContent,
- dataVersion: document.dataVersion,
- },
- })
- const documentUpdated = await responseData<JsonRecord>(documentUpdateResponse, 200, '编辑知识资料正文')
- document = versioned(documentUpdated)
- addPass(activities, module, '编辑 TXT 名称与正文', documentUpdateResponse, document.id)
- current = await waitForDocumentReady(request, headers, document.id)
- document = versioned(current)
- expect(String(current.content ?? '')).toContain(`${marker}-修订`)
-
- const documentId = document.id
- const documentDeleteResponse = await request.delete(`/api/v1/knowledge/documents/${documentId}`, {
- headers,
- params: { dataVersion: String(document.dataVersion) },
- })
- await responseData(documentDeleteResponse, 200, '删除知识资料')
- document = null
- addPass(activities, module, '删除 TXT 资料', documentDeleteResponse, documentId)
- const documentAbsent = await request.get(`/api/v1/knowledge/documents/${documentId}`, { headers })
- assertStatus(documentAbsent, 404, '删除后的知识资料不可读取')
-
- const baseId = knowledgeBase.id
- const baseDeleteResponse = await request.delete(`/api/v1/knowledge/bases/${baseId}`, {
- headers,
- params: { dataVersion: String(knowledgeBase.dataVersion) },
- })
- await responseData(baseDeleteResponse, 200, '删除知识库')
- knowledgeBase = null
- addPass(activities, module, '删除空知识库', baseDeleteResponse, baseId)
- const afterDeleteResponse = await request.get('/api/v1/knowledge/bases', {
- headers,
- params: { keyword: runPrefix, page: '1', pageSize: '200' },
- })
- const afterDeletePage = asRecord(await responseData(afterDeleteResponse, 200, '复查知识库删除结果'))
- expect(asRecords(afterDeletePage.items).some((item) => String(item.id) === baseId)).toBe(false)
- addPass(activities, module, '删除后列表不可见', afterDeleteResponse, baseId)
- } catch (error) {
- primaryError = error
- } finally {
- await cleanupVersioned(
- request,
- headers,
- document,
- '知识资料',
- (id) => `/api/v1/knowledge/documents/${id}`,
- (id) => `/api/v1/knowledge/documents/${id}`,
- activities,
- cleanupErrors,
- )
- await cleanupKnowledgeBase(request, headers, knowledgeBase, activities, cleanupErrors)
- await attachCaseReport(testInfo, module, activities, cleanupErrors)
- }
-
- if (primaryError) throw primaryError
- expect(cleanupErrors, '知识库测试数据必须清理干净').toEqual([])
- })
-
- test('智能体:项目与 Draft 智能体的创建、编辑、成员关系和删除', async ({ request }, testInfo) => {
- const module = '智能体项目'
- const activities: Activity[] = []
- const cleanupErrors: string[] = []
- const headers = await authHeaders(request)
- let project: VersionedRef | null = null
- let agent: VersionedRef | null = null
- let primaryError: unknown
-
- try {
- const projectResponse = await request.post('/api/v1/agent-projects', {
- headers,
- data: {
- name: `${runPrefix}-实训智能体项目`,
- code: `${runPrefix.toLowerCase()}-agent-project`,
- description: 'APE2E 项目创建数据。',
- unit: '端到端测试单元',
- status: 'enabled',
- },
- })
- const projectCreated = await responseData<JsonRecord>(projectResponse, 201, '创建智能体项目')
- project = versioned(projectCreated)
- addPass(activities, module, '创建项目', projectResponse, project.id)
-
- const projectListResponse = await request.get('/api/v1/agent-projects', {
- headers,
- params: { keyword: runPrefix, page: '1', pageSize: '20' },
- })
- const projectPage = asRecord(await responseData(projectListResponse, 200, '筛选智能体项目'))
- expect(asRecords(projectPage.items).some((item) => String(item.id) === project?.id)).toBeTruthy()
- addPass(activities, module, '项目列表筛选', projectListResponse, project.id)
-
- const projectUpdateResponse = await request.put(`/api/v1/agent-projects/${project.id}`, {
- headers,
- data: {
- name: `${runPrefix}-实训智能体项目-修订`,
- code: `${runPrefix.toLowerCase()}-agent-project`,
- description: 'APE2E 已提交项目编辑。',
- unit: '端到端测试单元',
- status: 'enabled',
- dataVersion: project.dataVersion,
- },
- })
- project = versioned(await responseData(projectUpdateResponse, 200, '编辑智能体项目'))
- addPass(activities, module, '编辑项目', projectUpdateResponse, project.id)
-
- const disabledResponse = await request.put(`/api/v1/agent-projects/${project.id}/status`, {
- headers,
- data: { status: 'disabled', dataVersion: project.dataVersion },
- })
- const disabled = await responseData<JsonRecord>(disabledResponse, 200, '停用智能体项目')
- project = versioned(disabled)
- expect(disabled.status).toBe('disabled')
- addPass(activities, module, '停用项目', disabledResponse, project.id)
-
- const enabledResponse = await request.put(`/api/v1/agent-projects/${project.id}/status`, {
- headers,
- data: { status: 'enabled', dataVersion: project.dataVersion },
- })
- project = versioned(await responseData(enabledResponse, 200, '重新启用智能体项目'))
- addPass(activities, module, '重新启用项目', enabledResponse, project.id)
-
- const agentResponse = await request.post('/api/v1/realtime-agents', {
- headers,
- data: {
- name: `${runPrefix}-Draft数字教员`,
- projectId: project.id,
- description: 'APE2E Draft 智能体。',
- interactionModes: ['text'],
- expiresAt: '2030-08-15T12:00:00+08:00',
- },
- })
- const agentCreated = await responseData<JsonRecord>(agentResponse, 201, '创建 Draft 智能体')
- agent = versioned(agentCreated)
- expect(agentCreated.status).toBe('draft')
- addPass(activities, module, '创建 Draft 智能体(一次性 Key 不写报告)', agentResponse, agent.id)
-
- const agentDetailResponse = await request.get(`/api/v1/realtime-agents/${agent.id}`, { headers })
- const agentDetail = await responseData<JsonRecord>(agentDetailResponse, 200, '读取 Draft 智能体')
- expect(agentDetail.projectId).toBe(project.id)
- expect(agentDetail.apiKey).toBeUndefined()
- expect(agentDetail.issuedApiKey).toBeUndefined()
- addPass(activities, module, '详情读取且不重复泄露一次性 Key', agentDetailResponse, agent.id)
-
- const agentUpdateResponse = await request.put(`/api/v1/realtime-agents/${agent.id}/sections/core`, {
- headers,
- data: {
- description: 'APE2E 已提交 Draft 核心配置编辑。',
- systemPrompt: '你是设备检修数字教员,仅依据正式安全规程回答。',
- welcomeMessage: '欢迎进行设备检修实训。',
- fallbackMessage: '当前资料不足,请联系教员。',
- interactionModes: ['text'],
- dataVersion: agent.dataVersion,
- },
- })
- const agentUpdated = await responseData<JsonRecord>(agentUpdateResponse, 200, '编辑 Draft 智能体')
- agent = versioned(agentUpdated)
- expect(agentUpdated.description).toContain('Draft 核心配置编辑')
- addPass(activities, module, '编辑 Draft 核心配置', agentUpdateResponse, agent.id)
-
- const membersResponse = await request.put(`/api/v1/agent-projects/${project.id}/members`, {
- headers,
- data: { agentIds: [agent.id], dataVersion: project.dataVersion },
- })
- const members = await responseData<JsonRecord>(membersResponse, 200, '提交项目成员')
- expect(Number(members.memberCount)).toBe(1)
- const projectAfterMembersResponse = await request.get(`/api/v1/agent-projects/${project.id}`, { headers })
- project = versioned(await responseData(projectAfterMembersResponse, 200, '刷新项目成员版本'))
- addPass(activities, module, '项目成员关系提交', membersResponse, project.id)
-
- const publishResponse = await request.post(`/api/v1/realtime-agents/${agent.id}/publish`, {
- headers,
- data: { dataVersion: agent.dataVersion },
- })
- assertStatus(publishResponse, 409, '配置不完整的 Draft 智能体不得发布')
- addPass(activities, module, 'Draft 发布前完整性校验拦截', publishResponse, agent.id)
-
- const agentId = agent.id
- const agentDeleteResponse = await request.delete(`/api/v1/realtime-agents/${agentId}`, {
- headers,
- params: { dataVersion: String(agent.dataVersion) },
- })
- await responseData(agentDeleteResponse, 200, '删除 Draft 智能体')
- agent = null
- addPass(activities, module, '删除 Draft 智能体', agentDeleteResponse, agentId)
- const agentAbsent = await request.get(`/api/v1/realtime-agents/${agentId}`, { headers })
- assertStatus(agentAbsent, 404, '删除后的 Draft 智能体不可读取')
-
- const refreshedProjectResponse = await request.get(`/api/v1/agent-projects/${project.id}`, { headers })
- project = versioned(await responseData(refreshedProjectResponse, 200, '刷新项目版本'))
- const projectId = project.id
- const projectDeleteResponse = await request.delete(`/api/v1/agent-projects/${projectId}`, {
- headers,
- params: { dataVersion: String(project.dataVersion) },
- })
- await responseData(projectDeleteResponse, 200, '删除智能体项目')
- project = null
- addPass(activities, module, '删除空项目', projectDeleteResponse, projectId)
- const projectAbsent = await request.get(`/api/v1/agent-projects/${projectId}`, { headers })
- assertStatus(projectAbsent, 404, '删除后的项目不可读取')
- } catch (error) {
- primaryError = error
- } finally {
- await cleanupVersioned(
- request,
- headers,
- agent,
- 'Draft智能体',
- (id) => `/api/v1/realtime-agents/${id}`,
- (id) => `/api/v1/realtime-agents/${id}`,
- activities,
- cleanupErrors,
- )
- await cleanupVersioned(
- request,
- headers,
- project,
- module,
- (id) => `/api/v1/agent-projects/${id}`,
- (id) => `/api/v1/agent-projects/${id}`,
- activities,
- cleanupErrors,
- )
- await attachCaseReport(testInfo, module, activities, cleanupErrors)
- }
-
- if (primaryError) throw primaryError
- expect(cleanupErrors, '智能体与项目测试数据必须清理干净').toEqual([])
- })
-
- test('运营配置:敏感词、唤醒词、热词和工具的完整 CRUD', async ({ request }, testInfo) => {
- const module = '运营配置'
- const activities: Activity[] = []
- const cleanupErrors: string[] = []
- const headers = await authHeaders(request)
- let sensitive: VersionedRef | null = null
- let wake: VersionedRef | null = null
- let hot: VersionedRef | null = null
- let tool: VersionedRef | null = null
- let primaryError: unknown
-
- try {
- const sensitiveResponse = await request.post('/api/v1/sensitive-words', {
- headers,
- data: {
- word: `${runPrefix}-违规操作`,
- category: 'APE2E安全',
- matchMode: '包含匹配',
- level: '高',
- replacement: '请遵守设备检修安全规程。',
- remark: 'APE2E 创建',
- enabled: true,
- },
- })
- const sensitiveCreated = await responseData<JsonRecord>(sensitiveResponse, 201, '创建敏感词')
- sensitive = versioned(sensitiveCreated)
- addPass(activities, '敏感词', '创建', sensitiveResponse, sensitive.id)
-
- const sensitiveListResponse = await request.get('/api/v1/sensitive-words', {
- headers,
- params: { keyword: runPrefix, page: '1', pageSize: '20' },
- })
- const sensitivePage = asRecord(await responseData(sensitiveListResponse, 200, '筛选敏感词'))
- expect(asRecords(sensitivePage.items).some((item) => String(item.id) === sensitive?.id)).toBeTruthy()
- addPass(activities, '敏感词', '列表筛选', sensitiveListResponse, sensitive.id)
-
- const sensitiveUpdateResponse = await request.put(`/api/v1/sensitive-words/${sensitive.id}`, {
- headers,
- data: { remark: 'APE2E 已编辑', replacement: '请停止违规操作并联系教员。', dataVersion: sensitive.dataVersion },
- })
- sensitive = versioned(await responseData(sensitiveUpdateResponse, 200, '编辑敏感词'))
- addPass(activities, '敏感词', '编辑', sensitiveUpdateResponse, sensitive.id)
-
- const sensitiveStatusResponse = await request.put(`/api/v1/sensitive-words/${sensitive.id}/status`, {
- headers,
- data: { enabled: false, dataVersion: sensitive.dataVersion },
- })
- const sensitiveDisabled = await responseData<JsonRecord>(sensitiveStatusResponse, 200, '停用敏感词')
- sensitive = versioned(sensitiveDisabled)
- expect(sensitiveDisabled.enabled).toBe(false)
- addPass(activities, '敏感词', '停用', sensitiveStatusResponse, sensitive.id)
-
- const sensitiveId = sensitive.id
- const sensitiveDeleteResponse = await request.delete(`/api/v1/sensitive-words/${sensitiveId}`, {
- headers,
- params: { dataVersion: String(sensitive.dataVersion) },
- })
- await responseData(sensitiveDeleteResponse, 200, '删除敏感词')
- sensitive = null
- addPass(activities, '敏感词', '删除', sensitiveDeleteResponse, sensitiveId)
- const sensitiveAbsent = await request.get(`/api/v1/sensitive-words/${sensitiveId}`, { headers })
- assertStatus(sensitiveAbsent, 404, '删除后的敏感词不可读取')
-
- const invalidWakeResponse = await request.post('/api/v1/wake-words', {
- headers,
- data: {
- name: `${runPrefix}-非法时段探针`,
- phrase: `${runPrefix}非法时段探针`,
- reply: '该请求不应写入数据库。',
- sensitivity: 50,
- activePeriod: '24:00-25:00',
- enabled: true,
- },
- })
- assertStatus(invalidWakeResponse, 400, '非法唤醒时段必须在写入前被拒绝')
- activities.push({
- module: '唤醒词',
- action: '非法时段写入前校验',
- result: 'PASS',
- httpStatus: invalidWakeResponse.status(),
- detail: '24:00-25:00 被拒绝,未创建不可清理的数据。',
- })
-
- const wakeResponse = await request.post('/api/v1/wake-words', {
- headers,
- data: {
- name: `${runPrefix}-检修唤醒`,
- phrase: `${runPrefix}你好教员`,
- reply: '您好,请描述设备现象。',
- sensitivity: 78,
- activePeriod: '06:00-22:00',
- enabled: true,
- },
- })
- const wakeCreated = await responseData<JsonRecord>(wakeResponse, 201, '创建唤醒词')
- wake = versioned(wakeCreated)
- expect(wakeCreated.activePeriod).toBe('06:00-22:00')
- addPass(activities, '唤醒词', '创建自定义时段', wakeResponse, wake.id, 'activePeriod=06:00-22:00')
-
- const wakeDetailResponse = await request.get(`/api/v1/wake-words/${wake.id}`, { headers })
- const wakeDetail = await responseData<JsonRecord>(wakeDetailResponse, 200, '读取自定义时段唤醒词')
- expect(wakeDetail.activePeriod).toBe('06:00-22:00')
- addPass(activities, '唤醒词', '读取自定义时段', wakeDetailResponse, wake.id)
-
- const wakeListResponse = await request.get('/api/v1/wake-words', {
- headers,
- params: { keyword: `${runPrefix}你好教员`, page: '1', pageSize: '20' },
- })
- const wakeList = asRecord(await responseData(wakeListResponse, 200, '检索自定义时段唤醒词'))
- expect(asRecords(wakeList.items).some((item) => String(item.id) === wake?.id && item.activePeriod === '06:00-22:00')).toBeTruthy()
- addPass(activities, '唤醒词', '列表检索自定义时段', wakeListResponse, wake.id)
-
- const wakeTestResponse = await request.post(`/api/v1/wake-words/${wake.id}/test`, { headers })
- if (wakeTestResponse.status() === 200) {
- const wakeTest = await responseData<JsonRecord>(wakeTestResponse, 200, '测试唤醒词匹配')
- expect(wakeTest.matched).toBe(true)
- addPass(activities, '唤醒词', '本地匹配测试', wakeTestResponse, wake.id)
- } else {
- activities.push({
- module: '唤醒词',
- action: '本地匹配测试',
- result: 'FAIL',
- httpStatus: wakeTestResponse.status(),
- resourceId: wake.id,
- detail: '已启用且刚创建的唤醒词测试接口返回非 200;继续执行后续 CRUD 并在报告中保留该真实缺陷。',
- })
- expect.soft(wakeTestResponse.status(), '已启用全天唤醒词的本地匹配测试接口应返回 HTTP 200').toBe(200)
- }
-
- const wakeUpdateResponse = await request.put(`/api/v1/wake-words/${wake.id}`, {
- headers,
- data: { reply: 'APE2E 已编辑唤醒回复。', sensitivity: 82, activePeriod: '06:30-21:30', dataVersion: wake.dataVersion },
- })
- if (wakeUpdateResponse.status() === 200) {
- const wakeUpdated = await responseData<JsonRecord>(wakeUpdateResponse, 200, '编辑唤醒词')
- expect(wakeUpdated.activePeriod).toBe('06:30-21:30')
- wake = versioned(wakeUpdated)
- addPass(activities, '唤醒词', '编辑并更新自定义时段', wakeUpdateResponse, wake.id, 'activePeriod=06:30-21:30')
- } else {
- activities.push({ module: '唤醒词', action: '编辑', result: 'FAIL', httpStatus: wakeUpdateResponse.status(), resourceId: wake.id, detail: '真实编辑请求失败;继续验证其他模块。' })
- expect.soft(wakeUpdateResponse.status(), '唤醒词编辑接口应返回 HTTP 200').toBe(200)
- }
-
- const wakeStatusResponse = await request.put(`/api/v1/wake-words/${wake.id}/status`, {
- headers,
- data: { enabled: false, dataVersion: wake.dataVersion },
- })
- if (wakeStatusResponse.status() === 200) {
- const wakeDisabled = await responseData<JsonRecord>(wakeStatusResponse, 200, '停用唤醒词')
- expect(wakeDisabled.activePeriod).toBe('06:30-21:30')
- wake = versioned(wakeDisabled)
- addPass(activities, '唤醒词', '停用', wakeStatusResponse, wake.id)
- } else {
- activities.push({ module: '唤醒词', action: '停用', result: 'FAIL', httpStatus: wakeStatusResponse.status(), resourceId: wake.id, detail: '真实状态提交失败;继续验证其他模块。' })
- expect.soft(wakeStatusResponse.status(), '唤醒词状态接口应返回 HTTP 200').toBe(200)
- }
-
- const wakeId = wake.id
- const wakeDeleteResponse = await request.delete(`/api/v1/wake-words/${wakeId}`, {
- headers,
- params: { dataVersion: String(wake.dataVersion) },
- })
- if (wakeDeleteResponse.status() === 200) {
- await responseData(wakeDeleteResponse, 200, '删除唤醒词')
- wake = null
- addPass(activities, '唤醒词', '删除', wakeDeleteResponse, wakeId)
- const wakeAbsent = await request.get(`/api/v1/wake-words/${wakeId}`, { headers })
- assertStatus(wakeAbsent, 404, '删除后的唤醒词不可读取')
- } else {
- activities.push({ module: '唤醒词', action: '删除', result: 'FAIL', httpStatus: wakeDeleteResponse.status(), resourceId: wake.id, detail: 'API 删除失败,finally 将再次按当前版本尝试兜底清理。' })
- expect.soft(wakeDeleteResponse.status(), '唤醒词删除接口应返回 HTTP 200').toBe(200)
- }
-
- const hotResponse = await request.post('/api/v1/hot-words', {
- headers,
- data: {
- term: `${runPrefix}-液压泵`,
- aliases: [`${runPrefix}-液压蹦`, `${runPrefix}-液压蹦`, `${runPrefix}-液压崩`],
- category: 'APE2E液压系统',
- description: '端到端热词。',
- enabled: true,
- },
- })
- const hotCreated = await responseData<JsonRecord>(hotResponse, 201, '创建热词')
- hot = versioned(hotCreated)
- expect(Array.isArray(hotCreated.aliases) ? hotCreated.aliases.length : 0).toBe(2)
- addPass(activities, '热词', '创建并去重别名', hotResponse, hot.id)
-
- const hotListResponse = await request.get('/api/v1/hot-words', {
- headers,
- params: { keyword: runPrefix, page: '1', pageSize: '20' },
- })
- const hotPage = asRecord(await responseData(hotListResponse, 200, '筛选热词'))
- expect(asRecords(hotPage.items).some((item) => String(item.id) === hot?.id)).toBeTruthy()
- addPass(activities, '热词', '列表筛选', hotListResponse, hot.id)
-
- const hotUpdateResponse = await request.put(`/api/v1/hot-words/${hot.id}`, {
- headers,
- data: { aliases: [`${runPrefix}-液压棒`], description: 'APE2E 已编辑热词。', dataVersion: hot.dataVersion },
- })
- const hotUpdated = await responseData<JsonRecord>(hotUpdateResponse, 200, '编辑热词')
- hot = versioned(hotUpdated)
- expect(hotUpdated.aliases).toEqual([`${runPrefix}-液压棒`])
- addPass(activities, '热词', '编辑', hotUpdateResponse, hot.id)
-
- const hotStatusResponse = await request.put(`/api/v1/hot-words/${hot.id}/status`, {
- headers,
- data: { enabled: false, dataVersion: hot.dataVersion },
- })
- hot = versioned(await responseData(hotStatusResponse, 200, '停用热词'))
- addPass(activities, '热词', '停用', hotStatusResponse, hot.id)
-
- const hotId = hot.id
- const hotDeleteResponse = await request.delete(`/api/v1/hot-words/${hotId}`, {
- headers,
- params: { dataVersion: String(hot.dataVersion) },
- })
- await responseData(hotDeleteResponse, 200, '删除热词')
- hot = null
- addPass(activities, '热词', '删除', hotDeleteResponse, hotId)
- const hotAbsent = await request.get(`/api/v1/hot-words/${hotId}`, { headers })
- assertStatus(hotAbsent, 404, '删除后的热词不可读取')
-
- const toolResponse = await request.post('/api/v1/tools', {
- headers,
- data: {
- name: `${runPrefix}-检修工具入口`,
- domain: 'https://example.com',
- entryPath: '/ape2e/start',
- category: 'APE2E业务协同',
- description: '仅验证平台工具配置,不探测外部网络。',
- openMode: '新窗口打开',
- enabled: false,
- sortOrder: 9900,
- allowedRoleCodes: [],
- },
- })
- const toolCreated = await responseData<JsonRecord>(toolResponse, 201, '创建工具')
- tool = versioned(toolCreated)
- expect(toolCreated.connectionStatus).toBe('untested')
- addPass(activities, '工具', '创建禁用工具', toolResponse, tool.id)
-
- const toolListResponse = await request.get('/api/v1/tools', {
- headers,
- params: { keyword: runPrefix, page: '1', pageSize: '20' },
- })
- const toolPage = asRecord(await responseData(toolListResponse, 200, '筛选工具'))
- expect(asRecords(toolPage.items).some((item) => String(item.id) === tool?.id)).toBeTruthy()
- addPass(activities, '工具', '列表筛选', toolListResponse, tool.id)
-
- const toolUpdateResponse = await request.put(`/api/v1/tools/${tool.id}`, {
- headers,
- data: {
- entryPath: '/ape2e/updated',
- description: 'APE2E 已提交工具编辑。',
- dataVersion: tool.dataVersion,
- },
- })
- tool = versioned(await responseData(toolUpdateResponse, 200, '编辑工具'))
- addPass(activities, '工具', '编辑', toolUpdateResponse, tool.id)
-
- const toolStatusResponse = await request.put(`/api/v1/tools/${tool.id}/status`, {
- headers,
- data: { enabled: true, dataVersion: tool.dataVersion },
- })
- tool = versioned(await responseData(toolStatusResponse, 200, '启用工具'))
- addPass(activities, '工具', '启用', toolStatusResponse, tool.id)
-
- const launchResponse = await request.get(`/api/v1/tools/${tool.id}/launch`, { headers })
- const launch = await responseData<JsonRecord>(launchResponse, 200, '读取工具启动参数')
- expect(launch.url).toBe('https://example.com/ape2e/updated')
- addPass(activities, '工具', '生成启动地址(未发起外部请求)', launchResponse, tool.id)
- activities.push({
- module: '工具',
- action: '外部连接探测',
- result: 'SKIP',
- resourceId: tool.id,
- detail: '工具配置 CRUD 与启动地址已验证;/check 会访问外部域名,按能力边界不执行且不使用 Mock。',
- })
- const toolId = tool.id
- const toolDeleteResponse = await request.delete(`/api/v1/tools/${toolId}`, {
- headers,
- params: { dataVersion: String(tool.dataVersion) },
- })
- await responseData(toolDeleteResponse, 200, '删除工具')
- tool = null
- addPass(activities, '工具', '删除', toolDeleteResponse, toolId)
- const toolAbsent = await request.get(`/api/v1/tools/${toolId}`, { headers })
- assertStatus(toolAbsent, 404, '删除后的工具不可读取')
- } catch (error) {
- primaryError = error
- } finally {
- await cleanupVersioned(request, headers, tool, '工具', (id) => `/api/v1/tools/${id}`, (id) => `/api/v1/tools/${id}`, activities, cleanupErrors)
- await cleanupVersioned(request, headers, hot, '热词', (id) => `/api/v1/hot-words/${id}`, (id) => `/api/v1/hot-words/${id}`, activities, cleanupErrors)
- await cleanupVersioned(request, headers, wake, '唤醒词', (id) => `/api/v1/wake-words/${id}`, (id) => `/api/v1/wake-words/${id}`, activities, cleanupErrors)
- await cleanupVersioned(request, headers, sensitive, '敏感词', (id) => `/api/v1/sensitive-words/${id}`, (id) => `/api/v1/sensitive-words/${id}`, activities, cleanupErrors)
- await attachCaseReport(testInfo, module, activities, cleanupErrors)
- }
-
- if (primaryError) throw primaryError
- expect(cleanupErrors, '运营配置测试数据必须清理干净').toEqual([])
- })
-
- test('视频工程:创建、编辑、四轨时间线、CAS 与删除', async ({ request }, testInfo) => {
- test.setTimeout(240_000)
- const module = '视频工程'
- const activities: Activity[] = []
- const cleanupErrors: string[] = []
- const headers = await authHeaders(request)
- let project: VersionedRef | null = null
- let generatedAvatar: VersionedRef | null = null
- let primaryError: unknown
-
- try {
- const avatarsResponse = await request.get('/api/v1/avatars', {
- headers,
- params: { page: '1', pageSize: '100', status: 'ready' },
- })
- const avatarsPage = asRecord(await responseData(avatarsResponse, 200, '读取可用数字形象'))
- let avatarId = String(asRecords(avatarsPage.items)[0]?.id ?? '')
- if (!avatarId) {
- const fixturePath = path.resolve('../ai_person/public/avatar-previews/mechanical-male.mp4')
- expect(fs.existsSync(fixturePath), `视频工程依赖的真实形象素材不存在:${fixturePath}`).toBeTruthy()
- const createAvatarResponse = await request.post('/api/v1/avatars', {
- headers,
- multipart: {
- name: `${runPrefix}-视频工程依赖形象`,
- specialty: '视频口播教学',
- gender: '男',
- resize: 'true',
- matting: 'off',
- file: {
- name: `${runPrefix}-video-project-avatar.mp4`,
- mimeType: 'video/mp4',
- buffer: fs.readFileSync(fixturePath),
- },
- },
- timeout: 120_000,
- })
- const createdPayload = asRecord(await responseData(createAvatarResponse, 201, '创建视频工程依赖形象'))
- generatedAvatar = versioned(createdPayload.avatar)
- avatarId = generatedAvatar.id
- const jobId = String(asRecord(createdPayload.job).id ?? '')
- expect(jobId).not.toBe('')
- const deadline = Date.now() + 150_000
- let ready = false
- while (Date.now() < deadline) {
- const jobResponse = await request.get(`/api/v1/jobs/${encodeURIComponent(jobId)}`, { headers })
- const job = asRecord(await responseData(jobResponse, 200, '等待视频工程依赖形象'))
- const status = String(job.status ?? '').toLowerCase()
- if (status === 'succeeded') {
- ready = true
- break
- }
- if (['failed', 'cancelled'].includes(status)) throw new Error(`视频工程依赖形象生成失败:${status}`)
- await new Promise((resolve) => setTimeout(resolve, 750))
- }
- expect(ready, '视频工程依赖形象应在 150 秒内生成完成').toBeTruthy()
- const avatarDetailResponse = await request.get(`/api/v1/avatars/${encodeURIComponent(avatarId)}`, { headers })
- const avatarDetail = await responseData<JsonRecord>(avatarDetailResponse, 200, '刷新视频工程依赖形象')
- generatedAvatar = versioned(avatarDetail)
- expect(String(avatarDetail.status).toLowerCase()).toBe('ready')
- addPass(activities, module, '通过真实上传生成工程依赖形象', createAvatarResponse, avatarId)
- }
-
- const voicesResponse = await request.get('/api/v1/capabilities', {
- headers,
- params: { capabilityType: 'TTS', enabled: 'true', page: '1', pageSize: '100' },
- })
- const voicesPage = asRecord(await responseData(voicesResponse, 200, '读取可用 TTS 能力'))
- const voice = asRecords(voicesPage.items)[0]
- const voiceId = String(voice?.id ?? '')
- expect(voiceId, '视频工程必须选择平台已启用的正式 TTS 能力').not.toBe('')
- activities.push({ module, action: '依赖能力探测', result: 'PASS', detail: '已选择平台现有 ready 数字形象和已启用音色;报告不复制资源配置。' })
-
- const createResponse = await request.post('/api/v1/video-projects', {
- headers,
- data: {
- name: `${runPrefix}-设备检修口播工程`,
- scriptText: '停机、断电、卸压后方可拆卸液压管路。',
- avatarId,
- voiceCapabilityId: voiceId,
- voiceSpeed: 1,
- orientation: 'portrait',
- backgroundType: 'workshop',
- subtitleEnabled: true,
- subtitleFont: 'Microsoft YaHei',
- subtitleFontSize: 42,
- subtitlePosition: 'bottom',
- durationMs: 4500,
- },
- })
- const created = await responseData<JsonRecord>(createResponse, 201, '创建视频工程')
- project = versioned(created)
- expect(created.outputWidth).toBe(1080)
- expect(created.outputHeight).toBe(1920)
- addPass(activities, module, '创建竖屏工程', createResponse, project.id)
-
- const listResponse = await request.get('/api/v1/video-projects', {
- headers,
- params: { keyword: runPrefix, page: '1', pageSize: '20' },
- })
- const page = asRecord(await responseData(listResponse, 200, '筛选视频工程'))
- expect(asRecords(page.items).some((item) => String(item.id) === project?.id)).toBeTruthy()
- addPass(activities, module, '列表筛选', listResponse, project.id)
-
- const updateResponse = await request.put(`/api/v1/video-projects/${project.id}`, {
- headers,
- data: {
- name: `${runPrefix}-设备检修口播工程-修订`,
- scriptText: '停机、断电、验电、卸压后,确认压力表回零。',
- voiceSpeed: 1.1,
- dataVersion: project.dataVersion,
- },
- })
- const updated = await responseData<JsonRecord>(updateResponse, 200, '编辑视频工程')
- project = versioned(updated)
- expect(updated.voiceSpeed).toBe(1.1)
- addPass(activities, module, '编辑工程参数', updateResponse, project.id)
-
- const timeline = {
- dataVersion: project.dataVersion,
- durationMs: 4500,
- tracks: [
- {
- code: 'AVATAR',
- name: '数字人',
- order: 0,
- clips: [{ id: `${runPrefix}-avatar-clip`, type: 'AVATAR', startMs: 0, endMs: 4500 }],
- },
- { code: 'MEDIA', name: '画面素材', order: 1, clips: [] },
- {
- code: 'AUDIO',
- name: '本地配音',
- order: 2,
- clips: [{ id: `${runPrefix}-audio-clip`, type: 'AUDIO', capabilityId: voiceId, startMs: 0, endMs: 4500 }],
- },
- {
- code: 'SUBTITLE',
- name: '同步字幕',
- order: 3,
- clips: [{ id: `${runPrefix}-subtitle-clip`, type: 'SUBTITLE', text: '停机、断电、验电、卸压', startMs: 0, endMs: 4500, style: { font: 'Microsoft YaHei', size: 42 } }],
- },
- ],
- }
- const timelineResponse = await request.put(`/api/v1/video-projects/${project.id}/timeline`, { headers, data: timeline })
- const timelineSaved = await responseData<JsonRecord>(timelineResponse, 200, '保存四轨时间线')
- project = versioned(timelineSaved)
- const savedTimeline = asRecord(timelineSaved.timeline)
- expect(asRecords(savedTimeline.tracks).map((track) => track.code)).toEqual(['AVATAR', 'MEDIA', 'AUDIO', 'SUBTITLE'])
- addPass(activities, module, '保存四轨时间线', timelineResponse, project.id)
-
- const staleResponse = await request.put(`/api/v1/video-projects/${project.id}/timeline`, { headers, data: timeline })
- assertStatus(staleResponse, 409, '过期时间线版本必须冲突')
- addPass(activities, module, 'CAS 过期时间线拦截', staleResponse, project.id)
-
- const detailResponse = await request.get(`/api/v1/video-projects/${project.id}`, { headers })
- const detail = await responseData<JsonRecord>(detailResponse, 200, '读取视频工程详情')
- project = versioned(detail)
- expect(asRecords(asRecord(detail.timeline).tracks).length).toBe(4)
- addPass(activities, module, '读取持久化时间线', detailResponse, project.id)
-
- const projectId = project.id
- const deleteResponse = await request.delete(`/api/v1/video-projects/${projectId}`, {
- headers,
- params: { dataVersion: String(project.dataVersion) },
- })
- await responseData(deleteResponse, 200, '删除视频工程')
- project = null
- addPass(activities, module, '删除工程', deleteResponse, projectId)
- const absent = await request.get(`/api/v1/video-projects/${projectId}`, { headers })
- assertStatus(absent, 404, '删除后的视频工程不可读取')
- addPass(activities, module, '删除后不可读取', absent, projectId)
- } catch (error) {
- primaryError = error
- } finally {
- await cleanupVersioned(
- request,
- headers,
- project,
- module,
- (id) => `/api/v1/video-projects/${id}`,
- (id) => `/api/v1/video-projects/${id}`,
- activities,
- cleanupErrors,
- )
- await cleanupVersioned(
- request,
- headers,
- generatedAvatar,
- '视频工程依赖形象',
- (id) => `/api/v1/avatars/${id}`,
- (id) => `/api/v1/avatars/${id}`,
- activities,
- cleanupErrors,
- )
- await attachCaseReport(testInfo, module, activities, cleanupErrors)
- }
-
- if (primaryError) throw primaryError
- expect(cleanupErrors, '视频工程测试数据必须清理干净').toEqual([])
- })
- })
|