|
- import type { Page, Request } from '@playwright/test'
-
- const RUNTIME_INSTANCE_ID = /^[A-Za-z0-9._:-]{16,128}$/
-
- type RuntimeEndpoint = 'heartbeat' | 'pull' | 'report' | 'release'
-
- export interface RuntimeTransportMonitor {
- heartbeatRequests: () => number
- heartbeatSuccesses: () => number
- pullRequests: () => number
- reportRequests: () => number
- releaseRequests: () => number
- releaseStatuses: () => number[]
- runtimeInstanceIds: () => string[]
- lastRuntimeInstanceId: () => string
- endpointInstances: () => Record<RuntimeEndpoint, string[]>
- headersValid: () => boolean
- pullStartedAfterHeartbeat: () => boolean
- runtimeInstanceOmittedFromUrls: () => boolean
- releaseBodyEmpty: () => boolean
- }
-
- function endpointFor(request: Request): RuntimeEndpoint | null {
- const pathname = new URL(request.url()).pathname
- if (request.method() !== 'POST') return null
- if (pathname === '/open/v2/terminals/heartbeat') return 'heartbeat'
- if (pathname === '/open/v2/terminals/commands/pull') return 'pull'
- if (/^\/open\/v2\/terminals\/commands\/[^/]+\/reports$/.test(pathname)) return 'report'
- if (pathname === '/open/v2/terminals/runtime/release') return 'release'
- return null
- }
-
- /**
- * 只记录运行租约契约的计数与非敏感 instanceId;设备密钥只检查“存在”,绝不读取值。
- * 每次主文档导航都允许生成新的 instanceId,但同一文档内四个运行端点必须完全一致。
- */
- export function observeRuntimeTransport(
- page: Page,
- terminalCode: string,
- trustedPageOrigin: string,
- ): RuntimeTransportMonitor {
- const counts: Record<RuntimeEndpoint, number> = { heartbeat: 0, pull: 0, report: 0, release: 0 }
- const instances: Record<RuntimeEndpoint, string[]> = { heartbeat: [], pull: [], report: [], release: [] }
- const successfulHeartbeatInstances = new Set<string>()
- const releaseResponseStatuses: number[] = []
- let activeDocumentInstance = ''
- let headerContractValid = true
- let pullGateValid = true
- let instanceUrlsValid = true
- let releasesHaveNoBody = true
-
- page.on('framenavigated', (frame) => {
- if (frame === page.mainFrame()) activeDocumentInstance = ''
- })
- page.on('request', (request) => {
- const endpoint = endpointFor(request)
- if (!endpoint) return
- counts[endpoint] += 1
- const headers = request.headers()
- const instanceId = headers['x-runtime-instance-id'] || ''
- const safeInstance = RUNTIME_INSTANCE_ID.test(instanceId)
- headerContractValid = headerContractValid
- && headers['x-terminal-id'] === terminalCode
- && Boolean(headers['x-device-secret'])
- && headers['x-terminal-page-origin'] === trustedPageOrigin
- && safeInstance
- && !headers['x-terminal-secret']
- if (safeInstance) {
- instances[endpoint].push(instanceId)
- if (!activeDocumentInstance) activeDocumentInstance = instanceId
- else headerContractValid = headerContractValid && activeDocumentInstance === instanceId
- instanceUrlsValid = instanceUrlsValid && !request.url().includes(instanceId)
- }
- if (endpoint === 'pull' && !successfulHeartbeatInstances.has(instanceId)) pullGateValid = false
- if (endpoint === 'release') releasesHaveNoBody = releasesHaveNoBody && !request.postData()
- })
- page.on('response', (response) => {
- const endpoint = endpointFor(response.request())
- if (!endpoint) return
- const instanceId = response.request().headers()['x-runtime-instance-id'] || ''
- if (endpoint === 'heartbeat' && response.status() === 200 && RUNTIME_INSTANCE_ID.test(instanceId)) {
- successfulHeartbeatInstances.add(instanceId)
- }
- if (endpoint === 'release') releaseResponseStatuses.push(response.status())
- })
-
- return {
- heartbeatRequests: () => counts.heartbeat,
- heartbeatSuccesses: () => successfulHeartbeatInstances.size,
- pullRequests: () => counts.pull,
- reportRequests: () => counts.report,
- releaseRequests: () => counts.release,
- releaseStatuses: () => [...releaseResponseStatuses],
- runtimeInstanceIds: () => [...new Set(Object.values(instances).flat())],
- lastRuntimeInstanceId: () => [...instances.heartbeat].at(-1) || '',
- endpointInstances: () => ({
- heartbeat: [...instances.heartbeat],
- pull: [...instances.pull],
- report: [...instances.report],
- release: [...instances.release],
- }),
- headersValid: () => headerContractValid,
- pullStartedAfterHeartbeat: () => pullGateValid,
- runtimeInstanceOmittedFromUrls: () => instanceUrlsValid,
- releaseBodyEmpty: () => releasesHaveNoBody,
- }
- }
-
- /** 返回布尔值而不是 storage 内容,避免设备凭据或租约 owner 进入测试进程与附件。 */
- export async function runtimeInstancesAbsentFromBrowserState(page: Page, instanceIds: string[]) {
- return page.evaluate((values) => {
- const locations = [window.location.href]
- for (const storage of [window.localStorage, window.sessionStorage]) {
- for (let index = 0; index < storage.length; index += 1) {
- const key = storage.key(index) || ''
- locations.push(key, storage.getItem(key) || '')
- }
- }
- return values.every((value) => value && locations.every((entry) => !entry.includes(value)))
- }, instanceIds)
- }
|