fix(runtime): show blocked hosts honestly and remove obsolete status options

This commit is contained in:
Jinwoo-H
2026-09-11 00:38:09 -04:00
parent 354922fec8
commit e283501d2e
9 changed files with 41 additions and 84 deletions
@@ -1,4 +1,4 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { beforeEach, describe, expect, it } from 'vitest'
import type { PairingOffer } from '../../shared/pairing'
import {
advanceRuntimeEnvironmentCapabilityIncarnation,
@@ -17,7 +17,6 @@ describe('runtime environment capability evidence', () => {
it('accepts evidence by dispatch order instead of completion order', () => {
const older = captureRuntimeEnvironmentCapabilityEvidence('env', pairing())
const newer = captureRuntimeEnvironmentCapabilityEvidence('env', pairing())
const pause = vi.fn()
expect(
applyRuntimeEnvironmentCapabilityVerdict({
@@ -30,12 +29,10 @@ describe('runtime environment capability evidence', () => {
applyRuntimeEnvironmentCapabilityVerdict({
evidence: older,
verdict: 'absent',
runtimeId: 'runtime-old',
onAbsent: pause
runtimeId: 'runtime-old'
})
).toBe(false)
expect(pause).not.toHaveBeenCalled()
expect(isRuntimeEnvironmentCapabilityPaused('env')).toBe(false)
})
@@ -68,8 +68,6 @@ export function applyRuntimeEnvironmentCapabilityVerdict(args: {
evidence: RuntimeEnvironmentCapabilityEvidence
verdict: RuntimeEnvironmentCapabilityVerdict
runtimeId: string
onCapable?: () => void
onAbsent?: () => void
}): boolean {
const state = stateFor(args.evidence.environmentId)
if (
@@ -83,11 +81,6 @@ export function applyRuntimeEnvironmentCapabilityVerdict(args: {
verdict: args.verdict,
runtimeId: args.runtimeId
}
if (args.verdict === 'capable') {
args.onCapable?.()
} else {
args.onAbsent?.()
}
return true
}
@@ -47,9 +47,9 @@ describe('runtime environment shared-control connection cache', () => {
applyRuntimeEnvironmentCapabilityVerdict({
evidence: absent,
verdict: 'absent',
runtimeId: 'runtime-test',
onAbsent: () => pauseRemoteRuntimeSharedControlRetry(ENVIRONMENT_ID)
runtimeId: 'runtime-test'
})
pauseRemoteRuntimeSharedControlRetry(ENVIRONMENT_ID)
expect(getRemoteRuntimeSharedControlDiagnostics(ENVIRONMENT_ID)?.state).toBe('closed')
await delay(400)
expect(server.connectionCount()).toBe(1)
@@ -58,12 +58,10 @@ describe('runtime environment shared-control connection cache', () => {
applyRuntimeEnvironmentCapabilityVerdict({
evidence: capable,
verdict: 'capable',
runtimeId: 'runtime-test',
onCapable: () => {
ensureRemoteRuntimeSharedControlConnection(ENVIRONMENT_ID, server.pairing)
reconnectRemoteRuntimeSharedControlConnection(ENVIRONMENT_ID)
}
runtimeId: 'runtime-test'
})
ensureRemoteRuntimeSharedControlConnection(ENVIRONMENT_ID, server.pairing)
reconnectRemoteRuntimeSharedControlConnection(ENVIRONMENT_ID)
await waitFor(() => server.connectionCount() === 2)
})
@@ -119,9 +117,9 @@ describe('runtime environment shared-control connection cache', () => {
applyRuntimeEnvironmentCapabilityVerdict({
evidence,
verdict: 'absent',
runtimeId: 'runtime-test',
onAbsent: () => pauseRemoteRuntimeSharedControlRetry(ENVIRONMENT_ID)
runtimeId: 'runtime-test'
})
pauseRemoteRuntimeSharedControlRetry(ENVIRONMENT_ID)
expect(getRemoteRuntimeSharedControlDiagnostics(ENVIRONMENT_ID)?.state).toBe('reconnecting')
await waitFor(() => server.connectionCount() === 2)
@@ -231,3 +231,20 @@ function remoteControl(
lastError: null
}
}
it('does not report reconnecting after verification is terminally blocked', () => {
expect(
runtimeHostConnectionStateForEntry({
status: null,
snapshot: {
environmentId: 'browser',
pairingRevision: 1,
sequence: 1,
checkedAt: 1,
status: null,
verification: 'blocked',
transport: 'disconnected'
}
})
).toBe('disconnected')
})
@@ -120,7 +120,7 @@ export function runtimeHostConnectionStateForEntry(
): RuntimeHostConnectionState {
if (entry?.snapshot) {
const snapshot = entry.snapshot
if (snapshot.retired) {
if (snapshot.retired || snapshot.verification === 'blocked') {
return 'disconnected'
}
if (snapshot.transport === 'disconnected') {
@@ -73,14 +73,10 @@ describe('restored client-hosted browser host attach on reachability', () => {
})
})
// The reconnect policy suppresses the *failure* publish only. A probe that answered still owes
// both recovery follow-ups, or a restored client-hosted page never comes back after the gap.
it('runs both recovery follow-ups on a success when the caller opted out of publishing failures', async () => {
it('runs both recovery follow-ups after a successful refresh', async () => {
stubApi(vi.fn().mockResolvedValue(createCompatibleRuntimeStatusResponse('runtime-a')))
await storeWithRestoredHandles(true)
.getState()
.refreshRuntimeEnvironmentStatus('env-a', undefined, { publishUnreachable: false })
await storeWithRestoredHandles(true).getState().refreshRuntimeEnvironmentStatus('env-a')
expect(prepareBrowserClientHostPlacement).toHaveBeenCalledWith({
selector: 'env-a',
@@ -89,24 +85,12 @@ describe('restored client-hosted browser host attach on reachability', () => {
expect(replayClientHostedBrowserCloseIntents).toHaveBeenCalledWith('env-a', expect.anything())
})
// Under either policy a failed probe owes *no* follow-ups: it verified nothing, so there is no
// recovered host to reattach restored pages to and no one to replay closes at.
it.each([
{ name: 'the default policy', options: undefined },
{ name: 'a caller that opted out of publishing', options: { publishUnreachable: false } }
])(
'starts no browser client host when the environment is unreachable: $name',
async (scenario) => {
stubApi(vi.fn().mockRejectedValue(new Error('unreachable')))
await storeWithRestoredHandles(true)
.getState()
.refreshRuntimeEnvironmentStatus('env-a', undefined, scenario.options)
expect(prepareBrowserClientHostPlacement).not.toHaveBeenCalled()
expect(replayClientHostedBrowserCloseIntents).not.toHaveBeenCalled()
}
)
it('runs no recovery follow-ups when the environment is unreachable', async () => {
stubApi(vi.fn().mockRejectedValue(new Error('unreachable')))
await storeWithRestoredHandles(true).getState().refreshRuntimeEnvironmentStatus('env-a')
expect(prepareBrowserClientHostPlacement).not.toHaveBeenCalled()
expect(replayClientHostedBrowserCloseIntents).not.toHaveBeenCalled()
})
it('starts no browser client host for restored pages the server hosts', async () => {
stubApi(vi.fn().mockResolvedValue(createCompatibleRuntimeStatusResponse('runtime-a')))
@@ -11,10 +11,6 @@ export type RuntimeEnvironmentStatus = {
connectionGeneration?: number
}
export type RuntimeStatusRefreshOptions = {
publishUnreachable?: boolean
}
export type RuntimeStatusSlice = {
readRuntimeHostStatusSnapshots: () => Promise<void>
applyRuntimeHostStatusSnapshot: (snapshot: RuntimeHostStatusSnapshot) => void
@@ -31,10 +27,6 @@ export type RuntimeStatusSlice = {
) => void
clearRuntimeEnvironmentStatus: (environmentId: string) => void
retainRuntimeEnvironmentStatuses: (environmentIds: Iterable<string>) => void
refreshRuntimeEnvironmentStatus: (
environmentId: string,
timeoutMs?: number,
options?: RuntimeStatusRefreshOptions
) => Promise<boolean>
refreshRuntimeEnvironmentStatus: (environmentId: string, timeoutMs?: number) => Promise<boolean>
hydrateRuntimeEnvironmentStatuses: () => Promise<void>
}
@@ -710,33 +710,17 @@ describe('runtime-status slice', () => {
clearRuntimeCompatibilityCacheForTests()
})
// Both directions of the failure-publication policy, from one failing probe. A user-initiated
// check publishes the outage it just observed; a caller holding live transport evidence must
// not, because status.get dials its own socket and its failure is unverifiable, not exited.
it.each([
{ name: 'a user-initiated check', options: undefined, publishes: true },
{ name: 'publishUnreachable defaulted', options: {}, publishes: true },
{
name: 'a caller that opted out of publishing',
options: { publishUnreachable: false },
publishes: false
}
])('records null and returns false when a runtime refresh fails: $name', async (scenario) => {
it('records null and returns false when a runtime refresh fails', async () => {
const getStatus = vi.fn().mockRejectedValue(new Error('closed'))
stubRuntimeEnvironmentApi({ getStatus })
const store = createSliceStore()
const cached = makeStatus()
store.getState().setRuntimeEnvironmentStatus('env-a', { status: cached, checkedAt: 1 })
const reachable = await store
.getState()
.refreshRuntimeEnvironmentStatus('env-a', undefined, scenario.options)
const reachable = await store.getState().refreshRuntimeEnvironmentStatus('env-a')
// The dial-answered contract the bridge's bounded retry chain reads is policy-independent.
expect(reachable).toBe(false)
expect(store.getState().runtimeStatusByEnvironmentId.get('env-a')?.status).toBe(
scenario.publishes ? null : cached
)
expect(store.getState().runtimeStatusByEnvironmentId.get('env-a')?.status).toBe(null)
})
it('hydrates saved environments through the single-environment refresh path', async () => {
@@ -1,11 +1,7 @@
import type { StateCreator } from 'zustand'
import type { AppState } from '../types'
import type { RuntimeStatusSlice } from './runtime-status-types'
export type {
RuntimeEnvironmentStatus,
RuntimeStatusRefreshOptions,
RuntimeStatusSlice
} from './runtime-status-types'
export type { RuntimeEnvironmentStatus, RuntimeStatusSlice } from './runtime-status-types'
import { runtimeEnvironmentStatusesEqual } from './runtime-environment-status-equality'
import {
clearRecentRuntimeCompatibilityFailure,
@@ -290,16 +286,12 @@ export const createRuntimeStatusSlice: StateCreator<AppState, [], [], RuntimeSta
})
},
refreshRuntimeEnvironmentStatus: (environmentId, timeoutMs = 10_000, options) =>
refreshRuntimeEnvironmentStatus: (environmentId, timeoutMs = 10_000) =>
refreshRuntimeEnvironmentStatus(environmentId, timeoutMs, (entry) => {
if (entry.snapshot) {
get().applyRuntimeHostStatusSnapshot(entry.snapshot)
return
}
if (entry.status === null && options?.publishUnreachable === false) {
// Unverifiable, not exited: leave the cached verdict for the caller's retry to settle.
return
}
// Why: setRuntimeEnvironmentStatus drops any stale compat failure on a non-null
// (reachable) status, so a recovered host's reuse-flagged refetches re-probe.
get().setRuntimeEnvironmentStatus(environmentId, entry)