|
- import type { Page, Route } from '@playwright/test'
-
- import { attachJson, captureScreenshot, expect, test } from './fixtures'
- import { fillFormItem, formItem, tableRowByText, visibleDialog } from './helpers'
- import { observeRuntimeTransport, runtimeInstancesAbsentFromBrowserState } from './remote-terminal-runtime-contract'
-
- type JsonRecord = Record<string, unknown>
- const asRecord = (value: unknown): JsonRecord => value && typeof value === 'object' ? value as JsonRecord : {}
-
- const now = new Date().toISOString()
- const activationCode = 'controlled-once-only-activation-value'
- const controlledAppOrigin = new URL(process.env.BASE_URL ?? 'http://127.0.0.1:8003').origin
-
- const publishedAgent = {
- id: 'remote-agent-published',
- dataVersion: 7,
- name: '设备点检数字教员',
- slug: 'remote-agent-published',
- description: '受控远程终端回归使用的已发布智能体',
- status: 'published',
- expiresAt: null,
- interactionModes: ['text'],
- uiConfig: { components: [{ key: 'input', enabled: true, order: 1 }], suggestions: [] },
- }
-
- const agentBinding = {
- id: publishedAgent.id,
- dataVersion: publishedAgent.dataVersion,
- name: publishedAgent.name,
- slug: publishedAgent.slug,
- status: publishedAgent.status,
- }
-
- function terminal(
- id: string,
- name: string,
- overrides: JsonRecord = {},
- ) {
- return {
- id,
- dataVersion: 1,
- name,
- code: id.toUpperCase(),
- location: '综合实训楼 B-02',
- displayMode: 'WIDGET',
- lifecycleStatus: 'ACTIVE',
- connectionStatus: 'ONLINE',
- enabled: true,
- online: true,
- boundAgentId: publishedAgent.id,
- boundAgent: agentBinding,
- expectedVolume: 60,
- actualVolume: 60,
- allowInterrupt: true,
- allowedOrigins: ['https://portal.example.test'],
- capabilities: ['WAKE', 'PAUSE', 'RESUME', 'STOP', 'VOLUME', 'RESTART'],
- runtimeVersion: 'ai-person-web/remote-v2',
- platform: 'Controlled Chromium',
- pageUrl: `https://portal.example.test/embed/${publishedAgent.slug}`,
- origin: 'https://portal.example.test',
- deviceId: `device-${id}`,
- sessionId: '',
- sessionStatus: 'IDLE',
- playbackStatus: 'IDLE',
- visibilityState: 'visible',
- lastHeartbeatAt: now,
- createdAt: now,
- updatedAt: now,
- ...overrides,
- }
- }
-
- const initialTerminals = [
- terminal('rt-online', '电气实训区在线终端'),
- terminal('rt-pending', '液压实训区待激活终端', {
- lifecycleStatus: 'PENDING_ACTIVATION', connectionStatus: 'NEVER_CONNECTED', online: false,
- actualVolume: null, capabilities: [], runtimeVersion: '', deviceId: '', lastHeartbeatAt: '',
- }),
- terminal('rt-offline', '装配实训区离线终端', {
- connectionStatus: 'OFFLINE', online: false, lastHeartbeatAt: '2026-08-23T08:00:00+08:00',
- }),
- terminal('rt-suspended', '仓储区停用终端', {
- lifecycleStatus: 'SUSPENDED', connectionStatus: 'OFFLINE', enabled: false, online: false,
- }),
- ]
-
- const envelope = (data: unknown, status = 200) => ({
- code: status >= 400 ? status * 100 : 0,
- message: status >= 400 ? '请求失败' : '成功',
- data,
- timestamp: Date.now(),
- requestId: 'remote-terminal-v2-controlled',
- })
-
- async function fulfill(route: Route, data: unknown, status = 200, headers: Record<string, string> = {}) {
- await route.fulfill({
- status,
- headers,
- contentType: 'application/json',
- body: JSON.stringify(envelope(data, status)),
- })
- }
-
- function profile(permissions: string[]) {
- const superAdmin = permissions.includes('*')
- return {
- user: {
- id: superAdmin ? 'remote-admin' : 'remote-viewer',
- username: superAdmin ? 'remote-admin' : 'remote-viewer',
- displayName: superAdmin ? '系统管理员' : '远控审阅员',
- departmentId: 'system',
- departmentName: '数字人平台',
- enabled: true,
- mustChangePassword: false,
- version: 1,
- },
- roles: [{
- id: superAdmin ? 'admin-role' : 'viewer-role',
- code: superAdmin ? 'ADMIN' : 'REMOTE_VIEWER',
- name: superAdmin ? '系统管理员' : '远控审阅员',
- shortName: superAdmin ? '系统' : '审阅',
- enabled: true,
- builtIn: false,
- isSuperAdmin: superAdmin,
- dataScope: 'ALL',
- }],
- activeRoleId: superAdmin ? 'admin-role' : 'viewer-role',
- permissions,
- authorizationMode: 'SINGLE_ACTIVE',
- loginTime: now,
- }
- }
-
- async function installRemoteMocks(page: Page, permissions: string[]) {
- const terminals = initialTerminals.map((item) => ({ ...item }))
- const commands: JsonRecord[] = []
- const submissions: JsonRecord[] = []
-
- await page.addInitScript(() => {
- window.sessionStorage.setItem('ai-person:web:access-token:v1', 'controlled-remote-v2-token')
- })
-
- await page.route('**/api/auth/v1/**', async (route) => {
- const pathname = new URL(route.request().url()).pathname
- if (pathname.endsWith('/auth/me')) return fulfill(route, profile(permissions))
- if (pathname.endsWith('/menus/navigation')) return fulfill(route, [])
- if (pathname.endsWith('/system-config/public')) return fulfill(route, { systemName: '虚拟教员系统', shortName: '数字人平台' })
- return fulfill(route, {})
- })
-
- await page.route('**/api/v1/**', async (route) => {
- const request = route.request()
- const url = new URL(request.url())
- const pathname = url.pathname
- const method = request.method()
-
- if (pathname.endsWith('/realtime-agents')) {
- return fulfill(route, { items: [publishedAgent], page: 1, pageSize: 100, total: 1, pages: 1 })
- }
-
- if (pathname === '/api/v1/remote-terminals' && method === 'GET') {
- const keyword = (url.searchParams.get('keyword') || '').toLowerCase()
- const filtered = terminals.filter((item) => !keyword
- || [item.name, item.code, item.location].join('\n').toLowerCase().includes(keyword))
- return fulfill(route, { items: filtered, page: 1, pageSize: 20, total: filtered.length, pages: 1 })
- }
-
- if (pathname === '/api/v1/remote-terminals' && method === 'POST') {
- const payload = request.postDataJSON() as JsonRecord
- submissions.push({ method, pathname, payload })
- const created = terminal('rt-created', String(payload.name || '受控新终端'), {
- dataVersion: 1,
- code: String(payload.code || 'RTC-CREATED'),
- location: String(payload.location || ''),
- displayMode: payload.displayMode,
- expectedVolume: payload.defaultVolume,
- actualVolume: null,
- allowInterrupt: payload.allowInterrupt,
- allowedOrigins: payload.allowedOrigins,
- lifecycleStatus: 'PENDING_ACTIVATION',
- connectionStatus: 'NEVER_CONNECTED',
- online: false,
- capabilities: [],
- runtimeVersion: '',
- deviceId: '',
- lastHeartbeatAt: '',
- })
- terminals.unshift(created)
- return fulfill(route, {
- terminal: created,
- activation: {
- activationCode,
- expiresAt: new Date(Date.now() + 30 * 60_000).toISOString(),
- shownOnce: true,
- activationPath: `/embed/${publishedAgent.slug}`,
- },
- }, 201, { 'Cache-Control': 'no-store' })
- }
-
- const terminalMatch = pathname.match(/^\/api\/v1\/remote-terminals\/([^/]+)$/)
- if (terminalMatch && method === 'GET') {
- const found = terminals.find((item) => item.id === decodeURIComponent(terminalMatch[1]!))
- return found ? fulfill(route, found) : fulfill(route, {}, 404)
- }
-
- const commandListMatch = pathname.match(/^\/api\/v1\/remote-terminals\/([^/]+)\/commands$/)
- if (commandListMatch && method === 'POST') {
- const payload = request.postDataJSON() as JsonRecord
- const command = {
- id: `controlled-command-${commands.length + 1}`,
- terminalId: decodeURIComponent(commandListMatch[1]!),
- type: String(payload.type || 'wake').toUpperCase(),
- label: payload.type === 'wake' || payload.type === 'WAKE' ? '唤醒' : String(payload.type || ''),
- detail: String(payload.detail || ''),
- operator: '系统管理员',
- status: 'SUCCEEDED',
- statusLabel: '执行成功',
- deliveryAttempts: 1,
- volume: payload.volume ?? null,
- queuedAt: now,
- sentAt: now,
- acknowledgedAt: now,
- executingAt: now,
- completedAt: now,
- expiresAt: new Date(Date.now() + 90_000).toISOString(),
- durationMs: 42,
- errorCode: '',
- errorMessage: '',
- result: { expanded: true },
- }
- commands.unshift(command)
- submissions.push({
- method,
- pathname,
- payload,
- idempotencyHeaderPresent: Boolean(request.headers()['idempotency-key']),
- })
- return fulfill(route, command, 202)
- }
- if (commandListMatch && method === 'GET') {
- const terminalId = decodeURIComponent(commandListMatch[1]!)
- const items = commands.filter((item) => item.terminalId === terminalId)
- return fulfill(route, { items, page: 1, pageSize: 10, total: items.length, pages: items.length ? 1 : 0 })
- }
-
- const commandEventsMatch = pathname.match(/^\/api\/v1\/remote-commands\/([^/]+)\/events$/)
- if (commandEventsMatch && method === 'GET') {
- const commandId = decodeURIComponent(commandEventsMatch[1]!)
- return fulfill(route, { items: [
- { id: `${commandId}-queued`, status: 'QUEUED', statusLabel: '等待下发', occurredAt: now, message: '命令已排队' },
- { id: `${commandId}-ack`, status: 'ACKNOWLEDGED', statusLabel: '终端已确认', occurredAt: now, message: '运行页已确认' },
- { id: `${commandId}-done`, status: 'SUCCEEDED', statusLabel: '执行成功', occurredAt: now, message: '真实页面动作已完成' },
- ] })
- }
-
- const commandMatch = pathname.match(/^\/api\/v1\/remote-commands\/([^/]+)$/)
- if (commandMatch && method === 'GET') {
- const found = commands.find((item) => item.id === decodeURIComponent(commandMatch[1]!))
- return found ? fulfill(route, found) : fulfill(route, {}, 404)
- }
-
- return fulfill(route, {})
- })
-
- return { terminals, commands, submissions }
- }
-
- async function chooseSelect(page: Page, scope: ReturnType<Page['locator']>, label: string, option: string) {
- const item = formItem(scope, label)
- await item.locator('.el-select').click()
- const dropdown = page.locator('.el-select-dropdown:visible').last()
- await expect(dropdown).toBeVisible()
- await dropdown.locator('.el-select-dropdown__item').filter({ hasText: option }).click()
- }
-
- function collectPageFailures(page: Page) {
- const consoleErrors: string[] = []
- const pageErrors: string[] = []
- page.on('console', (message) => {
- if (message.type() === 'error') consoleErrors.push(message.text())
- })
- page.on('pageerror', (error) => pageErrors.push(error.message))
- return { consoleErrors, pageErrors }
- }
-
- test.describe('远程终端 V2 受控页面回归', () => {
- test('表格状态、创建激活、详情控制、命令事件形成完整 UI 闭环', async ({ page }, testInfo) => {
- const failures = collectPageFailures(page)
- const mock = await installRemoteMocks(page, ['*'])
-
- await page.goto('/agents/remote-control')
- await expect(page.getByRole('heading', { name: '远程控制', exact: true })).toBeVisible()
- const table = page.locator('.desktop-terminal-table .el-table')
- await expect(table).toBeVisible()
- for (const heading of ['终端', '连接与心跳', '绑定智能体', '当前会话', '音量', '操作']) {
- await expect(table.locator('.el-table__header')).toContainText(heading)
- }
- for (const label of ['待激活', '在线', '离线', '已停用']) await expect(table.getByText(label, { exact: true })).toBeVisible()
- await captureScreenshot(page, testInfo, 'remote-v2-01-table-four-real-statuses')
-
- await page.getByRole('button', { name: '新增终端', exact: true }).click()
- const editor = visibleDialog(page)
- await expect(editor).toContainText('新增远程终端')
- await fillFormItem(editor, '终端名称', '受控回归悬浮终端')
- await fillFormItem(editor, '终端编码', 'RTC-CONTROLLED-01')
- await fillFormItem(editor, '部署位置', '综合实训楼 C-01')
- await chooseSelect(page, editor, '绑定智能体', publishedAgent.name)
- await formItem(editor, '页面模式').getByText('悬浮图标', { exact: true }).click()
-
- await editor.getByRole('button', { name: '创建并生成激活链接', exact: true }).click()
- await expect(page.getByText(/至少填写一个允许嵌入来源/).last()).toBeVisible()
- await page.waitForTimeout(400)
- expect(mock.submissions, '悬浮模式空来源时不得发起创建请求').toHaveLength(0)
-
- await formItem(editor, /允许嵌入来源/).getByPlaceholder('https://portal.example.local').first().fill('https://portal.example.test')
-
- const createdResponse = page.waitForResponse((response) => response.request().method() === 'POST'
- && new URL(response.url()).pathname === '/api/v1/remote-terminals')
- await editor.getByRole('button', { name: '创建并生成激活链接', exact: true }).click()
- const response = await createdResponse
- expect(response.status()).toBe(201)
- expect(response.headers()['cache-control']).toContain('no-store')
-
- const activation = visibleDialog(page)
- await expect(activation).toContainText('接入终端 · 受控回归悬浮终端')
- await expect(activation).toContainText('仅显示一次')
- await expect(activation).toContainText('此 iframe 嵌入代码包含一次性激活码')
- await expect(activation.getByRole('button', { name: '复制 iframe 代码', exact: true })).toBeVisible()
- await expect(activation.getByRole('button', { name: '重新生成激活链接', exact: true })).toBeVisible()
- await captureScreenshot(page, testInfo, 'remote-v2-02-one-time-activation-masked', [
- activation.getByTestId('terminal-activation-embed-code'),
- ])
- await activation.getByRole('button', { name: '完成', exact: true }).click()
-
- expect(mock.submissions[0]?.payload).toMatchObject({
- name: '受控回归悬浮终端',
- code: 'RTC-CONTROLLED-01',
- location: '综合实训楼 C-01',
- boundAgentId: publishedAgent.id,
- displayMode: 'WIDGET',
- defaultVolume: 60,
- allowInterrupt: true,
- allowedOrigins: ['https://portal.example.test'],
- })
-
- const row = tableRowByText(page, '电气实训区在线终端')
- await row.getByRole('button', { name: '详情', exact: true }).click()
- const drawer = page.locator('.remote-detail-drawer:visible')
- await expect(drawer).toBeVisible()
- for (const tab of ['概览', '实时控制', '命令记录', '接入与安全']) {
- await expect(drawer.getByRole('tab', { name: tab, exact: true })).toBeVisible()
- }
- await expect(drawer).toContainText('ai-person-web/remote-v2')
- await captureScreenshot(page, testInfo, 'remote-v2-03-detail-overview')
-
- await drawer.getByRole('tab', { name: '实时控制', exact: true }).click()
- await expect(drawer.getByRole('button', { name: '唤醒运行页', exact: true })).toBeEnabled()
- await expect(drawer.getByRole('button', { name: '暂停播报', exact: true })).toBeDisabled()
- await expect(drawer.getByRole('button', { name: '继续播报', exact: true })).toBeDisabled()
- await expect(drawer.getByRole('button', { name: '结束会话', exact: true })).toBeDisabled()
- await expect(drawer.getByRole('button', { name: '重载运行页', exact: true })).toBeEnabled()
- await expect(drawer.getByRole('button', { name: '应用音量', exact: true })).toBeDisabled()
-
- await drawer.getByRole('button', { name: '唤醒运行页', exact: true }).click()
- await expect(page.getByText('唤醒执行成功', { exact: true })).toBeVisible()
- const commandSubmission = mock.submissions.at(-1) || {}
- expect(String(asRecord(commandSubmission.payload).type || '').toUpperCase()).toBe('WAKE')
- expect(commandSubmission.idempotencyHeaderPresent).toBe(true)
-
- await drawer.getByRole('tab', { name: '命令记录', exact: true }).click()
- const commandRow = drawer.locator('.el-table__row').filter({ hasText: '唤醒' }).first()
- await expect(commandRow).toContainText('执行成功')
- await commandRow.getByRole('button', { name: '事件', exact: true }).click()
- const events = visibleDialog(page)
- await expect(events).toContainText('命令事件 · 唤醒')
- await expect(events).toContainText('等待下发')
- await expect(events).toContainText('终端已确认')
- await expect(events).toContainText('执行成功')
- await captureScreenshot(page, testInfo, 'remote-v2-04-command-history-and-events')
-
- expect(failures.consoleErrors, '受控远控页面不应产生 console.error').toEqual([])
- expect(failures.pageErrors, '受控远控页面不应产生 pageerror').toEqual([])
- await attachJson(testInfo, 'remote-v2-controlled-contract', {
- statesCovered: ['PENDING_ACTIVATION', 'ONLINE', 'OFFLINE', 'SUSPENDED'],
- createPayloadVerified: true,
- oneTimeActivationMasked: true,
- commandIdempotencyHeaderVerified: true,
- eventChain: ['QUEUED', 'ACKNOWLEDGED', 'SUCCEEDED'],
- sensitiveValuesIncluded: false,
- })
- })
-
- test('只有查看权限时管理与控制动作不可用', async ({ page }, testInfo) => {
- const failures = collectPageFailures(page)
- await installRemoteMocks(page, ['ai.remote.view'])
- await page.goto('/agents/remote-control')
-
- await expect(page.getByText('只读权限', { exact: true })).toBeVisible()
- await expect(page.getByRole('button', { name: '新增终端', exact: true })).toHaveCount(0)
- const row = tableRowByText(page, '电气实训区在线终端')
- await expect(row.getByRole('button', { name: '编辑', exact: true })).toHaveCount(0)
- await row.getByRole('button', { name: '详情', exact: true }).click()
-
- const drawer = page.locator('.remote-detail-drawer:visible')
- await drawer.getByRole('tab', { name: '实时控制', exact: true }).click()
- for (const action of ['唤醒运行页', '暂停播报', '继续播报', '结束会话', '重载运行页', '应用音量']) {
- await expect(drawer.getByRole('button', { name: action, exact: true })).toBeDisabled()
- }
- await drawer.getByRole('tab', { name: '接入与安全', exact: true }).click()
- await expect(drawer.getByRole('button', { name: '重新生成激活链接', exact: true })).toHaveCount(0)
- await expect(drawer.getByRole('button', { name: '停用终端', exact: true })).toHaveCount(0)
- await expect(drawer.getByRole('button', { name: '撤销终端', exact: true })).toHaveCount(0)
- await captureScreenshot(page, testInfo, 'remote-v2-05-view-only-permission-isolation')
-
- expect(failures.consoleErrors, '只读权限页面不应产生 console.error').toEqual([])
- expect(failures.pageErrors, '只读权限页面不应产生 pageerror').toEqual([])
- })
-
- test('最终回执丢失后新 deliveryId 重投只重放回执而不重复执行页面动作', async ({ page }, testInfo) => {
- const failures = collectPageFailures(page)
- const reports: Array<{ deliveryId: string; status: string; httpStatus: number; errorCode: string }> = []
- let pullCount = 0
- let sessionCreates = 0
- let terminalConfigRequests = 0
- let activationRequests = 0
- let activationRequestValid = true
- let legacyPublicConfigRequests = 0
- let legacyPublicSessionRequests = 0
- let terminalConfigHeadersValid = true
- let terminalSessionHeadersValid = true
- let terminalChatRequests = 0
- let terminalEndRequests = 0
- let terminalChatHeadersValid = true
- let terminalEndHeadersValid = true
- let ordinaryPublicPhase = false
- let ordinaryConfigRequests = 0
- let ordinarySessionRequests = 0
- let ordinaryChatRequests = 0
- let ordinaryRequestsOmitDeviceHeaders = true
- let unknownRedeliverySent = false
- let knownDeliveryReady = false
- let knownDeliveryAttempts = 0
- let stopDeliveryReady = false
- let stopDelivered = false
- let firstDeliverySucceededFailures = 0
- let releaseResponses = 0
- let leaderRuntimeInstanceId = ''
- let standbyInteractionBlocked = false
- let originMismatchCredentialPreserved = false
- let trustedHostReconnectSucceeded = false
-
- await page.route('**/api/auth/v1/**', async (route) => {
- const pathname = new URL(route.request().url()).pathname
- if (pathname.endsWith('/system-config/public')) return fulfill(route, { systemName: '虚拟教员系统', shortName: '数字人平台' })
- return fulfill(route, {})
- })
- await page.route('**/api/v1/**', async (route) => {
- const pathname = new URL(route.request().url()).pathname
- if (pathname.endsWith('/avatars')) return fulfill(route, { items: [], total: 0, page: 1, pageSize: 200, pages: 0 })
- return fulfill(route, {})
- })
- await page.route('**/open/v1/**', async (route) => {
- const request = route.request()
- const pathname = new URL(request.url()).pathname
- const requestHeaders = request.headers()
- const hasManagedSessionHeaders = requestHeaders['x-terminal-id'] === 'RTC-RELIABILITY-01'
- && requestHeaders['x-device-secret'] === 'controlled-device-secret'
- && requestHeaders['x-terminal-page-origin'] === controlledAppOrigin
- && Boolean(leaderRuntimeInstanceId)
- && requestHeaders['x-runtime-instance-id'] === leaderRuntimeInstanceId
- && !requestHeaders['x-terminal-secret']
- const omitsDeviceHeaders = !requestHeaders['x-terminal-id']
- && !requestHeaders['x-device-secret']
- && !requestHeaders['x-terminal-page-origin']
- && !requestHeaders['x-runtime-instance-id']
- && !requestHeaders['x-terminal-secret']
- if (pathname === `/open/v1/realtime/${publishedAgent.slug}` && request.method() === 'GET') {
- if (ordinaryPublicPhase) {
- ordinaryConfigRequests += 1
- ordinaryRequestsOmitDeviceHeaders = ordinaryRequestsOmitDeviceHeaders && omitsDeviceHeaders
- return fulfill(route, {
- ...publishedAgent,
- welcomeMessage: '普通公开会话已就绪',
- fallbackMessage: '服务暂不可用',
- accessMode: 'internal',
- brandVisible: true,
- uiConfig: {
- components: [
- { key: 'welcome', enabled: true, order: 1 },
- { key: 'history', enabled: true, order: 2 },
- { key: 'input', enabled: true, order: 3 },
- ],
- suggestions: [],
- },
- })
- }
- legacyPublicConfigRequests += 1
- return fulfill(route, {}, 500)
- }
- if (pathname === `/open/v1/realtime/${publishedAgent.slug}/sessions` && request.method() === 'POST') {
- if (ordinaryPublicPhase) {
- ordinarySessionRequests += 1
- ordinaryRequestsOmitDeviceHeaders = ordinaryRequestsOmitDeviceHeaders && omitsDeviceHeaders
- return fulfill(route, {
- sessionId: 'ordinary-public-session',
- sessionToken: 'ordinary-public-session-token',
- expiresAt: new Date(Date.now() + 60_000).toISOString(),
- }, 201)
- }
- legacyPublicSessionRequests += 1
- return fulfill(route, {}, 500)
- }
- if (pathname === `/open/v1/realtime/${publishedAgent.slug}/chat` && request.method() === 'POST') {
- if (ordinaryPublicPhase) {
- ordinaryChatRequests += 1
- ordinaryRequestsOmitDeviceHeaders = ordinaryRequestsOmitDeviceHeaders
- && omitsDeviceHeaders
- && requestHeaders.authorization === 'Bearer ordinary-public-session-token'
- return fulfill(route, {
- reply: '普通公开会话未携带远程终端设备凭据。',
- audioUrl: null,
- citations: [],
- provider: 'local',
- degraded: false,
- degradationReason: null,
- blocked: false,
- sensitiveCategory: null,
- requestId: 'ordinary-public-chat',
- })
- }
- terminalChatRequests += 1
- terminalChatHeadersValid = terminalChatHeadersValid
- && hasManagedSessionHeaders
- && requestHeaders.authorization === 'Bearer controlled-session-token'
- return fulfill(route, {
- reply: '受控终端会话设备授权头校验通过。',
- audioUrl: null,
- citations: [],
- provider: 'local',
- degraded: false,
- degradationReason: null,
- blocked: false,
- sensitiveCategory: null,
- requestId: 'controlled-terminal-chat',
- })
- }
- if (pathname === `/open/v1/realtime/${publishedAgent.slug}/end` && request.method() === 'POST') {
- terminalEndRequests += 1
- terminalEndHeadersValid = terminalEndHeadersValid
- && hasManagedSessionHeaders
- && requestHeaders.authorization === 'Bearer controlled-session-token'
- return fulfill(route, { sessionId: 'controlled-runtime-session', ended: true })
- }
- return fulfill(route, {})
- })
- await page.route('**/open/v2/**', async (route) => {
- const request = route.request()
- const pathname = new URL(request.url()).pathname
- const headers = request.headers()
- const baseDeviceHeadersValid = headers['x-terminal-id'] === 'RTC-RELIABILITY-01'
- && headers['x-device-secret'] === 'controlled-device-secret'
- && headers['x-terminal-page-origin'] === controlledAppOrigin
- && !headers['x-terminal-secret']
- const configHeadersValid = baseDeviceHeadersValid && !headers['x-runtime-instance-id']
- const ownerSessionHeadersValid = baseDeviceHeadersValid
- && Boolean(leaderRuntimeInstanceId)
- && headers['x-runtime-instance-id'] === leaderRuntimeInstanceId
- if (pathname === '/open/v2/terminals/activate' && request.method() === 'POST') {
- activationRequests += 1
- const payload = request.postDataJSON() as JsonRecord
- activationRequestValid = activationRequestValid
- && payload.activationCode === activationCode
- && payload.terminalCode === 'RTC-RELIABILITY-01'
- && payload.pageOrigin === controlledAppOrigin
- && !headers['x-terminal-id']
- && !headers['x-device-secret']
- && !headers['x-runtime-instance-id']
- && !headers['x-terminal-secret']
- return fulfill(route, {
- terminalId: 'RTC-RELIABILITY-01',
- terminalCode: 'RTC-RELIABILITY-01',
- deviceSecret: 'controlled-device-secret',
- deviceId: 'controlled-device-id',
- pageOrigin: controlledAppOrigin,
- }, 200, { 'Cache-Control': 'no-store' })
- }
- if (pathname === `/open/v2/terminals/agents/${publishedAgent.slug}/config` && request.method() === 'GET') {
- terminalConfigRequests += 1
- terminalConfigHeadersValid = terminalConfigHeadersValid && configHeadersValid
- return fulfill(route, {
- terminal: { id: 'rt-reliability', displayMode: 'WIDGET' },
- agent: {
- ...publishedAgent,
- welcomeMessage: '受控终端已就绪',
- fallbackMessage: '服务暂不可用',
- accessMode: 'private',
- brandVisible: true,
- interactionModes: ['text', 'voice'],
- uiConfig: {
- components: [
- { key: 'welcome', enabled: true, order: 1 },
- { key: 'history', enabled: true, order: 2 },
- { key: 'suggestions', enabled: true, order: 3 },
- { key: 'input', enabled: true, order: 4 },
- ],
- suggestions: ['如何完成设备点检?'],
- },
- },
- }, 200, { 'Cache-Control': 'no-store' })
- }
- if (pathname === `/open/v2/terminals/agents/${publishedAgent.slug}/sessions` && request.method() === 'POST') {
- sessionCreates += 1
- terminalSessionHeadersValid = terminalSessionHeadersValid && ownerSessionHeadersValid
- return fulfill(route, {
- sessionId: 'controlled-runtime-session',
- sessionToken: 'controlled-session-token',
- expiresAt: new Date(Date.now() + 60_000).toISOString(),
- websocketTicket: 'controlled-websocket-ticket',
- websocketUrl: `/open/v1/realtime/${publishedAgent.slug}/stream?ticket=controlled-websocket-ticket`,
- configVersion: publishedAgent.dataVersion,
- }, 201, { 'Cache-Control': 'no-store' })
- }
- if (pathname.endsWith('/heartbeat')) {
- const runtimeInstanceId = headers['x-runtime-instance-id'] || ''
- if (!leaderRuntimeInstanceId) leaderRuntimeInstanceId = runtimeInstanceId
- if (runtimeInstanceId !== leaderRuntimeInstanceId) {
- return fulfill(route, {}, 423, { 'Cache-Control': 'no-store' })
- }
- return fulfill(route, {
- terminalId: 'rt-reliability',
- terminalCode: 'RTC-RELIABILITY-01',
- online: true,
- serverTime: new Date().toISOString(),
- nextHeartbeatSeconds: 15,
- runtimeLeaseSeconds: 45,
- runtimeLeaseExpiresAt: new Date(Date.now() + 45_000).toISOString(),
- desiredConfig: { volume: 60, allowInterrupt: true },
- agent: agentBinding,
- displayMode: 'WIDGET',
- }, 200, { 'Cache-Control': 'no-store' })
- }
- if (pathname.endsWith('/commands/pull')) {
- pullCount += 1
- const isUnknownRedelivery = !unknownRedeliverySent
- if (isUnknownRedelivery) unknownRedeliverySent = true
- const isKnownDelivery = !isUnknownRedelivery && knownDeliveryReady && knownDeliveryAttempts < 2
- if (isKnownDelivery) knownDeliveryAttempts += 1
- const isStopDelivery = !isUnknownRedelivery && !isKnownDelivery && stopDeliveryReady && !stopDelivered
- const deliveryId = isUnknownRedelivery
- ? 'delivery-unknown-redelivery'
- : isKnownDelivery
- ? knownDeliveryAttempts === 1 ? 'delivery-first' : 'delivery-redelivered'
- : isStopDelivery ? 'delivery-stop' : ''
- if (isStopDelivery) stopDelivered = true
- return fulfill(route, {
- items: deliveryId ? [{
- id: isUnknownRedelivery ? 'command-unknown-redelivery' : isStopDelivery ? 'command-stop' : 'command-same-id',
- commandId: isUnknownRedelivery ? 'command-unknown-redelivery' : isStopDelivery ? 'command-stop' : 'command-same-id',
- deliveryId,
- type: isStopDelivery ? 'STOP' : 'WAKE',
- payload: {
- detail: isUnknownRedelivery
- ? '无缓存重投不得执行业务动作'
- : isStopDelivery ? '结束受控终端会话' : '可靠性回归唤醒',
- },
- deliveryAttempt: isUnknownRedelivery ? 2 : isKnownDelivery ? knownDeliveryAttempts : 1,
- redelivery: isUnknownRedelivery || deliveryId === 'delivery-redelivered',
- boundAgentId: publishedAgent.id,
- boundAgentSlug: publishedAgent.slug,
- boundAgentDataVersion: publishedAgent.dataVersion,
- boundAgent: agentBinding,
- agent: agentBinding,
- }] : [],
- pollAfterMs: deliveryId ? 0 : 1_000,
- }, 200, { 'Cache-Control': 'no-store' })
- }
- if (pathname.endsWith('/reports')) {
- const payload = request.postDataJSON() as JsonRecord
- const deliveryId = String(payload.deliveryId || '')
- const status = String(payload.status || '')
- const errorCode = String(payload.errorCode || '')
- if (deliveryId === 'delivery-first' && status === 'SUCCEEDED' && firstDeliverySucceededFailures < 3) {
- firstDeliverySucceededFailures += 1
- reports.push({ deliveryId, status, httpStatus: 503, errorCode })
- return fulfill(route, {}, 503)
- }
- reports.push({ deliveryId, status, httpStatus: 200, errorCode })
- return fulfill(route, { idempotent: false, status }, 200, { 'Cache-Control': 'no-store' })
- }
- if (pathname.endsWith('/runtime/release')) {
- if (headers['x-runtime-instance-id'] !== leaderRuntimeInstanceId) {
- return fulfill(route, {}, 423, { 'Cache-Control': 'no-store' })
- }
- releaseResponses += 1
- leaderRuntimeInstanceId = ''
- return fulfill(route, { released: true, idempotent: false, releasedAt: new Date().toISOString() }, 200, { 'Cache-Control': 'no-store' })
- }
- return fulfill(route, {}, 404)
- })
-
- const hostPath = '/ape2e/controlled-remote-widget-reliability-host'
- const mismatchHostPath = '/ape2e/controlled-remote-widget-origin-mismatch-host'
- let includeControlledActivation = true
- const mismatchHostOrigin = (() => {
- const url = new URL(controlledAppOrigin)
- url.hostname = 'origin-mismatch.invalid'
- return url.origin
- })()
- await page.route(`**${hostPath}`, async (route) => {
- if (new URL(route.request().url()).pathname !== hostPath) return route.continue()
- const activationFragment = includeControlledActivation
- ? `#terminal-activation=${encodeURIComponent(activationCode)}`
- : ''
- await route.fulfill({
- status: 200,
- contentType: 'text/html; charset=utf-8',
- headers: { 'Cache-Control': 'no-store' },
- body: `<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><title>受控远程终端宿主</title></head><body><iframe id="terminal-widget" title="远程数字人终端" src="/embed/${publishedAgent.slug}?terminal=RTC-RELIABILITY-01${activationFragment}" allow="microphone; autoplay" referrerpolicy="origin" style="width:208px;height:92px;border:0"></iframe><script>const frame=document.getElementById('terminal-widget');window.addEventListener('message',function(event){const data=event.data||{};if(event.source!==frame.contentWindow||event.origin!==window.location.origin||data.source!=='virtual-instructor-widget'||data.type!=='resize')return;const width=Math.max(208,Math.min(720,Number(data.width)||208));const height=Math.max(92,Math.min(960,Number(data.height)||92));frame.style.width=width+'px';frame.style.height=height+'px';});</script></body></html>`,
- })
- })
- await page.route(`**${mismatchHostPath}`, async (route) => {
- if (new URL(route.request().url()).pathname !== mismatchHostPath) return route.continue()
- await route.fulfill({
- status: 200,
- contentType: 'text/html; charset=utf-8',
- headers: { 'Cache-Control': 'no-store' },
- body: `<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><title>非法宿主</title></head><body><iframe id="terminal-widget-mismatch" title="来源不匹配的远程数字人终端" src="${controlledAppOrigin}/embed/${publishedAgent.slug}?terminal=RTC-RELIABILITY-01" allow="microphone; autoplay" referrerpolicy="origin" style="width:420px;height:680px;border:0"></iframe></body></html>`,
- })
- })
-
- const runtimeTransport = observeRuntimeTransport(page, 'RTC-RELIABILITY-01', controlledAppOrigin)
- const activationResponse = page.waitForResponse((response) => response.request().method() === 'POST'
- && new URL(response.url()).pathname === '/open/v2/terminals/activate'
- && response.status() === 200, { timeout: 20_000 })
- await page.goto(hostPath)
- await activationResponse
- includeControlledActivation = false
- expect(activationRequests).toBe(1)
- expect(activationRequestValid).toBe(true)
- const widget = page.frameLocator('#terminal-widget')
- await expect(widget.locator('.embed-widget')).not.toHaveClass(/is-expanded/)
- await expect.poll(() => reports.filter((item) => item.deliveryId === 'delivery-unknown-redelivery'
- && item.status === 'FAILED' && item.httpStatus === 200).length, {
- message: '没有本地终态缓存的重投必须直接失败收敛',
- timeout: 20_000,
- }).toBe(1)
- const unknownReports = reports.filter((item) => item.deliveryId === 'delivery-unknown-redelivery')
- expect(unknownReports.map((item) => item.status)).toEqual(['FAILED'])
- expect(unknownReports[0]?.errorCode).toBe('RUNTIME_OUTCOME_UNKNOWN')
- expect(sessionCreates, '未知结果重投不得执行 WAKE 业务动作').toBe(0)
- knownDeliveryReady = true
- await expect.poll(() => reports.filter((item) => item.deliveryId === 'delivery-redelivered'
- && item.status === 'SUCCEEDED' && item.httpStatus === 200).length, {
- timeout: 20_000,
- }).toBe(1)
- await expect(widget.locator('.embed-widget')).toHaveClass(/is-expanded/, { timeout: 20_000 })
-
- expect(sessionCreates, 'WAKE 页面业务动作必须只执行一次').toBe(1)
- expect(terminalConfigRequests, '受管终端必须通过设备凭据读取一次智能体配置').toBe(1)
- expect(terminalConfigHeadersValid, '终端配置请求必须携带设备头和页面 Origin').toBe(true)
- expect(terminalSessionHeadersValid, '终端会话请求必须携带设备头和页面 Origin').toBe(true)
- expect(legacyPublicConfigRequests, '带 terminal 参数时不得调用旧公开配置接口').toBe(0)
- expect(legacyPublicSessionRequests, '带 terminal 参数时不得调用旧公开会话接口').toBe(0)
- expect(firstDeliverySucceededFailures).toBe(3)
- expect(reports.filter((item) => item.deliveryId === 'delivery-first').map((item) => item.status)).toEqual([
- 'ACK', 'EXECUTING', 'SUCCEEDED', 'SUCCEEDED', 'SUCCEEDED',
- ])
- expect(reports.filter((item) => item.deliveryId === 'delivery-redelivered').map((item) => item.status)).toEqual([
- 'SUCCEEDED',
- ])
- expect(runtimeTransport.heartbeatSuccesses(), 'pull 前必须先有一次成功 heartbeat claim').toBeGreaterThan(0)
- expect(runtimeTransport.pullStartedAfterHeartbeat(), '首次 heartbeat 成功前不得启动 pull').toBe(true)
- expect(runtimeTransport.headersValid(), 'heartbeat/pull/report 必须携带同页运行实例头、三项设备头且无旧头').toBe(true)
- expect(runtimeTransport.runtimeInstanceOmittedFromUrls(), '运行实例标识不得进入 URL').toBe(true)
- expect(runtimeTransport.runtimeInstanceIds()).toHaveLength(1)
- expect(await runtimeInstancesAbsentFromBrowserState(page, runtimeTransport.runtimeInstanceIds()), '运行实例标识不得持久化到 URL/storage').toBe(true)
-
- const sessionsBeforeStandby = sessionCreates
- const chatsBeforeStandby = terminalChatRequests
- const heartbeatBeforeStandby = runtimeTransport.heartbeatRequests()
- const pullsBeforeStandby = runtimeTransport.pullRequests()
- await page.evaluate(({ slug }) => {
- const iframe = document.createElement('iframe')
- iframe.id = 'terminal-widget-standby'
- iframe.title = '远程数字人终端备用页'
- iframe.src = `/embed/${encodeURIComponent(slug)}?terminal=RTC-RELIABILITY-01`
- iframe.allow = 'microphone; autoplay'
- iframe.referrerPolicy = 'origin'
- iframe.style.cssText = 'width:420px;height:680px;border:0'
- document.body.appendChild(iframe)
- }, { slug: publishedAgent.slug })
- const standbyWidget = page.frameLocator('#terminal-widget-standby')
- await expect(standbyWidget.locator('.embed-widget')).toBeVisible({ timeout: 20_000 })
- await standbyWidget.locator('.embed-launcher').click()
- await expect(standbyWidget.locator('.embed-widget')).toHaveClass(/is-expanded/)
- await expect(standbyWidget.locator('.terminal-runtime-notice')).toContainText('另一页面运行', { timeout: 20_000 })
- await expect(standbyWidget.getByRole('textbox', { name: '维修问题' })).toBeDisabled()
- await expect(standbyWidget.getByRole('button', { name: '语音提问', exact: true })).toBeDisabled()
- await expect(standbyWidget.getByRole('button', { name: '发送问题', exact: true })).toBeDisabled()
- await expect(standbyWidget.getByText('如何完成设备点检?', { exact: true })).toHaveCount(0)
- await standbyWidget.locator('form.embed-input').evaluate((form) => {
- form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }))
- })
- await page.waitForTimeout(500)
- expect(sessionCreates, 'STANDBY 页面不得签发 scoped session').toBe(sessionsBeforeStandby)
- expect(terminalChatRequests, 'STANDBY 页面不得发送 scoped chat').toBe(chatsBeforeStandby)
- expect(runtimeTransport.heartbeatRequests(), '浏览器非 leader 不得竞争 heartbeat').toBe(heartbeatBeforeStandby)
- expect(runtimeTransport.pullRequests(), '浏览器非 leader 不得启动 pull').toBe(pullsBeforeStandby)
- standbyInteractionBlocked = true
- await page.locator('#terminal-widget-standby').evaluate((element) => element.remove())
-
- await widget.getByRole('textbox', { name: '维修问题' }).fill('验证受控终端会话请求头')
- await widget.getByRole('button', { name: '发送问题', exact: true }).click()
- await expect(widget.getByText('受控终端会话设备授权头校验通过。', { exact: true })).toBeVisible()
- expect(terminalChatRequests, 'REMOTE_TERMINAL chat 必须实际发出一次').toBe(1)
- expect(terminalChatHeadersValid, 'REMOTE_TERMINAL chat 必须同时携带会话令牌、三项设备头且无旧头').toBe(true)
-
- stopDeliveryReady = true
- await expect.poll(() => terminalEndRequests, {
- message: 'STOP 必须结束真实受控会话并调用 end',
- timeout: 20_000,
- }).toBe(1)
- await expect.poll(() => reports.filter((item) => item.deliveryId === 'delivery-stop'
- && item.status === 'SUCCEEDED' && item.httpStatus === 200).length, {
- message: 'STOP 必须形成成功终态回执',
- timeout: 20_000,
- }).toBe(1)
- expect(terminalEndHeadersValid, 'REMOTE_TERMINAL end 必须同时携带会话令牌、三项设备头且无旧头').toBe(true)
- await expect(widget.locator('.terminal-runtime-notice.is-error')).toHaveCount(0)
- await captureScreenshot(page, testInfo, 'remote-v2-06-redelivery-cached-outcome-single-action')
-
- const configRequestsBeforeMismatch = terminalConfigRequests
- const heartbeatRequestsBeforeMismatch = runtimeTransport.heartbeatRequests()
- const activationRequestsBeforeMismatch = activationRequests
- await page.goto(new URL(mismatchHostPath, mismatchHostOrigin).toString())
- const mismatchWidget = page.frameLocator('#terminal-widget-mismatch')
- await expect(mismatchWidget.locator('.embed-widget')).toBeVisible({ timeout: 20_000 })
- await expect(mismatchWidget.locator('.terminal-runtime-notice.is-error')).toContainText('嵌入来源与终端激活来源不一致', { timeout: 20_000 })
- expect(terminalConfigRequests, '非法宿主来源必须在读取 scoped config 前失败关闭').toBe(configRequestsBeforeMismatch)
- expect(runtimeTransport.heartbeatRequests(), '非法宿主来源不得参与 runtime lease').toBe(heartbeatRequestsBeforeMismatch)
- const mismatchFrame = page.frames().find((frame) => {
- try {
- const url = new URL(frame.url())
- return url.origin === controlledAppOrigin && url.pathname === `/embed/${publishedAgent.slug}`
- } catch {
- return false
- }
- })
- expect(mismatchFrame, '非法宿主页面仍应加载应用 iframe 以呈现拒绝原因').toBeTruthy()
- originMismatchCredentialPreserved = await mismatchFrame!.evaluate(() => (
- Boolean(window.localStorage.getItem('ai-person:remote-terminal-runtime:v2:RTC-RELIABILITY-01'))
- ))
- expect(originMismatchCredentialPreserved, '来源不匹配只拒绝本次连接,不得删除合法宿主激活的设备凭据').toBe(true)
-
- const heartbeatSuccessesBeforeTrustedReturn = runtimeTransport.heartbeatSuccesses()
- await page.goto(hostPath)
- await expect.poll(runtimeTransport.heartbeatSuccesses, {
- message: '返回合法宿主后必须复用原设备凭据重新取得 runtime lease',
- timeout: 20_000,
- }).toBeGreaterThan(heartbeatSuccessesBeforeTrustedReturn)
- await expect.poll(() => terminalConfigRequests, {
- message: '返回合法宿主后必须重新读取 scoped config',
- timeout: 20_000,
- }).toBeGreaterThan(configRequestsBeforeMismatch)
- const trustedWidget = page.frameLocator('#terminal-widget')
- await expect(trustedWidget.locator('.embed-widget')).toBeVisible()
- await expect(trustedWidget.locator('.terminal-runtime-notice.is-error')).toHaveCount(0)
- trustedHostReconnectSucceeded = true
- expect(activationRequests, '来源切换过程不得重复兑换一次性激活码').toBe(activationRequestsBeforeMismatch)
-
- const releasesBeforeLeavingHost = runtimeTransport.releaseRequests()
- const releaseResponsesBeforeLeavingHost = releaseResponses
- ordinaryPublicPhase = true
- await page.goto(`/live/${publishedAgent.slug}`)
- await expect.poll(runtimeTransport.releaseRequests, {
- message: '离开受管 WIDGET 时必须 best-effort 释放服务端运行租约',
- timeout: 15_000,
- }).toBeGreaterThan(releasesBeforeLeavingHost)
- await expect.poll(() => releaseResponses, {
- message: '受控 release mock 必须实际处理卸载请求',
- timeout: 15_000,
- }).toBeGreaterThan(releaseResponsesBeforeLeavingHost)
- expect(runtimeTransport.releaseBodyEmpty(), 'release 必须是无 body 的 keepalive POST').toBe(true)
- expect(runtimeTransport.headersValid(), 'release 必须复用同页运行实例和三项设备头且无旧头').toBe(true)
- await expect(page.locator('.live-agent-page')).toBeVisible()
- await page.getByRole('textbox', { name: '维修问题' }).fill('验证普通公开会话请求头隔离')
- await page.getByRole('button', { name: '发送问题', exact: true }).click()
- await expect(page.getByText('普通公开会话未携带远程终端设备凭据。', { exact: true })).toBeVisible()
- expect(ordinaryConfigRequests, '普通运行页必须读取公开配置').toBe(1)
- expect(ordinarySessionRequests, '普通运行页必须创建公开会话').toBe(1)
- expect(ordinaryChatRequests, '普通运行页必须发送公开 chat').toBe(1)
- expect(ordinaryRequestsOmitDeviceHeaders, '无 terminal 参数的公开请求不得携带任何设备授权头').toBe(true)
-
- await attachJson(testInfo, 'remote-v2-redelivery-evidence', {
- commandId: 'command-same-id',
- deliveryIds: ['delivery-first', 'delivery-redelivered', 'delivery-stop'],
- actionExecutionCount: sessionCreates,
- terminalScopedAccess: {
- configRequests: terminalConfigRequests,
- sessionRequests: sessionCreates,
- configDeviceHeadersValid: terminalConfigHeadersValid,
- sessionDeviceHeadersValid: terminalSessionHeadersValid,
- legacyPublicConfigRequests,
- legacyPublicSessionRequests,
- },
- terminalSessionFollowups: {
- chatRequests: terminalChatRequests,
- endRequests: terminalEndRequests,
- chatDeviceHeadersValid: terminalChatHeadersValid,
- endDeviceHeadersValid: terminalEndHeadersValid,
- },
- ordinaryPublicIsolation: {
- configRequests: ordinaryConfigRequests,
- sessionRequests: ordinarySessionRequests,
- chatRequests: ordinaryChatRequests,
- deviceHeadersOmitted: ordinaryRequestsOmitDeviceHeaders,
- },
- firstFinalReportNetworkFailures: firstDeliverySucceededFailures,
- redeliveryReportSequence: reports.map((item) => ({ ...item })),
- unknownRedelivery: {
- failedWithoutBusinessAction: true,
- errorCode: 'RUNTIME_OUTCOME_UNKNOWN',
- businessActionCountBeforeKnownDelivery: 0,
- },
- standbyInteraction: {
- blocked: standbyInteractionBlocked,
- scopedSessionRequests: 0,
- scopedChatRequests: 0,
- runtimeTransportRequests: 0,
- },
- trustedOriginIsolation: {
- activationRequests,
- activationRequestValid,
- mismatchConnectionRejectedBeforeConfig: true,
- credentialPreserved: originMismatchCredentialPreserved,
- trustedHostReconnectSucceeded,
- },
- runtimeLeaseContract: {
- heartbeatRequests: runtimeTransport.heartbeatRequests(),
- pullRequests: runtimeTransport.pullRequests(),
- reportRequests: runtimeTransport.reportRequests(),
- releaseRequests: runtimeTransport.releaseRequests(),
- releaseResponses,
- firstHeartbeatBeforePull: runtimeTransport.pullStartedAfterHeartbeat(),
- samePageInstanceHeadersValid: runtimeTransport.headersValid(),
- instanceInUrlOrStorage: false,
- releaseBodyEmpty: runtimeTransport.releaseBodyEmpty(),
- },
- secretIncluded: false,
- })
- const unexpectedConsoleErrors = failures.consoleErrors.filter((message) => !/503|Failed to load resource/i.test(message))
- expect(unexpectedConsoleErrors, '除受控 503 外,重投可靠性回归不应产生 console.error').toEqual([])
- expect(failures.pageErrors, '重投可靠性回归不应产生 pageerror').toEqual([])
- })
- })
|