Files
orca/src/shared/runtime-host-status-owner.test.ts
T
Jinwoo HongandOmar Shahine 3b82d8de64 fix(runtime): let connections own host status recovery (#20003)
* fix(runtime): let connections own host status recovery

Verify runtime status after authenticated connection recovery and publish
ordered snapshots to desktop and browser viewers. Consolidate failed-status
retries in the connection owner and remove renderer retry/diagnostics merging.

Adapt sidebar host-state derivation and regression coverage from Omar
Shahine's original fix in https://github.com/stablyai/orca/pull/19163.

Co-authored-by: Omar Shahine <10343873+omarshahine@users.noreply.github.com>

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

* fix(runtime): preserve timeout guidance and update IPC test fixtures

* fix(runtime): preserve status evidence and address review gaps

* test(sidebar): assert workspace host icons dimming and recovery tooltips

* fix(palette): require available hosts before adding implicit badges

* fix: retain disconnected host snapshots for new renderers

---------

Co-authored-by: Omar Shahine <10343873+omarshahine@users.noreply.github.com>
2026-09-11 03:19:01 -04:00

208 lines
7.7 KiB
TypeScript

import { afterEach, beforeEach, expect, it, vi } from 'vitest'
import { RuntimeHostStatusOwner } from './runtime-host-status-owner'
import { runtimeHostStatusFailure, type RuntimeHostStatusResponse } from './runtime-host-status'
import type { RuntimeStatus } from './runtime-types'
const owners: RuntimeHostStatusOwner[] = []
beforeEach(() => vi.useFakeTimers())
afterEach(() => {
owners.splice(0).forEach((owner) => owner.dispose())
vi.useRealTimers()
})
function success(runtimeId = 'host-1'): RuntimeHostStatusResponse & { ok: true } {
return {
id: 'status',
ok: true,
result: { runtimeId, capabilities: [] } as unknown as RuntimeStatus,
_meta: { runtimeId }
}
}
function deferred() {
let resolve!: (response: RuntimeHostStatusResponse) => void
const promise = new Promise<RuntimeHostStatusResponse>((done) => {
resolve = done
})
return { promise, resolve }
}
function createOwner(persistent = false) {
const request = vi
.fn<(signal: AbortSignal) => Promise<RuntimeHostStatusResponse>>()
.mockResolvedValue(success())
const publish = vi.fn()
const verified = vi.fn((_response: RuntimeHostStatusResponse, _active: boolean) => persistent)
const owner = new RuntimeHostStatusOwner({
environmentId: 'env-a',
pairingRevision: 1,
persistent,
request,
publish,
verified
})
owners.push(owner)
return { owner, request, publish, verified }
}
it('shares one verification between viewers with independent deadlines', async () => {
const { owner, request } = createOwner()
const pending = deferred()
request.mockReturnValue(pending.promise)
const impatient = owner.refresh({ timeoutMs: 100 })
const patient = owner.refresh({ timeoutMs: 1_000 })
await vi.advanceTimersByTimeAsync(100)
expect((await impatient).ok).toBe(false)
expect(request).toHaveBeenCalledOnce()
expect(request.mock.calls[0][0].aborted).toBe(false)
pending.resolve(success())
expect((await patient).ok).toBe(true)
})
it('uses ready transitions, not diagnostic updates or a healthy polling timer', async () => {
const { owner, request } = createOwner(true)
await owner.refresh()
owner.connectionChanged('ready')
await vi.advanceTimersByTimeAsync(0)
owner.connectionChanged('ready')
await vi.advanceTimersByTimeAsync(300_000)
expect(request).toHaveBeenCalledTimes(2)
owner.connectionChanged('disconnected')
await vi.advanceTimersByTimeAsync(300_000)
expect(request).toHaveBeenCalledTimes(2)
owner.connectionChanged('ready')
await vi.advanceTimersByTimeAsync(0)
expect(request).toHaveBeenCalledTimes(3)
})
it('retries a failed status operation while retaining healthy transport and last good metadata', async () => {
const { owner, request } = createOwner(true)
owner.connectionChanged('ready')
await owner.refresh()
request.mockResolvedValueOnce(runtimeHostStatusFailure('runtime_unavailable', 'status timed out'))
await owner.refresh()
expect(owner.read()).toMatchObject({
transport: 'ready',
verification: 'unavailable',
status: { runtimeId: 'host-1' }
})
await vi.advanceTimersByTimeAsync(3_000)
expect(owner.read().verification).toBe('verified')
expect(request).toHaveBeenCalledTimes(3)
})
it('retires a lost-socket request before explicit fallback and rejects its late result', async () => {
const { owner, request } = createOwner(true)
owner.connectionChanged('ready')
const old = deferred()
request.mockReturnValueOnce(old.promise)
const waiting = owner.refresh()
owner.connectionChanged('disconnected')
expect(request.mock.calls[0][0].aborted).toBe(true)
request.mockResolvedValueOnce(success('fallback-host'))
expect((await owner.refresh()).ok).toBe(true)
expect((await waiting).ok).toBe(true)
old.resolve(success('obsolete-host'))
await vi.advanceTimersByTimeAsync(0)
expect(owner.read().status?.runtimeId).toBe('fallback-host')
owner.connectionChanged('ready')
await vi.advanceTimersByTimeAsync(0)
expect(request).toHaveBeenCalledTimes(3)
})
it('a reconnect transfers waiting readers to a fresh verification', async () => {
const { owner, request } = createOwner(true)
owner.connectionChanged('ready')
const old = deferred()
request.mockReturnValueOnce(old.promise)
const waiting = owner.refresh()
owner.connectionChanged('disconnected')
owner.connectionChanged('ready')
expect((await waiting).ok).toBe(true)
old.resolve(success('old'))
await vi.advanceTimersByTimeAsync(0)
expect(owner.read().status?.runtimeId).toBe('host-1')
})
it('disconnect settles readers and prevents late results and retry resurrection', async () => {
const { owner, request, publish } = createOwner()
const old = deferred()
request.mockReturnValue(old.promise)
const waiting = owner.refresh()
owner.dispose()
expect((await waiting).ok).toBe(false)
const sequence = owner.read().sequence
old.resolve(success())
owner.connectionChanged('ready')
await vi.advanceTimersByTimeAsync(300_000)
expect(owner.read()).toMatchObject({ retired: true, sequence })
expect(publish.mock.lastCall?.[0].retired).toBe(true)
expect(request).toHaveBeenCalledOnce()
})
it('passive reads create neither standing retries nor connection intent', async () => {
const { owner, request, verified } = createOwner()
request.mockResolvedValueOnce(runtimeHostStatusFailure('runtime_unavailable', 'offline'))
await owner.refresh({ observeOnly: true })
await vi.advanceTimersByTimeAsync(300_000)
expect(request).toHaveBeenCalledOnce()
await owner.refresh({ observeOnly: true })
expect(verified.mock.lastCall?.[1]).toBe(false)
})
it('authentication rejection blocks automatic verification until explicit reconnect', async () => {
const { owner, request } = createOwner(true)
owner.connectionChanged('ready')
request.mockResolvedValueOnce(runtimeHostStatusFailure('unauthorized', 're-pair'))
await owner.refresh()
owner.connectionChanged('disconnected')
owner.connectionChanged('ready')
await vi.advanceTimersByTimeAsync(300_000)
expect(request).toHaveBeenCalledOnce()
expect((await owner.refresh({ reconnect: true })).ok).toBe(true)
})
it('blocks a rejected reconnect even without an outstanding status request', async () => {
const { owner, request } = createOwner(true)
owner.connectionChanged('ready')
await owner.refresh()
owner.connectionChanged('disconnected')
owner.authenticationRejected()
expect(owner.read()).toMatchObject({ verification: 'blocked', status: { runtimeId: 'host-1' } })
owner.connectionChanged('ready')
await vi.advanceTimersByTimeAsync(300_000)
expect(request).toHaveBeenCalledOnce()
})
it('cancelling one reader leaves the shared request available to other readers', async () => {
const { owner, request } = createOwner()
const pending = deferred()
request.mockReturnValue(pending.promise)
const controller = new AbortController()
const cancelled = owner.refresh({ signal: controller.signal })
const remaining = owner.refresh()
const rejection = expect(cancelled).rejects.toThrow('cancelled')
controller.abort(new Error('cancelled'))
await rejection
expect(request.mock.calls[0][0].aborted).toBe(false)
pending.resolve(success())
expect((await remaining).ok).toBe(true)
})
it.each(['unknown', 'ready'] as const)(
'distinguishes the caller deadline with %s transport',
async (transport) => {
const { owner, request } = createOwner()
owner.connectionChanged(transport)
request.mockReturnValue(deferred().promise)
const response = owner.refresh({ timeoutMs: 100 })
await vi.advanceTimersByTimeAsync(100)
expect(await response).toMatchObject({
ok: false,
error: {
message:
transport === 'ready'
? 'Status request timed out.'
: 'Timed out waiting for the remote Orca runtime.'
}
})
}
)