From 2c67faee7107698d08fb48611fd7b53051b76dcc Mon Sep 17 00:00:00 2001 From: leiyun Date: Sun, 23 Aug 2026 23:56:27 +0800 Subject: [PATCH] =?UTF-8?q?test:=20=E6=94=B6=E5=8F=A3=E8=BF=9C=E7=A8=8B?= =?UTF-8?q?=E7=BB=88=E7=AB=AFV2=E6=9C=80=E7=BB=88=E9=AA=8C=E6=94=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/remote-terminal-v2-lifecycle.spec.ts | 6 +++ tests/remote-terminal-v2-ui.spec.ts | 45 +++++++++++++++------- 2 files changed, 38 insertions(+), 13 deletions(-) diff --git a/tests/remote-terminal-v2-lifecycle.spec.ts b/tests/remote-terminal-v2-lifecycle.spec.ts index fac9939..ebeeee6 100644 --- a/tests/remote-terminal-v2-lifecycle.spec.ts +++ b/tests/remote-terminal-v2-lifecycle.spec.ts @@ -493,6 +493,7 @@ async function directRuntimeSessionFence( { message: '旧 owner 会话必须被服务端拒绝' }, ownerToken, ) + socket.send(JSON.stringify({ type: 'ping' })) const oldOwnerWebSocketCloseCode = await socketClosed const contenderSession = await post(sessionPath, contenderInstance, {}) @@ -1171,6 +1172,7 @@ test.describe('远程终端 V2 真实双浏览器生命周期', () => { } } primaryRuntime.page.on('response', restartResponseObserver) + const releasesBeforeRestart = primaryRuntime.transport.releaseRequests() const heartbeatAfterRestart = primaryRuntime.page.waitForResponse((response) => response.request().method() === 'POST' && new URL(response.url()).pathname === '/open/v2/terminals/heartbeat' && response.status() === 200, { timeout: 45_000 }) @@ -1184,6 +1186,10 @@ test.describe('远程终端 V2 真实双浏览器生命周期', () => { const restart = await parseCommandResponse(restartResponse, '下发 RESTART 命令') commandIds.restart = restart.id 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 后新页面运行实例重新取得租约') await expect.poll(() => primaryRuntime!.terminalFollowupRequests().end, { message: 'RESTART 必须调用受管会话 end', diff --git a/tests/remote-terminal-v2-ui.spec.ts b/tests/remote-terminal-v2-ui.spec.ts index d8bac54..f30030d 100644 --- a/tests/remote-terminal-v2-ui.spec.ts +++ b/tests/remote-terminal-v2-ui.spec.ts @@ -422,6 +422,7 @@ test.describe('远程终端 V2 受控页面回归', () => { let pullCount = 0 let sessionCreates = 0 let terminalConfigRequests = 0 + let terminalConfigFailures = 0 let activationRequests = 0 let activationRequestValid = true let legacyPublicConfigRequests = 0 @@ -587,6 +588,10 @@ test.describe('远程终端 V2 受控页面回归', () => { 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: { @@ -756,7 +761,8 @@ test.describe('远程终端 V2 受控页面回归', () => { await expect(widget.locator('.embed-widget')).toHaveClass(/is-expanded/, { timeout: 20_000 }) expect(sessionCreates, 'WAKE 页面业务动作必须只执行一次').toBe(1) - expect(terminalConfigRequests, '受管终端必须通过设备凭据读取一次智能体配置').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) @@ -805,7 +811,11 @@ test.describe('远程终端 V2 受控页面回归', () => { 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) + 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()) @@ -832,6 +842,16 @@ test.describe('远程终端 V2 受控页面回归', () => { 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 }) @@ -851,13 +871,17 @@ test.describe('远程终端 V2 受控页面回归', () => { Boolean(window.localStorage.getItem('ai-person:remote-terminal-runtime:v2:RTC-RELIABILITY-01')) )) 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 expect.poll(runtimeTransport.heartbeatSuccesses, { - message: '返回合法宿主后必须复用原设备凭据重新取得 runtime lease', - timeout: 20_000, - }).toBeGreaterThan(heartbeatSuccessesBeforeTrustedReturn) + await heartbeatAfterTrustedReturn await expect.poll(() => terminalConfigRequests, { message: '返回合法宿主后必须重新读取 scoped config', timeout: 20_000, @@ -869,17 +893,12 @@ test.describe('远程终端 V2 受控页面回归', () => { 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()