Files
orca/mobile/src/mobile-web/mobile-web-host-rpc-error-code.test.ts
T
Jinwoo-H 4c7fd1aec6 fix(mobile-web): make the shell to page bridge direction forward compatible
W1 (P1): every shell-authored result and event schema was .strict(), and the page fails a schema mismatch as invalid_message with retryable:false. The shell (APK) and the page (served by the desktop) ship from different releases, so one additive field or one new session tab kind from a newer APK killed the subscription and its one-shot fallback on the same byte - Loading tabs forever, surviving force-quit. tolerantMobileWebShellPayload deep-rewrites a schema at the page's two shell-payload parse sites: strict objects strip unknown keys, an array of unions drops members it cannot classify, and an unknown value for an optional/nullable closed set reads as absent. Page to shell request schemas keep .strict() - the shell is the security authority there. A census ratchet walks every contract export the page parses and fails if a strict node survives the transform.

W3 (P2): a host RPC failure collapsed into host_error, which is retryable, so method_not_found and the mobile allowlist's forbidden looked like blips. Both now map to unsupported_capability (non-retryable) through mobileWebBrokerHostRpcError, applied by codemod to the 44 regular 'if (!x.ok) throw host_error' sites.

W4 (P3): BrowserScreencastResult gains the navigation member the host already emits. Decoders unchanged.

W2 (P2): the file: confinement test gains a clientKind runtime case - pairedDeviceId is minted for scope 'runtime' too, so the fence also governs the web client, a remote desktop, and remote orca CLI.

W5 (P3): inputFloor and queryReplyAuthority stay literals with a WHY comment. Traced: the host publishes neither over the terminal stream. isMobileTerminalQueryReplyAuthority is never sent, and opcode-17 WriteUnavailable reports one refused write with no regain signal, so it is not the floor state the field declares.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-04 16:41:17 -04:00

79 lines
2.8 KiB
TypeScript

/**
* A host RPC the running page will never reach used to arrive as `host_error`, which the bridge
* marks retryable. The page's cached package is keyed per host and opens before any refresh, so it
* can drive a desktop release that predates or postdates it; every method that host lacks answers
* `method_not_found`, and the mobile allowlist answers `forbidden`. Both are structural absences,
* not blips.
*/
import { describe, expect, it, vi } from 'vitest'
import type { RpcClient } from '../transport/rpc-client'
import { createMobileWebBridgeRoundtripFixture } from './mobile-web-bridge-roundtrip-fixture'
import {
isRetryableMobileWebBridgeError,
mobileWebBridgeErrorCode,
mobileWebBridgeErrorCodeForHostRpc,
mobileWebBrokerHostRpcError
} from './mobile-web-broker-error'
const GRANT_LIMITS = {
maxRequestBytes: 4096,
maxResponseBytes: 128 * 1024,
maxConcurrent: 2,
rateCapacity: 8,
rateRefillPerSecond: 4
}
function failingClient(code: string): RpcClient {
return {
sendRequest: vi.fn(async () => ({
id: 'r1',
ok: false as const,
error: { code, message: `Method 'accounts.list' is not available` },
_meta: { runtimeId: 'runtime-1' }
})),
subscribe: vi.fn(() => () => {})
} as unknown as RpcClient
}
describe('host RPC error codes', () => {
it.each([
['method_not_found', 'unsupported_capability'],
['method_not_supported', 'unsupported_capability'],
['forbidden', 'unsupported_capability'],
['runtime_error', 'host_error'],
['', 'host_error']
])('maps host code %s to %s', (code, expected) => {
expect(mobileWebBridgeErrorCodeForHostRpc({ code })).toBe(expected)
expect(mobileWebBridgeErrorCode(mobileWebBrokerHostRpcError({ code }))).toBe(expected)
})
it('keeps a structural absence non-retryable and a genuine host failure retryable', () => {
expect(isRetryableMobileWebBridgeError(mobileWebBridgeErrorCodeForHostRpc({}))).toBe(true)
expect(
isRetryableMobileWebBridgeError(
mobileWebBridgeErrorCodeForHostRpc({ code: 'method_not_found' })
)
).toBe(false)
expect(
isRetryableMobileWebBridgeError(mobileWebBridgeErrorCodeForHostRpc({ code: 'forbidden' }))
).toBe(false)
})
it('tolerates a non-string host code', () => {
expect(mobileWebBridgeErrorCodeForHostRpc({ code: 42 })).toBe('host_error')
})
it.each([
['method_not_found', 'unsupported_capability', false],
['forbidden', 'unsupported_capability', false],
['runtime_error', 'host_error', true]
])('reports %s to the page as %s', async (code, expected, retryable) => {
const { client } = createMobileWebBridgeRoundtripFixture({
grants: [{ capability: 'account', operation: 'snapshot', limits: GRANT_LIMITS }],
rpcClient: failingClient(code)
})
await expect(client.account.snapshot()).rejects.toMatchObject({ code: expected, retryable })
})
})