Fix remote server switch reachability probe (#4578)
Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
parent
aa8451b3e9
commit
c5d3d595a4
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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) => ({
|
||||
|
|
|
|||
Loading…
Reference in New Issue