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

This commit is contained in:
Jinwoo-H
2026-09-11 01:09:56 -04:00
parent 793328388d
commit 07686a289d
17 changed files with 159 additions and 75 deletions
@@ -142,8 +142,7 @@ export function registerRuntimeEnvironmentConnectivityHandlers({
): Promise<RuntimeRpcResponse<RuntimeStatus>> => {
const environment = resolveEnvironment(getUserDataPath(), args.selector)
clearRuntimeEnvironmentManualDisconnect(environment.id)
return getRuntimeEnvironmentStatusOwner(getUserDataPath(), environment.id).refresh({
timeoutMs: args.timeoutMs,
return getRuntimeEnvironmentStatus(getUserDataPath(), environment.id, args.timeoutMs, {
reconnect: true
})
}
@@ -1,3 +1,4 @@
import { resetRuntimeEnvironmentStatusOwners } from './runtime-environment-request-connections'
vi.mock('electron', () => ({ BrowserWindow: { getAllWindows: () => [] } }))
import { mkdtempSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
@@ -59,6 +60,7 @@ describe('federated read RPC transport routing', () => {
})
afterEach(() => {
resetRuntimeEnvironmentStatusOwners()
rmSync(userDataPath, { recursive: true, force: true })
})
@@ -39,10 +39,13 @@ it('publishes real same-socket verification after every authenticated reconnect'
})
await getRuntimeEnvironmentStatus(profile, environment.id)
const owner = getRuntimeEnvironmentStatusOwner(profile, environment.id)
await vi.waitFor(() => {
expect(owner.read()).toMatchObject({ transport: 'ready', verification: 'verified' })
expect(server.requests).toHaveLength(2)
})
await vi.waitFor(
() => {
expect(owner.read()).toMatchObject({ transport: 'ready', verification: 'verified' })
expect(server.requests).toHaveLength(2)
},
{ timeout: 3_000 }
)
expect(server.connectionCount()).toBe(2) // Bootstrap plus persistent control.
runtimeId = 'host-after'
server.closeClients()
@@ -39,7 +39,7 @@ export async function getRuntimeEnvironmentStatus(
userDataPath: string,
selector: string,
timeoutMs?: number,
options?: { observeOnly?: true; signal?: AbortSignal }
options?: { observeOnly?: true; signal?: AbortSignal; reconnect?: true }
): Promise<RuntimeRpcResponse<RuntimeStatus>> {
const environment = resolveEnvironment(userDataPath, selector)
if (isRuntimeEnvironmentManuallyDisconnected(environment.id)) {
@@ -1,3 +1,4 @@
import { resetRuntimeEnvironmentStatusOwners } from './runtime-environment-request-connections'
import { mkdtempSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
@@ -118,6 +119,7 @@ describe('registerRuntimeEnvironmentHandlers', () => {
})
afterEach(() => {
resetRuntimeEnvironmentStatusOwners()
rmSync(userDataPath, { recursive: true, force: true })
})
@@ -1,3 +1,4 @@
import { resetRuntimeEnvironmentStatusOwners } from './runtime-environment-request-connections'
import { mkdtempSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
@@ -111,6 +112,7 @@ describe('registerRuntimeEnvironmentHandlers', () => {
})
afterEach(() => {
resetRuntimeEnvironmentStatusOwners()
rmSync(userDataPath, { recursive: true, force: true })
})
@@ -1,3 +1,4 @@
import { resetRuntimeEnvironmentStatusOwners } from './runtime-environment-request-connections'
import { mkdtempSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
@@ -131,6 +132,7 @@ describe('registerRuntimeEnvironmentHandlers', () => {
})
afterEach(() => {
resetRuntimeEnvironmentStatusOwners()
rmSync(userDataPath, { recursive: true, force: true })
})
@@ -1,3 +1,4 @@
import { resetRuntimeEnvironmentStatusOwners } from './runtime-environment-request-connections'
import { mkdtempSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
@@ -120,6 +121,7 @@ describe('registerRuntimeEnvironmentHandlers', () => {
})
afterEach(() => {
resetRuntimeEnvironmentStatusOwners()
rmSync(userDataPath, { recursive: true, force: true })
})
@@ -325,36 +327,41 @@ describe('registerRuntimeEnvironmentHandlers', () => {
})
})
it('returns shared-control diagnostics when saved remote runtime status throws', async () => {
registerRuntimeEnvironmentHandlers(store as never)
getRemoteRuntimeSharedControlDiagnosticsMock.mockReturnValue({
state: 'reconnecting',
pendingRequestCount: 0,
subscriptionCount: 1,
reconnectAttempt: 2,
lastConnectedAt: 123,
lastClose: { code: 1006, reason: '' },
lastError: 'closed'
})
sendRemoteRuntimeRequestMock.mockRejectedValue(new Error('socket closed'))
it.each(['runtimeEnvironments:getStatus', 'runtimeEnvironments:connect'])(
'preserves failure diagnostics and guidance on %s',
async (channel) => {
registerRuntimeEnvironmentHandlers(store as never)
getRemoteRuntimeSharedControlDiagnosticsMock.mockReturnValue({
state: 'reconnecting',
pendingRequestCount: 0,
subscriptionCount: 1,
reconnectAttempt: 2,
lastConnectedAt: 123,
lastClose: { code: 1006, reason: '' },
lastError: 'closed'
})
sendRemoteRuntimeRequestMock.mockRejectedValue(
new Error('Could not connect to the remote Orca runtime.')
)
const add = handler<
{ name: string; pairingCode: string },
{ environment: { id: string; name: string } }
>('runtimeEnvironments:addFromPairingCode')
await add(null, { name: 'desk', pairingCode: pairingCode() })
const add = handler<
{ name: string; pairingCode: string },
{ environment: { id: string; name: string } }
>('runtimeEnvironments:addFromPairingCode')
await add(null, { name: 'desk', pairingCode: pairingCode() })
const getStatus = handler<
{ selector: string; timeoutMs?: number },
{ ok: false; error: { message: string; data?: { remoteControl?: { state: string } } } }
>('runtimeEnvironments:getStatus')
const getStatus = handler<
{ selector: string; timeoutMs?: number },
{ ok: false; error: { message: string; data?: { remoteControl?: { state: string } } } }
>(channel)
await expect(getStatus(null, { selector: 'desk' })).resolves.toMatchObject({
ok: false,
error: {
message: 'socket closed',
data: { remoteControl: { state: 'reconnecting' } }
}
})
})
await expect(getStatus(null, { selector: 'desk' })).resolves.toMatchObject({
ok: false,
error: {
message: expect.stringContaining('connect both devices to Tailscale'),
data: { remoteControl: { state: 'reconnecting' } }
}
})
}
)
})
@@ -1,3 +1,4 @@
import { resetRuntimeEnvironmentStatusOwners } from './runtime-environment-request-connections'
import { mkdtempSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
@@ -115,6 +116,7 @@ describe('registerRuntimeEnvironmentHandlers', () => {
})
afterEach(() => {
resetRuntimeEnvironmentStatusOwners()
rmSync(userDataPath, { recursive: true, force: true })
})
@@ -1,3 +1,4 @@
import { resetRuntimeEnvironmentStatusOwners } from './runtime-environment-request-connections'
import { mkdtempSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
@@ -114,6 +115,7 @@ describe('registerRuntimeEnvironmentHandlers', () => {
})
afterEach(() => {
resetRuntimeEnvironmentStatusOwners()
rmSync(userDataPath, { recursive: true, force: true })
})
@@ -1,3 +1,4 @@
import { resetRuntimeEnvironmentStatusOwners } from './runtime-environment-request-connections'
import { mkdtempSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
@@ -121,6 +122,7 @@ describe('registerRuntimeEnvironmentHandlers', () => {
})
afterEach(() => {
resetRuntimeEnvironmentStatusOwners()
rmSync(userDataPath, { recursive: true, force: true })
})
@@ -16,14 +16,18 @@ export async function refreshRuntimeEnvironmentStatus(
timeoutMs
})
if (window.api.runtimeEnvironments.getStatusSnapshots) {
const snapshots = await window.api.runtimeEnvironments.getStatusSnapshots()
const snapshot = snapshots.find((entry) => entry.environmentId === environmentId)
if (snapshot) {
publish({
snapshot,
status: snapshot.verification === 'verified' ? snapshot.status : null,
checkedAt: snapshot.checkedAt
})
try {
const snapshots = await window.api.runtimeEnvironments.getStatusSnapshots()
const snapshot = snapshots.find((entry) => entry.environmentId === environmentId)
if (snapshot) {
publish({
snapshot,
status: snapshot.verification === 'verified' ? snapshot.status : null,
checkedAt: snapshot.checkedAt
})
}
} catch (error) {
console.error('Failed to read runtime host status snapshot:', error)
}
return response.ok
}
@@ -723,6 +723,26 @@ describe('runtime-status slice', () => {
expect(store.getState().runtimeStatusByEnvironmentId.get('env-a')?.status).toBe(null)
})
it('preserves successful reachability when reading its snapshot fails', async () => {
const log = vi.spyOn(console, 'error').mockImplementation(() => {})
vi.stubGlobal('window', {
api: {
runtimeEnvironments: {
getStatus: vi.fn().mockResolvedValue(createCompatibleRuntimeStatusResponse('runtime-a')),
getStatusSnapshots: vi.fn().mockRejectedValue(new Error('IPC read failed'))
}
}
})
try {
const store = createSliceStore()
expect(await store.getState().refreshRuntimeEnvironmentStatus('env-a')).toBe(true)
expect(store.getState().runtimeStatusByEnvironmentId.has('env-a')).toBe(false)
expect(log).toHaveBeenCalled()
} finally {
log.mockRestore()
}
})
it('hydrates saved environments through the single-environment refresh path', async () => {
const getStatus = vi.fn().mockResolvedValue(createCompatibleRuntimeStatusResponse('runtime-a'))
const list = vi.fn().mockResolvedValue([
@@ -34,11 +34,15 @@ it('primary browser status follows the authenticated socket and closing it retir
status: { environmentId: 'browser', pairingRevision: 1, publish, verified: vi.fn() }
})
clients.push(client)
await expect.poll(() => client.statusOwner?.read().verification).toBe('verified')
await expect
.poll(() => client.statusOwner?.read().verification, { timeout: 3_000 })
.toBe('verified')
expect(client.statusOwner?.read().status?.runtimeId).toBe('before')
runtimeId = 'after'
server.closeClients()
await expect.poll(() => client.statusOwner?.read().status?.runtimeId).toBe('after')
await expect
.poll(() => client.statusOwner?.read().status?.runtimeId, { timeout: 3_000 })
.toBe('after')
expect(client.statusOwner?.read().transport).toBe('ready')
client.close()
expect(publish.mock.lastCall?.[0]).toMatchObject({ retired: true, verification: 'blocked' })
+28 -4
View File
@@ -320,17 +320,15 @@ describe('execution host registry', () => {
])
})
it('includes runtime hosts from repo ownership but marks them disconnected without live status', () => {
it('keeps runtime hosts checking before their first status result', () => {
const hosts = buildExecutionHostRegistry({
repos: [{ connectionId: null, executionHostId: 'runtime:env-2' }],
settings: { activeRuntimeEnvironmentId: null }
})
// No live status means no evidence the Orca server is reachable, so it must
// read 'disconnected' rather than defaulting to 'available'/"Connected".
expect(hosts).toMatchObject([
{ id: 'local', health: 'local' },
{ id: 'runtime:env-2', kind: 'runtime', label: 'env-2', health: 'disconnected' }
{ id: 'runtime:env-2', kind: 'runtime', label: 'env-2', health: 'connecting' }
])
})
@@ -373,3 +371,29 @@ describe('execution host registry', () => {
])
})
})
it('keeps an initial unknown-transport verification connecting', () => {
const hosts = buildExecutionHostRegistry({
repos: [],
settings: null,
runtimeEnvironments: [{ id: 'host', name: 'Host' }],
runtimeStatusByEnvironmentId: new Map([
[
'host',
{
status: null,
snapshot: {
environmentId: 'host',
pairingRevision: 1,
sequence: 1,
checkedAt: 0,
status: null,
verification: 'checking',
transport: 'unknown'
}
}
]
])
})
expect(hosts.find((host) => host.id === 'runtime:host')?.health).toBe('connecting')
})
+4 -1
View File
@@ -168,7 +168,10 @@ function addRuntimeHost(
? 'disconnected'
: snapshot?.verification === 'blocked'
? 'blocked'
: snapshot?.transport === 'disconnected' || snapshot?.transport === 'connecting'
: !runtimeStatus ||
snapshot?.verification === 'checking' ||
snapshot?.transport === 'disconnected' ||
snapshot?.transport === 'connecting'
? 'connecting'
: snapshot?.transport === 'ready'
? compatibility?.kind === 'blocked'
+30 -24
View File
@@ -82,21 +82,24 @@ for (const topology of ['desktop', 'headless'] as const) {
testRepoPath
}, testInfo) => {
test.setTimeout(180_000)
const headless =
topology === 'headless'
? await launchHeadlessPairedRuntimeHost({ pinnedServePort: true })
: null
const offer = headless?.offer ?? (await createRuntimeDesktopPairingOffer(page))
await (headless
? headless.client.call('repo.add', { path: testRepoPath })
: page.evaluate(async (path) => {
await window.api.repos.add({ path })
await window.__store?.getState().fetchRepos()
}, testRepoPath))
const proxy = await interruptibleHost(offer)
const client = await launchPairedElectronClient(offer, testInfo, 'Direct host')
let headless: Awaited<ReturnType<typeof launchHeadlessPairedRuntimeHost>> | null = null
let proxy: Awaited<ReturnType<typeof interruptibleHost>> | undefined
let client: Awaited<ReturnType<typeof launchPairedElectronClient>> | undefined
let browser: Awaited<ReturnType<typeof launchPairedWebClient>> | undefined
try {
headless =
topology === 'headless'
? await launchHeadlessPairedRuntimeHost({ pinnedServePort: true })
: null
const offer = headless?.offer ?? (await createRuntimeDesktopPairingOffer(page))
await (headless
? headless.client.call('repo.add', { path: testRepoPath })
: page.evaluate(async (path) => {
await window.api.repos.add({ path })
await window.__store?.getState().fetchRepos()
}, testRepoPath))
proxy = await interruptibleHost(offer)
client = await launchPairedElectronClient(offer, testInfo, 'Direct host')
proxy.setOnline(false)
const offlineId = await client.page.evaluate(async (pairingCode) => {
const { environment } = await window.api.runtimeEnvironments.addFromPairingCode({
@@ -109,16 +112,16 @@ for (const topology of ['desktop', 'headless'] as const) {
return environment.id
}, proxy.offer.pairingUrl)
await expect
.poll(() => statusEvidence(client.page, offlineId))
.poll(() => statusEvidence(client!.page, offlineId))
.toMatchObject({ verification: 'unavailable' })
expect(await statusEvidence(client.page, client.environmentId)).toMatchObject({
expect(await statusEvidence(client!.page, client.environmentId)).toMatchObject({
verification: 'verified'
})
proxy.setOnline(true)
await expect
.poll(() => statusEvidence(client.page, offlineId), { timeout: 30_000 })
.poll(() => statusEvidence(client!.page, offlineId), { timeout: 30_000 })
.toMatchObject({ verification: 'verified', transport: 'ready' })
const initial = await statusEvidence(client.page, offlineId)
const initial = await statusEvidence(client!.page, offlineId)
await expect(client.page.getByText('Recovering host', { exact: true }).first()).toBeVisible()
await client.page.screenshot({ path: testInfo.outputPath(`${topology}-recovered.png`) })
browser = await launchPairedWebClient(electronApp, proxy.offer)
@@ -127,20 +130,23 @@ for (const topology of ['desktop', 'headless'] as const) {
.toMatchObject({ verification: 'verified', transport: 'ready' })
proxy.setOnline(false)
await expect
.poll(() => statusEvidence(client.page, offlineId))
.poll(() => statusEvidence(client!.page, offlineId))
.toMatchObject({ transport: 'disconnected' })
expect(await statusEvidence(client.page, client.environmentId)).toMatchObject({
await expect
.poll(() => statusEvidence(browser!.page), { timeout: 30_000 })
.toMatchObject({ transport: 'disconnected' })
expect(await statusEvidence(client!.page, client.environmentId)).toMatchObject({
verification: 'verified',
transport: 'ready'
})
proxy.setOnline(true)
await expect
.poll(() => statusEvidence(client.page, offlineId), { timeout: 30_000 })
.poll(() => statusEvidence(client!.page, offlineId), { timeout: 30_000 })
.toMatchObject({ verification: 'verified', transport: 'ready' })
await expect
.poll(() => statusEvidence(browser!.page), { timeout: 30_000 })
.toMatchObject({ verification: 'verified', transport: 'ready' })
expect((await statusEvidence(client.page, offlineId))!.sequence).toBeGreaterThan(
expect((await statusEvidence(client!.page, offlineId))!.sequence).toBeGreaterThan(
initial!.sequence
)
await browser.page.screenshot({
@@ -150,12 +156,12 @@ for (const topology of ['desktop', 'headless'] as const) {
await window.api.runtimeEnvironments.disconnect({ selector })
}, offlineId)
await expect
.poll(() => statusEvidence(client.page, offlineId))
.poll(() => statusEvidence(client!.page, offlineId))
.toMatchObject({ verification: 'blocked', transport: 'disconnected' })
} finally {
await browser?.dispose()
await client.dispose()
await proxy.close()
await client?.dispose()
await proxy?.close()
await headless?.dispose()
}
})