Files
orca/src/shared/handled-wire-discriminant.ts
T
Brennan Benson 6387e5b8d3 fix(folder-workspaces): keep the broken-folder marker when a host sends a new reason (#15027)
FolderWorkspacePathStatus is cast, not decoded, off the runtime RPC wire --
runtime-rpc-envelope declares result: z.unknown(), so unwrapRuntimeRpcResult hands
back whatever the host sent. Both title and description switch on status.reason with
no runtime guard, so a newer host publishing a fifth reason matched nothing and
returned undefined. FolderPathStatusIndicator's `!title` check then dropped the whole
indicator, and a broken folder workspace rendered as healthy -- worse than the blank
toast #15002 fixed, because there the warning was empty and here it is gone.

Guard before each switch, the shape #15002 landed. A default: arm is not available:
the type-aware config sets allowDefaultCaseForExhaustiveSwitch:false and rejects one
with switch-exhaustiveness-check.

Extract that guard into isHandledWireDiscriminant instead of hand-writing a third and
fourth copy, and move #15002's two bespoke guards onto it. It takes unknown and checks
typeof before Object.hasOwn -- hasOwn coerces its key, so a host that widened the field
to an array sends ['missing'], which a hasOwn-only guard admits before the switch drops
it straight back out. That was the P1 found in review on #15002; one implementation
makes it structural instead of tribal.

An unrecognized reason gets its own copy rather than reusing 'unavailable'. The
unavailable remedy -- "Check the runtime or SSH connection and try again" -- is a false
lead here: the host did check and reported the folder unusable, so retrying and
inspecting a healthy connection wastes the user's time. Update Orca is the real remedy.

Adding a fifth reason still fails typecheck in two places: TS2741 on the Record and
TS2366 plus switch-exhaustiveness-check on both switches.
2026-08-17 01:01:28 -07:00

16 lines
880 B
TypeScript

// Why: runtime RPC results are cast, not decoded — `runtime-rpc-envelope` declares `result:
// z.unknown()` — so a host on a newer build routinely puts a discriminant on the wire that this
// build's union has never heard of, and a switch on it falls through to `undefined`.
//
// Callers pass a `Record<TheUnion, true>` so a new union member fails typecheck twice: on the Record
// (TS2741) and on the switch (`switch-exhaustiveness-check`, which forbids an absorbing `default:`).
//
// typeof first: `Object.hasOwn` coerces its key, so a host that widened the field to an array sends
// `['missing']`, which a hasOwn-only guard admits before the switch drops it straight back out.
export function isHandledWireDiscriminant<T extends string>(
value: unknown,
handled: Record<T, true>
): value is T {
return typeof value === 'string' && Object.hasOwn(handled, value)
}