Просмотр исходного кода

test: 收口远程终端V2最终验收

main
leiyun 3 недель назад
Родитель
Сommit
2c67faee71
2 измененных файлов: 38 добавлений и 13 удалений
  1. +6
    -0
      tests/remote-terminal-v2-lifecycle.spec.ts
  2. +32
    -13
      tests/remote-terminal-v2-ui.spec.ts

+ 6
- 0
tests/remote-terminal-v2-lifecycle.spec.ts Просмотреть файл

@@ -493,6 +493,7 @@ async function directRuntimeSessionFence(
{ message: '旧 owner 会话必须被服务端拒绝' }, { message: '旧 owner 会话必须被服务端拒绝' },
ownerToken, ownerToken,
) )
socket.send(JSON.stringify({ type: 'ping' }))
const oldOwnerWebSocketCloseCode = await socketClosed const oldOwnerWebSocketCloseCode = await socketClosed


const contenderSession = await post(sessionPath, contenderInstance, {}) const contenderSession = await post(sessionPath, contenderInstance, {})
@@ -1171,6 +1172,7 @@ test.describe('远程终端 V2 真实双浏览器生命周期', () => {
} }
} }
primaryRuntime.page.on('response', restartResponseObserver) primaryRuntime.page.on('response', restartResponseObserver)
const releasesBeforeRestart = primaryRuntime.transport.releaseRequests()
const heartbeatAfterRestart = primaryRuntime.page.waitForResponse((response) => response.request().method() === 'POST' const heartbeatAfterRestart = primaryRuntime.page.waitForResponse((response) => response.request().method() === 'POST'
&& new URL(response.url()).pathname === '/open/v2/terminals/heartbeat' && new URL(response.url()).pathname === '/open/v2/terminals/heartbeat'
&& response.status() === 200, { timeout: 45_000 }) && response.status() === 200, { timeout: 45_000 })
@@ -1184,6 +1186,10 @@ test.describe('远程终端 V2 真实双浏览器生命周期', () => {
const restart = await parseCommandResponse(restartResponse, '下发 RESTART 命令') const restart = await parseCommandResponse(restartResponse, '下发 RESTART 命令')
commandIds.restart = restart.id commandIds.restart = restart.id
await waitCommand(api, headers, restart.id, 'SUCCEEDED', 60_000) await waitCommand(api, headers, restart.id, 'SUCCEEDED', 60_000)
await expect.poll(primaryRuntime.transport.releaseRequests, {
message: 'RESTART 刷新前必须立即释放旧 runtime lease,不能等待租约自然过期',
timeout: 10_000,
}).toBeGreaterThan(releasesBeforeRestart)
responseStatus(await heartbeatAfterRestart, 200, 'RESTART 后新页面运行实例重新取得租约') responseStatus(await heartbeatAfterRestart, 200, 'RESTART 后新页面运行实例重新取得租约')
await expect.poll(() => primaryRuntime!.terminalFollowupRequests().end, { await expect.poll(() => primaryRuntime!.terminalFollowupRequests().end, {
message: 'RESTART 必须调用受管会话 end', message: 'RESTART 必须调用受管会话 end',


+ 32
- 13
tests/remote-terminal-v2-ui.spec.ts Просмотреть файл

@@ -422,6 +422,7 @@ test.describe('远程终端 V2 受控页面回归', () => {
let pullCount = 0 let pullCount = 0
let sessionCreates = 0 let sessionCreates = 0
let terminalConfigRequests = 0 let terminalConfigRequests = 0
let terminalConfigFailures = 0
let activationRequests = 0 let activationRequests = 0
let activationRequestValid = true let activationRequestValid = true
let legacyPublicConfigRequests = 0 let legacyPublicConfigRequests = 0
@@ -587,6 +588,10 @@ test.describe('远程终端 V2 受控页面回归', () => {
if (pathname === `/open/v2/terminals/agents/${publishedAgent.slug}/config` && request.method() === 'GET') { if (pathname === `/open/v2/terminals/agents/${publishedAgent.slug}/config` && request.method() === 'GET') {
terminalConfigRequests += 1 terminalConfigRequests += 1
terminalConfigHeadersValid = terminalConfigHeadersValid && configHeadersValid terminalConfigHeadersValid = terminalConfigHeadersValid && configHeadersValid
if (terminalConfigFailures === 0) {
terminalConfigFailures += 1
return fulfill(route, {}, 503, { 'Cache-Control': 'no-store' })
}
return fulfill(route, { return fulfill(route, {
terminal: { id: 'rt-reliability', displayMode: 'WIDGET' }, terminal: { id: 'rt-reliability', displayMode: 'WIDGET' },
agent: { agent: {
@@ -756,7 +761,8 @@ test.describe('远程终端 V2 受控页面回归', () => {
await expect(widget.locator('.embed-widget')).toHaveClass(/is-expanded/, { timeout: 20_000 }) await expect(widget.locator('.embed-widget')).toHaveClass(/is-expanded/, { timeout: 20_000 })


expect(sessionCreates, 'WAKE 页面业务动作必须只执行一次').toBe(1) expect(sessionCreates, 'WAKE 页面业务动作必须只执行一次').toBe(1)
expect(terminalConfigRequests, '受管终端必须通过设备凭据读取一次智能体配置').toBe(1)
expect(terminalConfigFailures, '受管配置首次临时失败必须被测试覆盖').toBe(1)
expect(terminalConfigRequests, '受管终端配置首次 503 后必须自动重试成功').toBe(2)
expect(terminalConfigHeadersValid, '终端配置请求必须携带设备头和页面 Origin').toBe(true) expect(terminalConfigHeadersValid, '终端配置请求必须携带设备头和页面 Origin').toBe(true)
expect(terminalSessionHeadersValid, '终端会话请求必须携带设备头和页面 Origin').toBe(true) expect(terminalSessionHeadersValid, '终端会话请求必须携带设备头和页面 Origin').toBe(true)
expect(legacyPublicConfigRequests, '带 terminal 参数时不得调用旧公开配置接口').toBe(0) expect(legacyPublicConfigRequests, '带 terminal 参数时不得调用旧公开配置接口').toBe(0)
@@ -805,7 +811,11 @@ test.describe('远程终端 V2 受控页面回归', () => {
expect(sessionCreates, 'STANDBY 页面不得签发 scoped session').toBe(sessionsBeforeStandby) expect(sessionCreates, 'STANDBY 页面不得签发 scoped session').toBe(sessionsBeforeStandby)
expect(terminalChatRequests, 'STANDBY 页面不得发送 scoped chat').toBe(chatsBeforeStandby) expect(terminalChatRequests, 'STANDBY 页面不得发送 scoped chat').toBe(chatsBeforeStandby)
expect(runtimeTransport.heartbeatRequests(), '浏览器非 leader 不得竞争 heartbeat').toBe(heartbeatBeforeStandby) expect(runtimeTransport.heartbeatRequests(), '浏览器非 leader 不得竞争 heartbeat').toBe(heartbeatBeforeStandby)
expect(runtimeTransport.pullRequests(), '浏览器非 leader 不得启动 pull').toBe(pullsBeforeStandby)
expect(
[...new Set(runtimeTransport.endpointInstances().pull)],
'浏览器非 leader 不得以第二个 runtime instance 启动 pull',
).toEqual([leaderRuntimeInstanceId])
expect(runtimeTransport.pullRequests(), '主 leader 在校验期间应继续正常长轮询').toBeGreaterThanOrEqual(pullsBeforeStandby)
standbyInteractionBlocked = true standbyInteractionBlocked = true
await page.locator('#terminal-widget-standby').evaluate((element) => element.remove()) await page.locator('#terminal-widget-standby').evaluate((element) => element.remove())


@@ -832,6 +842,16 @@ test.describe('远程终端 V2 受控页面回归', () => {
const configRequestsBeforeMismatch = terminalConfigRequests const configRequestsBeforeMismatch = terminalConfigRequests
const heartbeatRequestsBeforeMismatch = runtimeTransport.heartbeatRequests() const heartbeatRequestsBeforeMismatch = runtimeTransport.heartbeatRequests()
const activationRequestsBeforeMismatch = activationRequests 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()) await page.goto(new URL(mismatchHostPath, mismatchHostOrigin).toString())
const mismatchWidget = page.frameLocator('#terminal-widget-mismatch') const mismatchWidget = page.frameLocator('#terminal-widget-mismatch')
await expect(mismatchWidget.locator('.embed-widget')).toBeVisible({ timeout: 20_000 }) await expect(mismatchWidget.locator('.embed-widget')).toBeVisible({ timeout: 20_000 })
@@ -851,13 +871,17 @@ test.describe('远程终端 V2 受控页面回归', () => {
Boolean(window.localStorage.getItem('ai-person:remote-terminal-runtime:v2:RTC-RELIABILITY-01')) Boolean(window.localStorage.getItem('ai-person:remote-terminal-runtime:v2:RTC-RELIABILITY-01'))
)) ))
expect(originMismatchCredentialPreserved, '来源不匹配只拒绝本次连接,不得删除合法宿主激活的设备凭据').toBe(true) expect(originMismatchCredentialPreserved, '来源不匹配只拒绝本次连接,不得删除合法宿主激活的设备凭据').toBe(true)

const heartbeatSuccessesBeforeTrustedReturn = runtimeTransport.heartbeatSuccesses()
// 此处只验证 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 page.goto(hostPath)
await expect.poll(runtimeTransport.heartbeatSuccesses, {
message: '返回合法宿主后必须复用原设备凭据重新取得 runtime lease',
timeout: 20_000,
}).toBeGreaterThan(heartbeatSuccessesBeforeTrustedReturn)
await heartbeatAfterTrustedReturn
await expect.poll(() => terminalConfigRequests, { await expect.poll(() => terminalConfigRequests, {
message: '返回合法宿主后必须重新读取 scoped config', message: '返回合法宿主后必须重新读取 scoped config',
timeout: 20_000, timeout: 20_000,
@@ -869,17 +893,12 @@ test.describe('远程终端 V2 受控页面回归', () => {
expect(activationRequests, '来源切换过程不得重复兑换一次性激活码').toBe(activationRequestsBeforeMismatch) expect(activationRequests, '来源切换过程不得重复兑换一次性激活码').toBe(activationRequestsBeforeMismatch)


const releasesBeforeLeavingHost = runtimeTransport.releaseRequests() const releasesBeforeLeavingHost = runtimeTransport.releaseRequests()
const releaseResponsesBeforeLeavingHost = releaseResponses
ordinaryPublicPhase = true ordinaryPublicPhase = true
await page.goto(`/live/${publishedAgent.slug}`) await page.goto(`/live/${publishedAgent.slug}`)
await expect.poll(runtimeTransport.releaseRequests, { await expect.poll(runtimeTransport.releaseRequests, {
message: '离开受管 WIDGET 时必须 best-effort 释放服务端运行租约', message: '离开受管 WIDGET 时必须 best-effort 释放服务端运行租约',
timeout: 15_000, timeout: 15_000,
}).toBeGreaterThan(releasesBeforeLeavingHost) }).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.releaseBodyEmpty(), 'release 必须是无 body 的 keepalive POST').toBe(true)
expect(runtimeTransport.headersValid(), 'release 必须复用同页运行实例和三项设备头且无旧头').toBe(true) expect(runtimeTransport.headersValid(), 'release 必须复用同页运行实例和三项设备头且无旧头').toBe(true)
await expect(page.locator('.live-agent-page')).toBeVisible() await expect(page.locator('.live-agent-page')).toBeVisible()


Загрузка…
Отмена
Сохранить