refactor(runtime-environments): one exhaustive host-kind classifier for connect, remove, call readiness, hint and phone relay (#29500)

* test(runtime-environments): pin connect, remove, call readiness, hint and phone relay per host kind

* refactor(runtime-environments): dispatch on one exhaustive host-kind classifier

* test: avoid type assertions in host-kind characterization fakes

* refactor(runtime-environments): keep service-owned connect/remove in one module; tighten characterization tests

* style: break, not bare return, in host service ops switches
This commit is contained in:
Neil
2026-10-16 00:51:10 -07:00
committed by GitHub
parent 12a1128afe
commit c57e530a50
11 changed files with 530 additions and 44 deletions
@@ -6,10 +6,13 @@ import {
resolveEnvironment
} from '../../shared/runtime-environment-store'
import {
isWslHostRuntimeEnvironment,
redactRuntimeEnvironment,
type PublicKnownRuntimeEnvironment
} from '../../shared/runtime-environments'
import {
connectThroughOwningService,
removeThroughOwningService
} from './runtime-environment-host-service-ops'
import { RemoteRuntimeClientError } from '../../shared/remote-runtime-client-error'
import { RuntimeRpcCallQueueOverloadError } from '../../shared/runtime-rpc-call-queue'
import type { RuntimeRpcFailure, RuntimeRpcResponse } from '../../shared/runtime-rpc-envelope'
@@ -41,7 +44,6 @@ import {
observeWslOrcadHostResponse,
wslOrcadHostSupports
} from '../wsl/wsl-orcad-host-environments'
import { getWslHostService } from '../wsl/wsl-host-service-instance'
import { assertWslHostReachable, isGatedOffWslHost } from '../wsl/wsl-host-gate'
function manuallyDisconnectedResponse(
@@ -127,12 +129,9 @@ export function registerRuntimeEnvironmentConnectivityHandlers({
}
// A closed gate leaves persisted WSL hosts untouched on disk.
assertWslHostReachable(environment)
const wsl = isWslHostRuntimeEnvironment(environment) ? getWslHostService() : null
if (wsl) {
// Also releases the distro's holder and drops the WSL record.
return wsl
.remove(environment.id)
.then(() => ({ removed: redactRuntimeEnvironment(environment) }))
const ownedRemoval = removeThroughOwningService(environment)
if (ownedRemoval) {
return ownedRemoval.then(() => ({ removed: redactRuntimeEnvironment(environment) }))
}
const removed = removeEnvironment(getUserDataPath(), args.selector)
void retireRemovedRuntimeEnvironment(removed.id, invalidateTransport, store)
@@ -162,10 +161,7 @@ export function registerRuntimeEnvironmentConnectivityHandlers({
assertWslHostReachable(environment)
clearRuntimeEnvironmentManualDisconnect(environment.id)
clearSshCredentialDeclinedForEnvironment(environment)
if (isWslHostRuntimeEnvironment(environment)) {
// An explicit connect may boot the distro.
await getWslHostService()?.connect(environment.id)
}
await connectThroughOwningService(environment)
return getRuntimeEnvironmentStatus(getUserDataPath(), environment.id, args.timeoutMs, {
reconnect: true
})
@@ -0,0 +1,250 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
// Pins what connect and remove do for each kind of host, so host-kind dispatch stays exhaustive.
type WslServiceFake = { connect: ReturnType<typeof vi.fn>; remove: ReturnType<typeof vi.fn> }
const mocks = vi.hoisted(() => {
const order: string[] = []
const wsl: { service: WslServiceFake | null } = { service: null }
return {
order,
handle: vi.fn(),
resolveEnvironment: vi.fn(),
removeEnvironment: vi.fn(),
retire: vi.fn(),
closeRequest: vi.fn(),
getStatus: vi.fn(),
clearDecline: vi.fn(),
wsl
}
})
vi.mock('electron', () => ({ ipcMain: { handle: mocks.handle } }))
vi.mock('../../shared/runtime-environment-store', () => ({
addEnvironmentFromPairingCode: vi.fn(),
listEnvironments: vi.fn(() => []),
removeEnvironment: mocks.removeEnvironment,
resolveEnvironment: mocks.resolveEnvironment
}))
vi.mock('./runtime-environment-removal-cleanup', () => ({
retireRemovedRuntimeEnvironment: mocks.retire
}))
vi.mock('./runtime-environment-request-connections', async (importOriginal) => ({
...(await importOriginal<Record<string, unknown>>()),
closeRemoteRuntimeRequestConnection: mocks.closeRequest
}))
vi.mock('./runtime-environment-transport-routing', () => ({
callRuntimeEnvironment: vi.fn(),
getRuntimeEnvironmentStatus: mocks.getStatus
}))
vi.mock('../ssh/ssh-credential-decline', () => ({
clearSshCredentialDeclinedForEnvironment: mocks.clearDecline
}))
vi.mock('../wsl/wsl-host-service-instance', () => ({ getWslHostService: () => mocks.wsl.service }))
import { registerRuntimeEnvironmentConnectivityHandlers } from './runtime-environment-connectivity-handlers'
import { WSL_HOSTS_GATED_OFF_MESSAGE } from '../wsl/wsl-host-gate'
import { WSL_HOSTS_INTERNAL_GATE_ENV } from '../../shared/wsl-host-internal-gate'
const endpoints = [
{
id: 'ws-1',
kind: 'websocket',
label: 'WebSocket',
endpoint: 'ws://h',
deviceToken: 't',
publicKeyB64: 'k'
}
]
const HOSTS = {
paired: { id: 'paired-1', name: 'Box', runtimeId: 'r', endpoints },
sshManagedLegacy: {
id: 'ssh-legacy',
name: 'Legacy',
runtimeId: 'r',
endpoints,
orcadDeployment: {
sshTargetId: 't1',
sshTargetGeneration: 1,
localPort: 4100,
remotePort: 6768
}
},
sshManaged: {
id: 'ssh-1',
name: 'Managed',
runtimeId: 'r',
endpoints,
orcadDeployment: {
kind: 'ssh',
sshTargetId: 't1',
sshTargetGeneration: 1,
localPort: 4100,
remotePort: 6768
}
},
sshAccess: {
id: 'access-1',
name: 'Access',
runtimeId: 'r',
endpoints,
sshAccess: {
sshTargetId: 't2',
sshTargetGeneration: 1,
localPort: 4200,
remotePort: 6768,
endpointId: 'ws-1',
previousPreferredEndpointId: 'ws-0'
}
},
wsl: {
id: 'wsl:ubuntu',
name: 'Ubuntu (WSL)',
runtimeId: 'r',
endpoints,
orcadDeployment: { kind: 'wsl', distro: 'Ubuntu', localPort: 42000, remotePort: 6768 }
}
} as const
type HostName = keyof typeof HOSTS
const NETWORK_HOSTS: HostName[] = ['paired', 'sshManagedLegacy', 'sshManaged', 'sshAccess']
const redacted = (name: HostName) => ({
...HOSTS[name],
endpoints: [{ id: 'ws-1', kind: 'websocket', label: 'WebSocket', endpoint: 'ws://h' }]
})
function handler(channel: string): (event: unknown, args: Record<string, unknown>) => unknown {
const call = mocks.handle.mock.calls.find(([name]) => name === channel)
expect(call).toBeTruthy()
return call![1]
}
function register(activeRuntimeEnvironmentId?: string): void {
registerRuntimeEnvironmentConnectivityHandlers({
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: connect and remove read only settings.
store: { getSettings: () => ({ activeRuntimeEnvironmentId }) } as never,
getUserDataPath: () => '/u',
invalidateTransport: vi.fn()
})
}
function useHost(name: HostName): void {
mocks.resolveEnvironment.mockReturnValue(HOSTS[name])
mocks.removeEnvironment.mockReturnValue(HOSTS[name])
}
beforeEach(() => {
vi.clearAllMocks()
mocks.order.length = 0
vi.stubEnv(WSL_HOSTS_INTERNAL_GATE_ENV, '1')
mocks.wsl.service = {
// Why a macrotask: a dropped await would let getStatus run first and reorder the log.
connect: vi.fn(async () => {
await new Promise((resolve) => setTimeout(resolve, 0))
mocks.order.push('wsl.connect')
}),
remove: vi.fn(async () => {
await new Promise((resolve) => setTimeout(resolve, 0))
mocks.order.push('wsl.remove')
})
}
mocks.clearDecline.mockImplementation(() => mocks.order.push('clearDecline'))
mocks.getStatus.mockImplementation(async () => {
mocks.order.push('getStatus')
return { id: 'status.get', ok: true, result: {}, _meta: { runtimeId: 'r' } }
})
register()
})
afterEach(() => {
vi.unstubAllEnvs()
})
describe('runtimeEnvironments:connect by host kind', () => {
it('connects a WSL host through its service before asking for status', async () => {
useHost('wsl')
await handler('runtimeEnvironments:connect')(null, { selector: 'wsl:ubuntu', timeoutMs: 5 })
expect(mocks.wsl.service!.connect).toHaveBeenCalledWith('wsl:ubuntu')
expect(mocks.order).toEqual(['clearDecline', 'wsl.connect', 'getStatus'])
expect(mocks.getStatus).toHaveBeenCalledWith('/u', 'wsl:ubuntu', 5, { reconnect: true })
})
it('still asks a WSL host for status when no service exists', async () => {
useHost('wsl')
mocks.wsl.service = null
await handler('runtimeEnvironments:connect')(null, { selector: 'wsl:ubuntu' })
expect(mocks.order).toEqual(['clearDecline', 'getStatus'])
})
it('refuses a WSL host with the gate closed before touching anything', async () => {
useHost('wsl')
vi.stubEnv(WSL_HOSTS_INTERNAL_GATE_ENV, '')
await expect(
handler('runtimeEnvironments:connect')(null, { selector: 'wsl:ubuntu' })
).rejects.toThrow(WSL_HOSTS_GATED_OFF_MESSAGE)
expect(mocks.order).toEqual([])
})
it.each(NETWORK_HOSTS)('only asks a %s host for status', async (name) => {
useHost(name)
await handler('runtimeEnvironments:connect')(null, { selector: HOSTS[name].id })
expect(mocks.wsl.service!.connect).not.toHaveBeenCalled()
expect(mocks.clearDecline).toHaveBeenCalledWith(HOSTS[name])
expect(mocks.order).toEqual(['clearDecline', 'getStatus'])
})
})
describe('runtimeEnvironments:remove by host kind', () => {
it('hands a WSL host to its service and reports the resolved record', async () => {
useHost('wsl')
const result = handler('runtimeEnvironments:remove')(null, { selector: 'wsl:ubuntu' })
expect(result).toBeInstanceOf(Promise)
await expect(result).resolves.toEqual({ removed: redacted('wsl') })
expect(mocks.wsl.service!.remove).toHaveBeenCalledWith('wsl:ubuntu')
expect(mocks.removeEnvironment).not.toHaveBeenCalled()
expect(mocks.retire).not.toHaveBeenCalled()
})
it('removes a WSL host generically when no service exists', () => {
useHost('wsl')
mocks.wsl.service = null
expect(handler('runtimeEnvironments:remove')(null, { selector: 'Ubuntu (WSL)' })).toEqual({
removed: redacted('wsl')
})
expect(mocks.removeEnvironment).toHaveBeenCalledWith('/u', 'Ubuntu (WSL)')
expect(mocks.closeRequest).toHaveBeenCalledWith('Ubuntu (WSL)')
})
it('refuses a WSL host with the gate closed', () => {
useHost('wsl')
vi.stubEnv(WSL_HOSTS_INTERNAL_GATE_ENV, '')
expect(() => handler('runtimeEnvironments:remove')(null, { selector: 'wsl:ubuntu' })).toThrow(
WSL_HOSTS_GATED_OFF_MESSAGE
)
expect(mocks.wsl.service!.remove).not.toHaveBeenCalled()
expect(mocks.removeEnvironment).not.toHaveBeenCalled()
})
it('refuses the default host before the gate check', () => {
mocks.handle.mockClear()
register('wsl:ubuntu')
useHost('wsl')
vi.stubEnv(WSL_HOSTS_INTERNAL_GATE_ENV, '')
expect(() => handler('runtimeEnvironments:remove')(null, { selector: 'wsl:ubuntu' })).toThrow(
'This server is the default host for new projects. Choose another one in Advanced before removing it.'
)
})
it.each(NETWORK_HOSTS)('removes a %s host generically and synchronously', (name) => {
useHost(name)
const id = HOSTS[name].id
expect(handler('runtimeEnvironments:remove')(null, { selector: id })).toEqual({
removed: redacted(name)
})
expect(mocks.wsl.service!.remove).not.toHaveBeenCalled()
expect(mocks.removeEnvironment).toHaveBeenCalledWith('/u', id)
expect(mocks.retire).toHaveBeenCalledWith(id, expect.any(Function), expect.anything())
// Removed by its own id, so no legacy-selector transport is closed.
expect(mocks.closeRequest).not.toHaveBeenCalled()
})
})
@@ -0,0 +1,32 @@
/** Connect and remove for hosts a desktop service owns (WSL); network hosts need neither. */
import { classifyRuntimeEnvironmentHost } from '../../shared/runtime-environment-host-kind'
import type { KnownRuntimeEnvironment } from '../../shared/runtime-environments'
import { getWslHostService } from '../wsl/wsl-host-service-instance'
/** An explicit user connect; may boot a WSL host's distro. */
export async function connectThroughOwningService(
environment: KnownRuntimeEnvironment
): Promise<void> {
const host = classifyRuntimeEnvironmentHost(environment)
switch (host.kind) {
case 'wsl':
await getWslHostService()?.connect(environment.id)
break
case 'network':
break
}
}
/** Null when no service owns the host, which leaves removal to the generic store path. */
export function removeThroughOwningService(
environment: KnownRuntimeEnvironment
): Promise<unknown> | null {
const host = classifyRuntimeEnvironmentHost(environment)
switch (host.kind) {
case 'wsl':
// Also releases the distro's holder and drops the WSL record.
return getWslHostService()?.remove(environment.id) ?? null
case 'network':
return null
}
}
@@ -1,18 +1,35 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
const mocks = vi.hoisted(() => ({
ensureTunnel: vi.fn(async () => {}),
rebuildTunnel: vi.fn(async () => {}),
verify: vi.fn(),
restoreUpdate: vi.fn(() => null),
redeploy: vi.fn(async () => true)
}))
const mocks = vi.hoisted(() => {
const environments: Record<string, Record<string, unknown>> = {}
const reads: Record<string, number> = {}
return {
environments,
reads,
ensureForRuntimeCall: vi.fn(async () => {}),
hasWslService: true,
ensureTunnel: vi.fn(async () => {}),
rebuildTunnel: vi.fn(async () => {}),
verify: vi.fn(),
restoreUpdate: vi.fn(() => null),
redeploy: vi.fn(async () => true)
}
})
vi.mock('../../shared/runtime-environment-store', () => ({
resolveEnvironment: (_userData: string, id: string) => ({
id,
orcadDeployment: { sshTargetId: 'ssh-1' }
})
resolveEnvironment: (_userData: string, id: string) => {
const environment = mocks.environments[id]
if (!environment) {
return { id, orcadDeployment: { sshTargetId: 'ssh-1' } }
}
mocks.reads[id] = (mocks.reads[id] ?? 0) + 1
// A fresh object per read, so a test can tell the re-read after bring-up from the first.
return { ...environment, read: mocks.reads[id] }
}
}))
vi.mock('../wsl/wsl-host-service-instance', () => ({
getWslHostService: () =>
mocks.hasWslService ? { ensureForRuntimeCall: mocks.ensureForRuntimeCall } : null
}))
vi.mock('../ssh/managed-server-update-deps', () => ({ managedServerUpdateDeps: () => ({}) }))
vi.mock('../ssh/orcad-managed-tunnel', () => ({
@@ -33,6 +50,8 @@ vi.mock('./ssh-renderer-broadcast', () => ({ broadcastSshState: vi.fn() }))
import { MANAGED_ORCAD_NOT_ACTIVATED_DETAIL } from '../ssh/orcad-managed-serving'
import { resolveManagedRuntimeEnvironment } from './runtime-environment-managed-tunnel'
import { WSL_HOSTS_GATED_OFF_MESSAGE } from '../wsl/wsl-host-gate'
import { WSL_HOSTS_INTERNAL_GATE_ENV } from '../../shared/wsl-host-internal-gate'
describe('resolving a managed runtime for a call', () => {
beforeEach(() => vi.clearAllMocks())
@@ -57,3 +76,61 @@ describe('resolving a managed runtime for a call', () => {
expect(mocks.restoreUpdate).toHaveBeenCalledWith('env-1', expect.any(Function))
})
})
describe('resolving a runtime for a call, by host kind', () => {
const wsl = {
id: 'wsl:ubuntu',
orcadDeployment: { kind: 'wsl', distro: 'Ubuntu', localPort: 42000, remotePort: 6768 }
}
beforeEach(() => {
vi.clearAllMocks()
mocks.hasWslService = true
for (const id of Object.keys(mocks.reads)) {
delete mocks.reads[id]
}
mocks.environments = { 'wsl:ubuntu': wsl, 'paired-1': { id: 'paired-1' } }
mocks.verify.mockResolvedValue({ state: 'serving' })
vi.stubEnv(WSL_HOSTS_INTERNAL_GATE_ENV, '1')
})
afterEach(() => vi.unstubAllEnvs())
it('brings a WSL host up through its service, never through an SSH tunnel', async () => {
// The record re-read after bring-up, which may have moved the bridge port.
await expect(resolveManagedRuntimeEnvironment('/u', 'wsl:ubuntu')).resolves.toEqual({
...wsl,
read: 2
})
expect(mocks.ensureForRuntimeCall).toHaveBeenCalledWith('wsl:ubuntu')
expect(mocks.ensureTunnel).not.toHaveBeenCalled()
expect(mocks.verify).not.toHaveBeenCalled()
expect(mocks.restoreUpdate).not.toHaveBeenCalled()
})
it('resolves a WSL host with no service without bringing anything up', async () => {
mocks.hasWslService = false
await expect(resolveManagedRuntimeEnvironment('/u', 'wsl:ubuntu')).resolves.toEqual({
...wsl,
read: 2
})
expect(mocks.ensureTunnel).not.toHaveBeenCalled()
})
it('refuses a WSL host with the gate closed', async () => {
vi.stubEnv(WSL_HOSTS_INTERNAL_GATE_ENV, '')
await expect(resolveManagedRuntimeEnvironment('/u', 'wsl:ubuntu')).rejects.toThrow(
WSL_HOSTS_GATED_OFF_MESSAGE
)
expect(mocks.ensureForRuntimeCall).not.toHaveBeenCalled()
})
it('runs the managed tunnel path for a paired server too', async () => {
await expect(resolveManagedRuntimeEnvironment('/u', 'paired-1')).resolves.toEqual({
id: 'paired-1',
read: 2
})
expect(mocks.ensureTunnel).toHaveBeenCalledWith('/u', 'paired-1')
expect(mocks.verify).toHaveBeenCalledWith('/u', 'paired-1')
expect(mocks.restoreUpdate).toHaveBeenCalledWith('paired-1', expect.any(Function))
expect(mocks.ensureForRuntimeCall).not.toHaveBeenCalled()
})
})
@@ -1,8 +1,9 @@
import { resolveEnvironment } from '../../shared/runtime-environment-store'
import {
getOrcadSshDeployment,
isWslHostRuntimeEnvironment
type KnownRuntimeEnvironment
} from '../../shared/runtime-environments'
import { classifyRuntimeEnvironmentHost } from '../../shared/runtime-environment-host-kind'
import type {
SshManagedServerStatus,
SshManagedServerUpdateNote,
@@ -34,11 +35,23 @@ export async function resolveManagedRuntimeEnvironment(
const environment = resolveEnvironment(userDataPath, selector)
// Every call, subscription and status probe passes here, so a gated-off WSL host is never reached.
assertWslHostReachable(environment)
if (isWslHostRuntimeEnvironment(environment)) {
// Reached over the wsl.exe bridge; connects only when an open project or terminal needs it.
await getWslHostService()?.ensureForRuntimeCall(environment.id)
return resolveEnvironment(userDataPath, environment.id)
const host = classifyRuntimeEnvironmentHost(environment)
switch (host.kind) {
case 'wsl':
// Reached over the wsl.exe bridge; connects only when an open project or terminal needs it.
await getWslHostService()?.ensureForRuntimeCall(environment.id)
break
case 'network':
await prepareNetworkServerForCall(userDataPath, environment)
break
}
return resolveEnvironment(userDataPath, environment.id)
}
async function prepareNetworkServerForCall(
userDataPath: string,
environment: KnownRuntimeEnvironment
): Promise<void> {
await ensureOrcadManagedTunnel(userDataPath, environment.id)
// Why: a server that stopped (idle, killed, host rebooted) starts before the call that needs it.
const serving = await verifyOrcadManagedServing(userDataPath, environment.id)
@@ -71,7 +84,6 @@ export async function resolveManagedRuntimeEnvironment(
.catch(() => {})
}
}
return resolveEnvironment(userDataPath, environment.id)
}
function isStoppedElsewhere(serving: OrcadManagedServing): boolean {
@@ -68,6 +68,27 @@ describe('runtime environment mobile relay hosts', () => {
expect(mocks.resolveManaged).not.toHaveBeenCalled()
})
it('relays SSH-managed servers, with and without the persisted deployment kind', async () => {
const tunnel = { sshTargetId: 't', sshTargetGeneration: 1, localPort: 1, remotePort: 2 }
mocks.environments = [
{ id: 'legacy', name: 'A', createdAt: 1, runtimeId: null, orcadDeployment: tunnel },
{
id: 'managed',
name: 'B',
createdAt: 2,
runtimeId: null,
orcadDeployment: { kind: 'ssh', ...tunnel }
}
]
const hosts = createRuntimeEnvironmentMobileRelayHosts()
expect(hosts.list().environments.map((environment) => environment.id)).toEqual([
'legacy',
'managed'
])
await hosts.resolve('managed')
expect(mocks.resolveManaged).toHaveBeenCalledWith('/user-data', 'managed')
})
it("lists configured servers with the desktop's own status, ignoring another pairing's snapshot", () => {
mocks.environments = [
{ id: 'env-1', name: 'VM', createdAt: 1, pairingRevision: 7, runtimeId: 'runtime-a' },
@@ -3,9 +3,9 @@ import { pairedRuntimeBrowserClientHostsPage } from '../browser/paired-runtime-b
import { listEnvironments } from '../../shared/runtime-environment-store'
import {
getPreferredPairingOffer,
isWslHostRuntimeEnvironment,
type KnownRuntimeEnvironment
} from '../../shared/runtime-environments'
import { classifyRuntimeEnvironmentHost } from '../../shared/runtime-environment-host-kind'
import {
runtimeEnvironmentStatusFromSnapshot,
type RuntimeEnvironmentStatus
@@ -31,9 +31,17 @@ export function retireMobileDesktopRelayEnvironment(environmentId: string): void
* distros, connected only for work open here, so a phone must not wake or route to them.
*/
function listRelayableEnvironments(userDataPath: string) {
return listEnvironments(userDataPath).filter(
(environment) => !isWslHostRuntimeEnvironment(environment)
)
return listEnvironments(userDataPath).filter(isRelayableToPhones)
}
function isRelayableToPhones(environment: KnownRuntimeEnvironment): boolean {
const host = classifyRuntimeEnvironmentHost(environment)
switch (host.kind) {
case 'network':
return true
case 'wsl':
return false
}
}
/** The desktop's configured servers, addressed only by exact environment id. */
@@ -1,5 +1,6 @@
import { describe, expect, it } from 'vitest'
import { withRuntimeUnreachableHint } from './runtime-environment-unreachable-hint'
import { withRemoteRuntimeTailscaleHint } from '../../shared/remote-runtime-tailscale-hint'
const UNREACHABLE = 'Could not connect to the remote Orca runtime at ws://127.0.0.1:42000.'
@@ -26,4 +27,40 @@ describe('withRuntimeUnreachableHint', () => {
}
expect(withRuntimeUnreachableHint(wsl, 'Unauthorized', null)).toBe('Unauthorized')
})
it.each([
['a paired server', {}],
[
'a legacy SSH-managed server',
{ orcadDeployment: { sshTargetId: 't', sshTargetGeneration: 1, localPort: 1, remotePort: 2 } }
],
[
'an SSH-managed server',
{
orcadDeployment: {
kind: 'ssh' as const,
sshTargetId: 't',
sshTargetGeneration: 1,
localPort: 1,
remotePort: 2
}
}
]
])('gives %s exactly the Tailscale hint', (_name, environment) => {
expect(withRuntimeUnreachableHint(environment, UNREACHABLE, 'ws://203.0.113.4:6768')).toBe(
withRemoteRuntimeTailscaleHint(UNREACHABLE, 'ws://203.0.113.4:6768')
)
})
it('pins the full WSL hint text', () => {
expect(
withRuntimeUnreachableHint(
{ orcadDeployment: { kind: 'wsl', distro: 'Debian', localPort: 1, remotePort: 2 } },
'Timed out waiting for the remote Orca runtime.',
null
)
).toBe(
'Timed out waiting for the remote Orca runtime. The Orca server in the WSL distro Debian isn’t answering; connect it again in Settings → Remote Orca Servers → WSL hosts.'
)
})
})
@@ -1,9 +1,7 @@
/** What to suggest when a server cannot be reached, chosen by the kind of host that owns it. */
import { withRemoteRuntimeTailscaleHint } from '../../shared/remote-runtime-tailscale-hint'
import {
isWslHostRuntimeEnvironment,
type KnownRuntimeEnvironment
} from '../../shared/runtime-environments'
import { classifyRuntimeEnvironmentHost } from '../../shared/runtime-environment-host-kind'
import type { KnownRuntimeEnvironment } from '../../shared/runtime-environments'
const UNREACHABLE_RE =
/could not connect to the remote orca runtime|remote orca runtime closed the connection|timed out (?:waiting for|while connecting to) the remote orca runtime/i
@@ -13,11 +11,14 @@ export function withRuntimeUnreachableHint(
message: string,
endpoint: string | null | undefined
): string {
if (!isWslHostRuntimeEnvironment(environment)) {
return withRemoteRuntimeTailscaleHint(message, endpoint)
const host = classifyRuntimeEnvironmentHost(environment)
switch (host.kind) {
case 'network':
return withRemoteRuntimeTailscaleHint(message, endpoint)
case 'wsl':
// Why not Tailscale: a WSL host is a distro on this computer, reached over wsl.exe.
return UNREACHABLE_RE.test(message)
? `${message} The Orca server in the WSL distro ${host.deployment.distro} isn’t answering; connect it again in Settings → Remote Orca Servers → WSL hosts.`
: message
}
// Why not Tailscale: a WSL host is a distro on this computer, reached over wsl.exe.
return UNREACHABLE_RE.test(message)
? `${message} The Orca server in the WSL distro ${environment.orcadDeployment.distro} isn’t answering; connect it again in Settings → Remote Orca Servers → WSL hosts.`
: message
}
@@ -0,0 +1,23 @@
import { describe, expect, it } from 'vitest'
import { classifyRuntimeEnvironmentHost } from './runtime-environment-host-kind'
const tunnel = { sshTargetId: 't', sshTargetGeneration: 1, localPort: 1, remotePort: 2 }
const wsl = { kind: 'wsl' as const, distro: 'Ubuntu', localPort: 1, remotePort: 2 }
describe('classifyRuntimeEnvironmentHost', () => {
it.each([
['no deployment', {}],
['a null deployment', { orcadDeployment: null }],
['an SSH link saved without its kind', { orcadDeployment: tunnel }],
['an SSH link', { orcadDeployment: { kind: 'ssh' as const, ...tunnel } }]
])('treats %s as a network host', (_name, environment) => {
expect(classifyRuntimeEnvironmentHost(environment)).toEqual({ kind: 'network', environment })
})
it('treats a WSL deployment as a WSL host and hands back its link', () => {
const environment = { orcadDeployment: wsl }
const host = classifyRuntimeEnvironmentHost(environment)
expect(host).toEqual({ kind: 'wsl', environment, deployment: wsl })
expect(host.environment).toBe(environment)
})
})
@@ -0,0 +1,29 @@
import type { OrcadDeploymentLink } from './runtime-environments'
import type { OrcadWslDeploymentLink } from './wsl-host-deployment-link'
/**
* What kind of host serves a runtime environment. Switch on `kind` (lint requires the switch to be
* exhaustive), so a new kind fails the build at every switch on it.
* `network`: reached over the network or an SSH tunnel (paired, SSH access, SSH-managed).
* `wsl`: a distro on this computer, reached over the wsl.exe bridge.
*/
export type RuntimeEnvironmentHost<T> =
| { kind: 'network'; environment: T }
| { kind: 'wsl'; environment: T; deployment: OrcadWslDeploymentLink }
export function classifyRuntimeEnvironmentHost<
T extends { orcadDeployment?: OrcadDeploymentLink | null }
>(environment: T): RuntimeEnvironmentHost<T> {
const deployment = environment.orcadDeployment
if (!deployment) {
return { kind: 'network', environment }
}
switch (deployment.kind) {
// Why undefined: SSH deployment links are persisted without their kind.
case undefined:
case 'ssh':
return { kind: 'network', environment }
case 'wsl':
return { kind: 'wsl', environment, deployment }
}
}