diff --git a/src/main/ipc/runtime-environment-capability-evidence.test.ts b/src/main/ipc/runtime-environment-capability-evidence.test.ts index 4671326ef02..8c9bce5f272 100644 --- a/src/main/ipc/runtime-environment-capability-evidence.test.ts +++ b/src/main/ipc/runtime-environment-capability-evidence.test.ts @@ -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) }) diff --git a/src/main/ipc/runtime-environment-capability-evidence.ts b/src/main/ipc/runtime-environment-capability-evidence.ts index d32bda584e9..197ec71f4f8 100644 --- a/src/main/ipc/runtime-environment-capability-evidence.ts +++ b/src/main/ipc/runtime-environment-capability-evidence.ts @@ -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 } diff --git a/src/main/ipc/runtime-environment-request-connections.test.ts b/src/main/ipc/runtime-environment-request-connections.test.ts index 750d1becc6a..b02d1b06fb5 100644 --- a/src/main/ipc/runtime-environment-request-connections.test.ts +++ b/src/main/ipc/runtime-environment-request-connections.test.ts @@ -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) diff --git a/src/renderer/src/runtime/runtime-host-connection-state.test.ts b/src/renderer/src/runtime/runtime-host-connection-state.test.ts index 2e739feb9f2..30ee06a5e98 100644 --- a/src/renderer/src/runtime/runtime-host-connection-state.test.ts +++ b/src/renderer/src/runtime/runtime-host-connection-state.test.ts @@ -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') +}) diff --git a/src/renderer/src/runtime/runtime-host-connection-state.ts b/src/renderer/src/runtime/runtime-host-connection-state.ts index ecad165dcdd..c10477415a1 100644 --- a/src/renderer/src/runtime/runtime-host-connection-state.ts +++ b/src/renderer/src/runtime/runtime-host-connection-state.ts @@ -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') { diff --git a/src/renderer/src/store/slices/runtime-status-restored-browser-host-attach.test.ts b/src/renderer/src/store/slices/runtime-status-restored-browser-host-attach.test.ts index 0ba4d645026..d4b788cd49a 100644 --- a/src/renderer/src/store/slices/runtime-status-restored-browser-host-attach.test.ts +++ b/src/renderer/src/store/slices/runtime-status-restored-browser-host-attach.test.ts @@ -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'))) diff --git a/src/renderer/src/store/slices/runtime-status-types.ts b/src/renderer/src/store/slices/runtime-status-types.ts index bac5e105314..78e47123cf5 100644 --- a/src/renderer/src/store/slices/runtime-status-types.ts +++ b/src/renderer/src/store/slices/runtime-status-types.ts @@ -11,10 +11,6 @@ export type RuntimeEnvironmentStatus = { connectionGeneration?: number } -export type RuntimeStatusRefreshOptions = { - publishUnreachable?: boolean -} - export type RuntimeStatusSlice = { readRuntimeHostStatusSnapshots: () => Promise applyRuntimeHostStatusSnapshot: (snapshot: RuntimeHostStatusSnapshot) => void @@ -31,10 +27,6 @@ export type RuntimeStatusSlice = { ) => void clearRuntimeEnvironmentStatus: (environmentId: string) => void retainRuntimeEnvironmentStatuses: (environmentIds: Iterable) => void - refreshRuntimeEnvironmentStatus: ( - environmentId: string, - timeoutMs?: number, - options?: RuntimeStatusRefreshOptions - ) => Promise + refreshRuntimeEnvironmentStatus: (environmentId: string, timeoutMs?: number) => Promise hydrateRuntimeEnvironmentStatuses: () => Promise } diff --git a/src/renderer/src/store/slices/runtime-status.test.ts b/src/renderer/src/store/slices/runtime-status.test.ts index 8e3260957bf..6709eec4e80 100644 --- a/src/renderer/src/store/slices/runtime-status.test.ts +++ b/src/renderer/src/store/slices/runtime-status.test.ts @@ -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 () => { diff --git a/src/renderer/src/store/slices/runtime-status.ts b/src/renderer/src/store/slices/runtime-status.ts index fe8cc87a832..c11fba3308a 100644 --- a/src/renderer/src/store/slices/runtime-status.ts +++ b/src/renderer/src/store/slices/runtime-status.ts @@ -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 + 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)