Fix remote server switch reachability probe (#4578)

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Jinwoo Hong 2026-06-03 14:59:33 -04:00 committed by GitHub
parent aa8451b3e9
commit c5d3d595a4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 62 additions and 30 deletions

View File

@ -133,6 +133,14 @@ export function clearRuntimeCompatibilityCache(environmentId?: string | null): v
compatibleRuntimeEnvironments.clear()
}
export function markRuntimeEnvironmentCompatible(environmentId: string): void {
const trimmed = environmentId.trim()
if (!trimmed) {
return
}
rememberRuntimeEnvironmentCompatibility(trimmed, Promise.resolve())
}
export function clearRuntimeCompatibilityCacheForTests(): void {
clearRuntimeCompatibilityCache()
}

View File

@ -21,6 +21,7 @@ vi.mock('@/lib/agent-status', async (importOriginal) => {
})
const runtimeEnvironmentCall = vi.fn()
const runtimeEnvironmentGetStatus = vi.fn()
const settingsSet = vi.fn().mockResolvedValue(undefined)
const env2Lineage: WorktreeLineage = {
@ -37,6 +38,17 @@ beforeEach(() => {
delete (globalThis as { __ORCA_WEB_CLIENT__?: boolean }).__ORCA_WEB_CLIENT__
clearRuntimeCompatibilityCacheForTests()
vi.clearAllMocks()
runtimeEnvironmentGetStatus.mockResolvedValue({
id: 'status-rpc-1',
ok: true,
result: {
runtimeId: 'runtime-2',
graphStatus: 'ready',
runtimeProtocolVersion: RUNTIME_PROTOCOL_VERSION,
minCompatibleRuntimeClientVersion: MIN_COMPATIBLE_RUNTIME_CLIENT_VERSION
},
_meta: { runtimeId: 'runtime-2' }
})
runtimeEnvironmentCall.mockImplementation(({ method }: { method: string }) => {
const result =
method === 'status.get'
@ -100,7 +112,7 @@ beforeEach(() => {
vi.stubGlobal('window', {
api: {
settings: { set: settingsSet },
runtimeEnvironments: { call: runtimeEnvironmentCall }
runtimeEnvironments: { call: runtimeEnvironmentCall, getStatus: runtimeEnvironmentGetStatus }
}
})
})
@ -212,7 +224,11 @@ describe('createSettingsSlice runtime switching', () => {
await expect(store.getState().switchRuntimeEnvironment('env-2')).resolves.toBe(true)
expect(settingsSet).toHaveBeenCalledWith({ activeRuntimeEnvironmentId: 'env-2' })
expect(runtimeEnvironmentCall).toHaveBeenCalledWith(
expect(runtimeEnvironmentGetStatus).toHaveBeenCalledWith({
selector: 'env-2',
timeoutMs: 15_000
})
expect(runtimeEnvironmentCall).not.toHaveBeenCalledWith(
expect.objectContaining({ selector: 'env-2', method: 'status.get' })
)
expect(runtimeEnvironmentCall).toHaveBeenCalledWith(
@ -348,7 +364,7 @@ describe('createSettingsSlice runtime switching', () => {
})
it('keeps the current environment when the selected remote server is unreachable', async () => {
runtimeEnvironmentCall.mockRejectedValueOnce(
runtimeEnvironmentGetStatus.mockRejectedValueOnce(
new Error('Remote Orca runtime closed the connection.')
)
const store = createTestStore()
@ -362,9 +378,11 @@ describe('createSettingsSlice runtime switching', () => {
await expect(store.getState().switchRuntimeEnvironment('env-2')).resolves.toBe(false)
expect(settingsSet).not.toHaveBeenCalled()
expect(runtimeEnvironmentCall).toHaveBeenCalledWith(
expect.objectContaining({ selector: 'env-2', method: 'status.get' })
)
expect(runtimeEnvironmentGetStatus).toHaveBeenCalledWith({
selector: 'env-2',
timeoutMs: 15_000
})
expect(runtimeEnvironmentCall).not.toHaveBeenCalled()
expect(runtimeEnvironmentCall).not.toHaveBeenCalledWith(
expect.objectContaining({ selector: 'env-1', method: 'terminal.close' })
)
@ -377,22 +395,16 @@ describe('createSettingsSlice runtime switching', () => {
})
it('keeps the current environment when the selected server is protocol-incompatible', async () => {
runtimeEnvironmentCall.mockImplementation(({ method }: { method: string }) => {
const result =
method === 'status.get'
? {
runtimeId: 'runtime-old',
graphStatus: 'ready',
runtimeProtocolVersion: MIN_COMPATIBLE_RUNTIME_SERVER_VERSION - 1,
minCompatibleRuntimeClientVersion: 0
}
: {}
return Promise.resolve({
id: 'rpc-1',
ok: true,
result,
_meta: { runtimeId: 'runtime-old' }
})
runtimeEnvironmentGetStatus.mockResolvedValueOnce({
id: 'status-rpc-old',
ok: true,
result: {
runtimeId: 'runtime-old',
graphStatus: 'ready',
runtimeProtocolVersion: MIN_COMPATIBLE_RUNTIME_SERVER_VERSION - 1,
minCompatibleRuntimeClientVersion: 0
},
_meta: { runtimeId: 'runtime-old' }
})
const store = createTestStore()
store.setState({
@ -404,12 +416,11 @@ describe('createSettingsSlice runtime switching', () => {
await expect(store.getState().switchRuntimeEnvironment('env-old')).resolves.toBe(false)
expect(settingsSet).not.toHaveBeenCalled()
expect(runtimeEnvironmentCall).toHaveBeenCalledWith(
expect.objectContaining({ selector: 'env-old', method: 'status.get' })
)
expect(runtimeEnvironmentCall).not.toHaveBeenCalledWith(
expect.objectContaining({ selector: 'env-old', method: 'repo.list' })
)
expect(runtimeEnvironmentGetStatus).toHaveBeenCalledWith({
selector: 'env-old',
timeoutMs: 15_000
})
expect(runtimeEnvironmentCall).not.toHaveBeenCalled()
expect(toast.error).toHaveBeenCalledWith('Failed to switch servers', {
description: expect.stringContaining('server is too old')
})

View File

@ -3,11 +3,18 @@ import type { StateCreator } from 'zustand'
import type { AppState } from '../types'
import type { GlobalSettings } from '../../../../shared/types'
import { toast } from 'sonner'
import { callRuntimeRpc, clearRuntimeCompatibilityCache } from '@/runtime/runtime-rpc-client'
import {
callRuntimeRpc,
clearRuntimeCompatibilityCache,
markRuntimeEnvironmentCompatible,
unwrapRuntimeRpcResult
} from '@/runtime/runtime-rpc-client'
import { assertRuntimeStatusCompatible } from '@/runtime/runtime-protocol-compat'
import {
getRemoteRuntimePtyEnvironmentId,
getRemoteRuntimeTerminalHandle
} from '@/runtime/runtime-terminal-stream'
import type { RuntimeStatus } from '../../../../shared/runtime-types'
import { normalizeTerminalQuickCommands } from '../../../../shared/terminal-quick-commands'
import { normalizeTaskProviderSettings } from '../../../../shared/task-providers'
import { normalizeOpenInApplications } from '../../../../shared/open-in-applications'
@ -236,9 +243,15 @@ async function verifyRuntimeEnvironmentReachable(environmentId: string | null):
if (!environmentId) {
return
}
await callRuntimeRpc({ kind: 'environment', environmentId }, 'repo.list', undefined, {
const response = await window.api.runtimeEnvironments.getStatus({
selector: environmentId,
timeoutMs: 15_000
})
const status = unwrapRuntimeRpcResult<RuntimeStatus>(response)
assertRuntimeStatusCompatible(status)
// Why: the switch probe already proved compatibility; avoid immediately
// re-probing through the heavier generic runtime RPC path during hydration.
markRuntimeEnvironmentCompatible(environmentId)
}
export const createSettingsSlice: StateCreator<AppState, [], [], SettingsSlice> = (set, get) => ({