mirror of
https://github.com/stablyai/orca.git
synced 2026-09-29 08:03:20 +00:00
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
79 lines
2.8 KiB
TypeScript
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 })
|
|
})
|
|
})
|