Files
orca/src/shared/remote-runtime-client-error-classification.ts
T
Jinwoo Hong 2ec36a95c4 test(runtime): pin transport error code/message classification agreement (#12676)
Since #12667, a present error code short-circuits classification: a code that is genuinely transient but missing from `RECOVERABLE_CODES` classifies as FATAL. That is the shape that dead-ends terminal panes — #12650 fixed exactly that for a different error, where a transient failure misclassified as fatal unmounted the Reconnect banner and left recreating the session as the only escape.

Today the code and fragment sets agree. Nothing prevented a future code from being added without a matching entry, and the failure would have been silent.

This pins that agreement: for every reachable transport error, a code that classifies fatal must not carry a message that would have classified recoverable. 57 coded pairs plus 8 code-less ones, derived by invoking the producers where possible so a reworded message updates the corpus instead of leaving a stale copy silently passing. The failure message names the offending fragment and says what to do about it.

Enumeration turned up producers beyond the obvious ones — notably the Tailscale-hinted variants, where `runtime-environment-transport-routing.ts` mutates the message on an already-coded error before it crosses IPC, making those distinct corpus members.

Also documented (not asserted, because it is unreachable today): `runtime_rpc_queue_overloaded` is absent from both host passthrough allowlists, so if it ever crossed `mapRuntimeError` it would flatten to `runtime_error` while keeping its "queue is full" message — precisely the dangerous shape. The queue pool is never instantiated on the server dispatcher, so it cannot happen now.

The known exception is pinned rather than silently exempted: a dedicated test records WHY the guard cannot see `remote_runtime_busy` (fatal by code, matching no fragment, so the two sides have nothing to disagree about). If someone rewords a busy message into connection wording, that test fails and points at STA-3479.

Proven non-vacuous by four separate injections. The only production change is two `const` to `export const`.
2026-08-05 01:01:19 -07:00

58 lines
2.1 KiB
TypeScript

export type RemoteRuntimeClientErrorLike = { code?: string; message: string }
export const RUNTIME_RPC_QUEUE_OVERLOAD_CODE = 'runtime_rpc_queue_overloaded'
export const RUNTIME_RPC_QUEUE_OVERLOAD_MESSAGE_FRAGMENT = 'remote runtime call queue is full'
// Exported so the transport-error corpus guard can name the offending entry when a
// code and its message disagree; see remote-runtime-transport-error-agreement.test.ts.
export const RECOVERABLE_CODES: ReadonlySet<string> = new Set([
'remote_runtime_unavailable',
RUNTIME_RPC_QUEUE_OVERLOAD_CODE,
'runtime_timeout',
'runtime_unavailable',
'reconnecting',
'timeout'
])
export const RECOVERABLE_MESSAGE_FRAGMENTS: readonly string[] = [
'could not connect to the remote orca runtime',
'remote orca runtime closed the connection',
'remote orca runtime connection closed',
'remote orca runtime is not connected',
RUNTIME_RPC_QUEUE_OVERLOAD_MESSAGE_FRAGMENT,
'remote runtime connection closed',
'remote runtime subscription closed before it started',
'remote terminal stream is not connected',
'timed out waiting for the remote orca runtime'
]
export function isRuntimeRpcQueueOverloadError(error: RemoteRuntimeClientErrorLike): boolean {
if (error.code) {
return error.code === RUNTIME_RPC_QUEUE_OVERLOAD_CODE
}
return error.message.toLowerCase().includes(RUNTIME_RPC_QUEUE_OVERLOAD_MESSAGE_FRAGMENT)
}
export function isRecoverableRemoteRuntimeConnectionError(
error: RemoteRuntimeClientErrorLike
): boolean {
if (error.code) {
return RECOVERABLE_CODES.has(error.code)
}
const message = error.message.toLowerCase()
return RECOVERABLE_MESSAGE_FRAGMENTS.some((fragment) => message.includes(fragment))
}
export function toRemoteRuntimeClientErrorLike(error: unknown): RemoteRuntimeClientErrorLike {
if (error && typeof error === 'object') {
const candidate = error as { code?: unknown; message?: unknown }
if (typeof candidate.message === 'string') {
return {
...(typeof candidate.code === 'string' ? { code: candidate.code } : {}),
message: candidate.message
}
}
}
return { message: String(error) }
}