|
- import {
- request as playwrightRequest,
- type APIRequestContext,
- type Page,
- type TestInfo,
- } from '@playwright/test'
-
- import { attachJson, captureScreenshot, expect, runId, runPrefix, test } from './fixtures'
- import { authHeaders, envelopeData, fillFormItem, formItem, loginAsAdmin } from './helpers'
-
- type JsonRecord = Record<string, unknown>
- type Headers = Record<string, string>
-
- interface VersionedTerminal {
- id: string
- dataVersion: number
- }
-
- interface RuntimeActivity {
- module: string
- action: string
- result: 'PASS' | 'FAIL' | 'CLEANED' | 'CLEANUP_FAILED'
- httpStatus?: number
- resourceId?: string
- detail?: string
- }
-
- interface ResponseLike {
- status(): number
- json(): Promise<unknown>
- headers(): Record<string, string>
- }
-
- // 本地 Vite 目前只代理 /api;需要验证 /open/v1 运行端协议时可把
- // E2E_RUNTIME_API_URL 指向同库启动、且已注入进程级 pepper 的真实 API。
- // 部署环境的 BASE_URL 同时代理 /api 与 /open 时无需单独配置。
- const runtimeApiBaseURL = (
- process.env.E2E_RUNTIME_API_URL
- || process.env.BASE_URL
- || 'http://127.0.0.1:8003'
- ).replace(/\/$/, '')
-
- const asRecord = (value: unknown): JsonRecord => value && typeof value === 'object'
- ? value as JsonRecord
- : {}
-
- const asRecords = (value: unknown): JsonRecord[] => Array.isArray(value)
- ? value.map(asRecord)
- : []
-
- const assertStatus = (response: ResponseLike, expected: number | number[], label: string) => {
- const allowed = Array.isArray(expected) ? expected : [expected]
- expect(allowed, `${label}:HTTP ${response.status()}`).toContain(response.status())
- }
-
- async function responseData<T = JsonRecord>(
- response: ResponseLike,
- 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)
- }
-
- async function selectBoundAgent(page: Page, dialog: ReturnType<Page['locator']>, excluded = '') {
- const select = formItem(dialog, /业务用途/).locator('.el-select')
- await select.click()
- const dropdown = page.locator('.el-select-dropdown:visible').last()
- await expect(dropdown).toBeVisible()
- const options = dropdown.locator('.el-select-dropdown__item:not(.is-disabled)')
- await expect(options.first()).toBeVisible()
- const labels = (await options.allInnerTexts()).map((value) => value.trim()).filter(Boolean)
- const selected = labels.find((value) => value !== excluded)
- if (!selected) throw new Error(excluded ? '没有第二个可绑定的真实智能体' : '没有可绑定的真实智能体')
- await options.filter({ hasText: selected }).first().click()
- return selected
- }
-
- async function setSliderValue(slider: ReturnType<Page['locator']>, target: number) {
- await slider.focus()
- let current = Number(await slider.getAttribute('aria-valuenow'))
- if (!Number.isFinite(current)) throw new Error('终端音量滑块缺少 aria-valuenow')
- const key = target > current ? 'ArrowRight' : 'ArrowLeft'
- while (current !== target) {
- await slider.press(key)
- current = Number(await slider.getAttribute('aria-valuenow'))
- }
- await expect(slider).toHaveAttribute('aria-valuenow', String(target))
- }
-
- async function setSwitchChecked(control: ReturnType<Page['locator']>, checked: boolean) {
- const current = (await control.getAttribute('class'))?.includes('is-checked') ?? false
- if (current !== checked) await control.click()
- if (checked) await expect(control).toHaveClass(/is-checked/)
- else await expect(control).not.toHaveClass(/is-checked/)
- }
-
- async function consumeOneTimeSecret(page: Page, title: string | RegExp) {
- const box = page.locator('.el-message-box:visible').last()
- await box.waitFor({ state: 'visible' })
- // 在同一个浏览器求值中读取后立即覆盖正文。这样后续任一步骤失败时,
- // Playwright 自动生成的错误上下文、DOM 快照和失败截图都只能看到遮罩值。
- const displayed = await box.evaluate((element) => {
- const titleElement = element.querySelector('.el-message-box__title')
- const messageElement = element.querySelector('.el-message-box__message')
- const value = {
- title: titleElement?.textContent?.trim() || '',
- secret: messageElement?.textContent?.trim() || '',
- }
- if (messageElement) messageElement.textContent = '••••••••(一次性密钥已由测试进程安全接收并立即遮罩)'
- return value
- })
- await box.getByRole('button', { name: '我已安全保存' }).click()
- await box.waitFor({ state: 'hidden' })
- const matchesTitle = typeof title === 'string' ? displayed.title === title : title.test(displayed.title)
- if (!matchesTitle) throw new Error('一次性密钥弹窗标题不符合预期')
- if (displayed.secret.length < 24) throw new Error('页面没有展示有效的一次性终端接入密钥')
- return displayed.secret
- }
-
- const recordPass = (
- activities: RuntimeActivity[],
- action: string,
- response: ResponseLike,
- resourceId?: string,
- detail?: string,
- ) => activities.push({
- module: '远控终端运行协议',
- action,
- result: 'PASS',
- httpStatus: response.status(),
- resourceId,
- detail,
- })
-
- async function openTerminalPage(page: Page, terminalName: string) {
- await page.goto('/agents/remote-control')
- await expect(page.getByText('远程控制', { exact: true }).first()).toBeVisible()
- const keyword = page.getByPlaceholder('搜索终端名称、部署位置或业务用途')
- await expect(keyword).toBeVisible()
- await keyword.fill(terminalName)
- const card = page.locator('.terminal-card').filter({ hasText: terminalName }).first()
- await expect(card).toBeVisible()
- return card
- }
-
- async function captureTerminalCard(
- page: Page,
- testInfo: TestInfo,
- terminalName: string,
- label: string,
- connectionStatus?: string,
- ) {
- const card = await openTerminalPage(page, terminalName)
- if (connectionStatus) await expect(card.getByText(connectionStatus, { exact: true })).toBeVisible()
- await captureScreenshot(page, testInfo, label, [page.locator('code:visible')])
- }
-
- async function captureTerminalCommand(
- page: Page,
- testInfo: TestInfo,
- terminalName: string,
- label: string,
- commandStatus: string,
- ) {
- const card = await openTerminalPage(page, terminalName)
- await card.getByRole('button', { name: /业务控制|查看详情/ }).click()
- const dialog = page.locator('.el-dialog:visible').last()
- await expect(dialog).toContainText(terminalName)
- const status = dialog.getByText(commandStatus, { exact: true })
- await status.scrollIntoViewIfNeeded()
- await expect(status).toBeVisible()
- await captureScreenshot(page, testInfo, label, [dialog.locator('code:visible')])
- }
-
- async function captureDeletedState(
- page: Page,
- testInfo: TestInfo,
- terminalName: string,
- ) {
- await page.goto('/agents/remote-control')
- await expect(page.getByText('远程控制', { exact: true }).first()).toBeVisible()
- const keyword = page.getByPlaceholder('搜索终端名称、部署位置或业务用途')
- await keyword.fill(terminalName)
- await expect(page.locator('.terminal-card').filter({ hasText: terminalName })).toHaveCount(0)
- await expect(page.getByText('没有符合条件的远程终端')).toBeVisible()
- await captureScreenshot(page, testInfo, '远控终端-13-页面删除后无残留')
- }
-
- async function fallbackCleanup(
- api: APIRequestContext,
- headers: Headers,
- terminal: VersionedTerminal | null,
- activities: RuntimeActivity[],
- cleanupErrors: string[],
- ) {
- if (!terminal) return
- try {
- const detail = await api.get(`/api/v1/remote-terminals/${encodeURIComponent(terminal.id)}`, { headers })
- if (detail.status() === 404) {
- activities.push({
- module: '远控终端运行协议',
- action: '失败兜底清理',
- result: 'CLEANED',
- resourceId: terminal.id,
- detail: '终端已不存在',
- })
- return
- }
- if (detail.status() !== 200) {
- const message = `远控终端清理读取失败:HTTP ${detail.status()}`
- cleanupErrors.push(message)
- activities.push({
- module: '远控终端运行协议',
- action: '失败兜底清理',
- result: 'CLEANUP_FAILED',
- httpStatus: detail.status(),
- resourceId: terminal.id,
- detail: message,
- })
- return
- }
- const current = asRecord(envelopeData(await detail.json()))
- const dataVersion = Number(current.dataVersion)
- const removed = await api.delete(`/api/v1/remote-terminals/${encodeURIComponent(terminal.id)}`, {
- headers,
- params: { dataVersion: String(dataVersion) },
- })
- if (![200, 404].includes(removed.status())) {
- const message = `远控终端清理删除失败:HTTP ${removed.status()}`
- cleanupErrors.push(message)
- activities.push({
- module: '远控终端运行协议',
- action: '失败兜底清理',
- result: 'CLEANUP_FAILED',
- httpStatus: removed.status(),
- resourceId: terminal.id,
- detail: message,
- })
- return
- }
- const absent = await api.get(`/api/v1/remote-terminals/${encodeURIComponent(terminal.id)}`, { headers })
- if (absent.status() !== 404) {
- const message = `远控终端清理后仍可读取:HTTP ${absent.status()}`
- cleanupErrors.push(message)
- activities.push({
- module: '远控终端运行协议',
- action: '失败兜底清理',
- result: 'CLEANUP_FAILED',
- httpStatus: absent.status(),
- resourceId: terminal.id,
- detail: message,
- })
- return
- }
- activities.push({
- module: '远控终端运行协议',
- action: '失败兜底清理',
- result: 'CLEANED',
- httpStatus: removed.status(),
- resourceId: terminal.id,
- })
- } catch {
- const message = '远控终端清理发生异常(敏感上下文未写入报告)'
- cleanupErrors.push(message)
- activities.push({
- module: '远控终端运行协议',
- action: '失败兜底清理',
- result: 'CLEANUP_FAILED',
- resourceId: terminal.id,
- detail: message,
- })
- }
- }
-
- test.describe('真实远控终端运行协议', () => {
- test('页面创建编辑停启、运行端鉴权、页面下发、状态事件、页面轮换与删除闭环', async ({ page }, testInfo) => {
- test.setTimeout(180_000)
- const api = await playwrightRequest.newContext({ baseURL: runtimeApiBaseURL })
- const activities: RuntimeActivity[] = []
- const cleanupErrors: string[] = []
- let headers: Headers = {}
- let terminal: VersionedTerminal | null = null
- let terminalDeleted = false
- let activeStage = '初始化管理员会话'
-
- try {
- headers = await authHeaders(api)
- await loginAsAdmin(page)
-
- const suffix = `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`.toUpperCase()
- // 页面业务约束为 36 字符;保留 APE2E 标记和随机尾缀,避免浏览器按 maxlength
- // 截断后测试仍拿未截断值做后验。
- const terminalName = `${runPrefix.slice(0, 22)}远控终端-${suffix.slice(-6)}`
- const terminalCode = `APE2E-RT-${suffix}`.slice(0, 64)
- const initialLocation = '智能装备实训中心 A203'
- const editedLocation = '智能装备实训中心 B306'
-
- activeStage = '通过页面创建终端并接收一次性凭据'
- await page.goto('/agents/remote-control')
- await expect(page.getByRole('button', { name: '新增终端' })).toBeVisible()
- await page.getByRole('button', { name: '新增终端' }).click()
- let editor = page.locator('.el-dialog:visible').last()
- await expect(editor).toContainText('新增远程终端')
- await fillFormItem(editor, '终端名称', terminalName)
- await fillFormItem(editor, '部署位置', initialLocation)
- const initialAgent = await selectBoundAgent(page, editor)
- await setSwitchChecked(editor.locator('.option-grid .el-switch').nth(0), true)
- await setSwitchChecked(editor.locator('.option-grid .el-switch').nth(1), true)
- await editor.locator('.technical-collapse .el-collapse-item__header').click()
- await fillFormItem(editor, '终端编码', terminalCode)
- await fillFormItem(editor, '控制服务完整 URL', 'wss://ape2e.invalid/control')
- const createdResponsePromise = page.waitForResponse((response) => response.request().method() === 'POST'
- && /\/api\/v1\/remote-terminals(?:\?|$)/.test(response.url()))
- await editor.getByRole('button', { name: '保存终端' }).click()
- const createdResponse = await createdResponsePromise
- const created = asRecord(envelopeData(await createdResponse.json()))
- const createdTerminal = asRecord(created.terminal)
- const credential = asRecord(created.credential)
- const terminalId = String(createdTerminal.id ?? '')
- const initialSecret = String(credential.secret ?? '')
- const initialVersion = Number(createdTerminal.dataVersion)
- const displayedInitialSecret = initialSecret
- ? await consumeOneTimeSecret(page, '请立即保存终端接入密钥')
- : ''
- assertStatus(createdResponse, 201, '创建远控终端')
- if (!terminalId || !Number.isInteger(initialVersion)) throw new Error('创建响应缺少终端标识或数据版本')
- if (!initialSecret || credential.shownOnce !== true) throw new Error('创建响应没有生成仅显示一次的终端凭据')
- if (displayedInitialSecret !== initialSecret) throw new Error('页面一次性终端密钥与创建响应不一致')
- terminal = { id: terminalId, dataVersion: initialVersion }
- const createdDetailResponse = await api.get(`/api/v1/remote-terminals/${encodeURIComponent(terminalId)}`, { headers })
- const createdDetail = asRecord(await responseData(createdDetailResponse, 200, '创建终端 API 后验'))
- expect(createdDetail).toMatchObject({
- name: terminalName,
- code: terminalCode,
- location: initialLocation,
- boundAgent: initialAgent,
- enabled: true,
- volume: 60,
- allowInterrupt: true,
- })
- recordPass(activities, '页面创建启用终端、绑定真实智能体并接收一次性凭据', createdResponse, terminalId, '页面展示值与响应一致;凭据只保存在测试进程内,未写入附件或截图')
- await captureTerminalCard(page, testInfo, terminalName, '远控终端-01-创建后离线', '离线')
-
- activeStage = '通过页面编辑绑定智能体、音量与打断设置'
- let card = await openTerminalPage(page, terminalName)
- await card.getByRole('button', { name: '编辑', exact: true }).click()
- editor = page.locator('.el-dialog:visible').last()
- await expect(editor).toContainText('编辑远程终端')
- await fillFormItem(editor, '部署位置', editedLocation)
- const editedAgent = await selectBoundAgent(page, editor, initialAgent)
- await setSliderValue(editor.getByRole('slider'), 72)
- await setSwitchChecked(editor.locator('.option-grid .el-switch').nth(0), false)
- const editResponsePromise = page.waitForResponse((response) => response.request().method() === 'PUT'
- && response.url().includes(`/api/v1/remote-terminals/${encodeURIComponent(terminalId)}`)
- && !response.url().includes('/status'))
- await editor.getByRole('button', { name: '保存终端' }).click()
- const editResponse = await editResponsePromise
- const editedTerminal = asRecord(await responseData(editResponse, 200, '页面编辑远控终端'))
- terminal.dataVersion = Number(editedTerminal.dataVersion)
- const editedDetailResponse = await api.get(`/api/v1/remote-terminals/${encodeURIComponent(terminalId)}`, { headers })
- const editedDetail = asRecord(await responseData(editedDetailResponse, 200, '编辑终端 API 后验'))
- expect(editedDetail).toMatchObject({
- location: editedLocation,
- boundAgent: editedAgent,
- enabled: true,
- volume: 72,
- allowInterrupt: false,
- })
- expect(String(editedDetail.boundAgentId ?? '')).not.toBe('')
- card = await openTerminalPage(page, terminalName)
- await expect(card).toContainText(editedLocation)
- await expect(card).toContainText(editedAgent)
- await card.getByRole('button', { name: '编辑', exact: true }).click()
- editor = page.locator('.el-dialog:visible').last()
- await expect(editor.getByRole('slider')).toHaveAttribute('aria-valuenow', '72')
- await expect(editor.locator('.option-grid .el-switch').nth(0)).not.toHaveClass(/is-checked/)
- await captureScreenshot(page, testInfo, '远控终端-02-编辑绑定音量打断刷新持久化')
- await editor.getByRole('button', { name: '取消' }).click()
- recordPass(activities, '页面编辑并刷新验证绑定智能体、音量与语音打断设置', editResponse, terminalId, `${initialAgent} → ${editedAgent},音量 72%,禁用语音打断`)
-
- activeStage = '通过页面停用并重新启用终端'
- card = await openTerminalPage(page, terminalName)
- let statusSwitch = card.locator('footer .el-switch')
- const disableResponsePromise = page.waitForResponse((response) => response.request().method() === 'PUT'
- && response.url().includes(`/api/v1/remote-terminals/${encodeURIComponent(terminalId)}/status`))
- await statusSwitch.click()
- const disableResponse = await disableResponsePromise
- const disabledTerminal = asRecord(await responseData(disableResponse, 200, '页面停用远控终端'))
- terminal.dataVersion = Number(disabledTerminal.dataVersion)
- await expect(card.getByText('已停用', { exact: true })).toBeVisible()
- const disabledDetailResponse = await api.get(`/api/v1/remote-terminals/${encodeURIComponent(terminalId)}`, { headers })
- expect(asRecord(await responseData(disabledDetailResponse, 200, '停用终端 API 后验')).enabled).toBe(false)
- await captureScreenshot(page, testInfo, '远控终端-03-页面停用')
-
- card = await openTerminalPage(page, terminalName)
- statusSwitch = card.locator('footer .el-switch')
- const enableResponsePromise = page.waitForResponse((response) => response.request().method() === 'PUT'
- && response.url().includes(`/api/v1/remote-terminals/${encodeURIComponent(terminalId)}/status`))
- await statusSwitch.click()
- const enableResponse = await enableResponsePromise
- const enabledTerminal = asRecord(await responseData(enableResponse, 200, '页面重新启用远控终端'))
- terminal.dataVersion = Number(enabledTerminal.dataVersion)
- const enabledDetailResponse = await api.get(`/api/v1/remote-terminals/${encodeURIComponent(terminalId)}`, { headers })
- expect(asRecord(await responseData(enabledDetailResponse, 200, '启用终端 API 后验')).enabled).toBe(true)
- recordPass(activities, '页面停用终端', disableResponse, terminalId)
- recordPass(activities, '页面重新启用终端', enableResponse, terminalId)
-
- const runtimePath = `/open/v1/remote-terminals/${encodeURIComponent(terminalId)}`
-
- activeStage = '拒绝缺失和错误凭据的心跳'
- const missingHeartbeat = await api.post(`${runtimePath}/heartbeat`, {
- data: { runtimeVersion: 'APE2E-1.0' },
- })
- assertStatus(missingHeartbeat, 401, '缺失凭据心跳必须拒绝')
- recordPass(activities, '缺失凭据心跳被拒绝', missingHeartbeat, terminalId)
- const wrongHeartbeat = await api.post(`${runtimePath}/heartbeat`, {
- headers: { 'X-Terminal-Secret': 'ape2e-intentionally-invalid' },
- data: { runtimeVersion: 'APE2E-1.0' },
- })
- assertStatus(wrongHeartbeat, 401, '错误凭据心跳必须拒绝')
- recordPass(activities, '错误凭据心跳被拒绝', wrongHeartbeat, terminalId)
- await captureTerminalCard(page, testInfo, terminalName, '远控终端-04-非法心跳拒绝后仍离线', '离线')
-
- activeStage = '使用正确凭据上报心跳'
- const runtimeHeaders = { 'X-Terminal-Secret': initialSecret }
- const heartbeatResponse = await api.post(`${runtimePath}/heartbeat`, {
- headers: runtimeHeaders,
- data: { runtimeVersion: 'APE2E-1.0' },
- })
- const heartbeat = asRecord(await responseData(heartbeatResponse, 200, '终端正确心跳'))
- expect(heartbeat.online).toBe(true)
- expect(heartbeatResponse.headers()['cache-control']).toBe('no-store')
- recordPass(activities, '正确凭据心跳使终端在线', heartbeatResponse, terminalId, '响应带 Cache-Control: no-store')
- await captureTerminalCard(page, testInfo, terminalName, '远控终端-05-正确心跳后在线', '在线')
-
- activeStage = '通过管理页面下发唤醒指令'
- card = await openTerminalPage(page, terminalName)
- await card.getByRole('button', { name: /业务控制/ }).click()
- let detailDialog = page.locator('.el-dialog:visible').last()
- await expect(detailDialog).toContainText('终端在线')
- const commandResponsePromise = page.waitForResponse((response) => response.request().method() === 'POST'
- && response.url().includes(`/api/v1/remote-terminals/${encodeURIComponent(terminalId)}/commands`))
- await detailDialog.locator('.command-grid button').filter({ hasText: '远程唤醒' }).click()
- const commandResponse = await commandResponsePromise
- const command = asRecord(await responseData(commandResponse, 202, '下发远控指令'))
- const commandId = String(command.id ?? '')
- if (!commandId) throw new Error('指令下发响应缺少指令标识')
- expect(command.statusCode).toBe('QUEUED')
- await expect(detailDialog.locator('.command-history')).toContainText('远程唤醒')
- await expect(detailDialog.locator('.command-history')).toContainText('等待终端确认')
- recordPass(activities, '管理页面下发唤醒指令并立即显示历史', commandResponse, commandId, '状态 QUEUED')
- await captureScreenshot(page, testInfo, '远控终端-06-页面下发指令已排队', [detailDialog.locator('code:visible')])
-
- activeStage = '运行端拉取指令并生成投递事件'
- const pullResponse = await api.post(`${runtimePath}/commands/pull`, {
- headers: runtimeHeaders,
- data: { limit: 10, waitSeconds: 0 },
- })
- const pulled = asRecord(await responseData(pullResponse, 200, '运行端拉取指令'))
- const items = asRecords(pulled.items)
- expect(items).toHaveLength(1)
- const delivery = items[0]!
- expect(String(delivery.id ?? '')).toBe(commandId)
- expect(delivery.deliveryAttempt).toBe(1)
- expect(delivery.redelivery).toBe(false)
- const deliveryId = String(delivery.deliveryId ?? '')
- if (deliveryId.length < 8) throw new Error('指令拉取响应缺少有效投递标识')
- recordPass(activities, '运行端拉取指令并产生 SENT 投递事件', pullResponse, commandId, '首次投递且非重投')
- await captureTerminalCommand(page, testInfo, terminalName, '远控终端-07-运行端已拉取指令', '等待终端确认')
-
- const reportPath = `${runtimePath}/commands/${encodeURIComponent(commandId)}/reports`
- const ackReport = `report-${suffix}-ack`.slice(0, 64)
- const executingReport = `report-${suffix}-executing`.slice(0, 64)
- const succeededReport = `report-${suffix}-succeeded`.slice(0, 64)
-
- activeStage = '上报 ACK 并验证 reportId 幂等'
- const ackPayload = { reportId: ackReport, deliveryId, status: 'ACK' }
- const ackResponse = await api.post(reportPath, { headers: runtimeHeaders, data: ackPayload })
- const ack = asRecord(await responseData(ackResponse, 200, '上报 ACK'))
- expect(ack.idempotent).toBe(false)
- expect(ack.status).toBe('ACK')
- const replayResponse = await api.post(reportPath, { headers: runtimeHeaders, data: ackPayload })
- const replay = asRecord(await responseData(replayResponse, 200, '重复上报 ACK'))
- expect(replay.idempotent).toBe(true)
- expect(replay.status).toBe('ACK')
- recordPass(activities, '上报 ACK 事件', ackResponse, commandId, '状态 ACKNOWLEDGED')
- recordPass(activities, '相同 reportId 重放保持幂等', replayResponse, commandId, 'idempotent=true,未重复推进状态')
- await captureTerminalCommand(page, testInfo, terminalName, '远控终端-08-ACK与幂等重放', '已送达')
-
- activeStage = '上报 EXECUTING 事件'
- const executingResponse = await api.post(reportPath, {
- headers: runtimeHeaders,
- data: {
- reportId: executingReport,
- deliveryId,
- status: 'EXECUTING',
- result: { message: '开始执行唤醒', durationMs: 8 },
- },
- })
- const executing = asRecord(await responseData(executingResponse, 200, '上报 EXECUTING'))
- expect(executing.idempotent).toBe(false)
- expect(executing.status).toBe('EXECUTING')
- recordPass(activities, '上报 EXECUTING 事件', executingResponse, commandId, '状态 EXECUTING')
- await captureTerminalCommand(page, testInfo, terminalName, '远控终端-09-指令执行中', '执行中')
-
- activeStage = '上报 SUCCEEDED 事件'
- const succeededResponse = await api.post(reportPath, {
- headers: runtimeHeaders,
- data: {
- reportId: succeededReport,
- deliveryId,
- status: 'SUCCEEDED',
- result: { message: '唤醒执行完成', durationMs: 28 },
- },
- })
- const succeeded = asRecord(await responseData(succeededResponse, 200, '上报 SUCCEEDED'))
- expect(succeeded.idempotent).toBe(false)
- expect(succeeded.status).toBe('SUCCEEDED')
- recordPass(activities, '上报 SUCCEEDED 事件', succeededResponse, commandId, '状态 SUCCEEDED')
- await captureTerminalCommand(page, testInfo, terminalName, '远控终端-10-指令执行成功', '执行成功')
-
- activeStage = '管理端详情和历史聚合校验事件链'
- const commandDetailResponse = await api.get(`/api/v1/remote-commands/${encodeURIComponent(commandId)}`, { headers })
- const commandDetail = asRecord(await responseData(commandDetailResponse, 200, '读取远控指令详情'))
- expect(commandDetail.statusCode).toBe('SUCCEEDED')
- expect(commandDetail.deliveryAttempts).toBe(1)
- expect(String(commandDetail.ackAt ?? '')).not.toBe('')
- const historyResponse = await api.get(`/api/v1/remote-terminals/${encodeURIComponent(terminalId)}/commands`, {
- headers,
- params: { page: '1', pageSize: '20' },
- })
- const history = asRecord(await responseData(historyResponse, 200, '读取远控指令历史'))
- const historyItem = asRecords(history.items).find((item) => String(item.id ?? '') === commandId)
- expect(historyItem?.statusCode).toBe('SUCCEEDED')
- recordPass(activities, '管理端详情与历史聚合校验完整事件链', historyResponse, commandId, 'SENT → ACKNOWLEDGED → EXECUTING → SUCCEEDED')
- await captureTerminalCommand(page, testInfo, terminalName, '远控终端-11-页面历史与事件链', '执行成功')
-
- activeStage = '通过页面轮换凭据并验证新旧凭据边界'
- card = await openTerminalPage(page, terminalName)
- await card.getByRole('button', { name: /业务控制/ }).click()
- detailDialog = page.locator('.el-dialog:visible').last()
- await detailDialog.locator('.console-technical-collapse .el-collapse-item__header').click()
- const rotateResponsePromise = page.waitForResponse((response) => response.request().method() === 'POST'
- && response.url().includes(`/api/v1/remote-terminals/${encodeURIComponent(terminalId)}/rotate-secret`))
- await detailDialog.getByRole('button', { name: '轮换接入密钥' }).click()
- let messageBox = page.locator('.el-message-box:visible').last()
- await expect(messageBox).toContainText('轮换后旧密钥将立即失效')
- await messageBox.getByRole('button', { name: '确认轮换' }).click()
- const rotateResponse = await rotateResponsePromise
- const rotated = asRecord(envelopeData(await rotateResponse.json()))
- const rotatedCredential = asRecord(rotated.credential)
- const rotatedSecret = String(rotatedCredential.secret ?? '')
- const rotatedVersion = Number(rotated.dataVersion)
- const displayedRotatedSecret = rotatedSecret
- ? await consumeOneTimeSecret(page, '新终端接入密钥(仅显示一次)')
- : ''
- assertStatus(rotateResponse, 200, '轮换终端凭据')
- if (!rotatedSecret || rotatedSecret === initialSecret || rotatedCredential.shownOnce !== true) {
- throw new Error('终端凭据轮换未生成独立且仅显示一次的新凭据')
- }
- if (!Number.isInteger(rotatedVersion) || rotatedVersion <= terminal.dataVersion) throw new Error('凭据轮换后数据版本没有递增')
- if (displayedRotatedSecret !== rotatedSecret) throw new Error('页面一次性新密钥与轮换响应不一致')
- terminal.dataVersion = rotatedVersion
- const oldSecretHeartbeat = await api.post(`${runtimePath}/heartbeat`, {
- headers: runtimeHeaders,
- data: { runtimeVersion: 'APE2E-1.0' },
- })
- assertStatus(oldSecretHeartbeat, 401, '轮换后旧凭据必须失效')
- const rotatedHeaders = { 'X-Terminal-Secret': rotatedSecret }
- const newSecretHeartbeat = await api.post(`${runtimePath}/heartbeat`, {
- headers: rotatedHeaders,
- data: { runtimeVersion: 'APE2E-1.0' },
- })
- const newHeartbeat = asRecord(await responseData(newSecretHeartbeat, 200, '轮换后新凭据心跳'))
- expect(newHeartbeat.online).toBe(true)
- recordPass(activities, '页面轮换终端一次性凭据', rotateResponse, terminalId, '新凭据仅保存在测试进程内,未写入附件或截图')
- recordPass(activities, '轮换后旧凭据被拒绝', oldSecretHeartbeat, terminalId)
- recordPass(activities, '轮换后新凭据可用', newSecretHeartbeat, terminalId)
- await captureTerminalCard(page, testInfo, terminalName, '远控终端-12-页面轮换后新凭据在线', '在线')
-
- activeStage = '通过页面删除终端并确认资源不可访问'
- card = await openTerminalPage(page, terminalName)
- const deletedResponsePromise = page.waitForResponse((response) => response.request().method() === 'DELETE'
- && response.url().includes(`/api/v1/remote-terminals/${encodeURIComponent(terminalId)}`))
- await card.getByRole('button', { name: '删除', exact: true }).click()
- messageBox = page.locator('.el-message-box:visible').last()
- await expect(messageBox).toContainText(`确定删除远程终端“${terminalName}”吗`)
- await messageBox.getByRole('button', { name: '确认删除' }).click()
- const deletedResponse = await deletedResponsePromise
- const deleted = asRecord(await responseData(deletedResponse, 200, '删除远控终端'))
- expect(deleted.deleted).toBe(true)
- const absentDetail = await api.get(`/api/v1/remote-terminals/${encodeURIComponent(terminalId)}`, { headers })
- assertStatus(absentDetail, 404, '删除后终端详情必须不可访问')
- const absentCommand = await api.get(`/api/v1/remote-commands/${encodeURIComponent(commandId)}`, { headers })
- assertStatus(absentCommand, 404, '删除终端后关联指令详情必须不可访问')
- const absentListResponse = await api.get('/api/v1/remote-terminals', {
- headers,
- params: { page: '1', pageSize: '20', keyword: terminalName },
- })
- const absentList = asRecord(await responseData(absentListResponse, 200, '删除后检索远控终端'))
- expect(absentList.total).toBe(0)
- const deletedRuntimeHeartbeat = await api.post(`${runtimePath}/heartbeat`, {
- headers: rotatedHeaders,
- data: { runtimeVersion: 'APE2E-1.0' },
- })
- assertStatus(deletedRuntimeHeartbeat, 401, '删除后运行端凭据必须失效')
- terminalDeleted = true
- recordPass(activities, '页面删除终端并确认管理端无残留', deletedResponse, terminalId, '详情与关联指令均为 404,列表检索为 0')
- recordPass(activities, '删除后运行端凭据失效', deletedRuntimeHeartbeat, terminalId)
- await captureDeletedState(page, testInfo, terminalName)
- } catch (error) {
- activities.push({
- module: '远控终端运行协议',
- action: activeStage,
- result: 'FAIL',
- resourceId: terminal?.id,
- detail: '阶段断言未通过;敏感请求上下文未写入活动附件。',
- })
- throw error
- } finally {
- if (!terminalDeleted) await fallbackCleanup(api, headers, terminal, activities, cleanupErrors)
- await attachJson(testInfo, '远控终端运行协议-生命周期与清理', {
- runId,
- runPrefix,
- apiBase: '本机正式 API(地址已省略)',
- activities,
- cleanupComplete: cleanupErrors.length === 0,
- cleanupErrors,
- verifiedEventSequence: ['SENT', 'ACKNOWLEDGED', 'EXECUTING', 'SUCCEEDED'],
- managementSubmission: '创建、编辑、绑定智能体、音量/打断设置、停启、指令下发、凭据轮换与删除均由真实页面控件提交;API 仅承担后验和运行端协议。',
- security: '管理员密码、访问令牌、终端一次性凭据与请求头均未写入日志、活动附件或截图;一次性密钥警告框也纳入失败截图遮罩;Playwright trace/video 已禁用。',
- })
- await api.dispose()
- }
-
- expect(cleanupErrors, '远控终端测试数据必须清理干净').toEqual([])
- })
- })
|