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 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 = {}) { 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, 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 terminalConfigFailures = 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 if (terminalConfigFailures === 0) { terminalConfigFailures += 1 return fulfill(route, {}, 503, { 'Cache-Control': 'no-store' }) } 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: `受控远程终端宿主`, }) }) 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: `非法宿主`, }) }) 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(terminalConfigFailures, '受管配置首次临时失败必须被测试覆盖').toBe(1) expect(terminalConfigRequests, '受管终端配置首次 503 后必须自动重试成功').toBe(2) 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( [...new Set(runtimeTransport.endpointInstances().pull)], '浏览器非 leader 不得以第二个 runtime instance 启动 pull', ).toEqual([leaderRuntimeInstanceId]) expect(runtimeTransport.pullRequests(), '主 leader 在校验期间应继续正常长轮询').toBeGreaterThanOrEqual(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.addInitScript(({ appOrigin, hostOrigin }) => { if (window.location.origin !== appOrigin || !document.referrer.startsWith(hostOrigin)) return window.localStorage.setItem('ai-person:remote-terminal-runtime:v2:RTC-RELIABILITY-01', JSON.stringify({ terminalId: 'RTC-RELIABILITY-01', terminalCode: 'RTC-RELIABILITY-01', secret: 'controlled-device-secret', deviceId: 'controlled-device-id', pageOrigin: appOrigin, })) }, { appOrigin: controlledAppOrigin, hostOrigin: mismatchHostOrigin }) 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) // 此处只验证 Origin fail-closed 与凭据保留;受控路由不模拟真实时间, // 手动推进到服务端租约已到期,正常卸载/release 由真实生命周期用例覆盖。 leaderRuntimeInstanceId = '' const heartbeatAfterTrustedReturn = page.waitForResponse((response) => ( response.request().method() === 'POST' && new URL(response.url()).pathname === '/open/v2/terminals/heartbeat' && response.status() === 200 ), { timeout: 20_000 }) await page.goto(hostPath) await heartbeatAfterTrustedReturn 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() ordinaryPublicPhase = true await page.goto(`/live/${publishedAgent.slug}`) await expect.poll(runtimeTransport.releaseRequests, { message: '离开受管 WIDGET 时必须 best-effort 释放服务端运行租约', timeout: 15_000, }).toBeGreaterThan(releasesBeforeLeavingHost) 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([]) }) })