From 5be50beb414c88fdd7bc3d75ce05f5a57dc6b4ad Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Fri, 18 Sep 2026 01:12:51 -0400 Subject: [PATCH] fix(mobile): let runRpcOperation send a params-less method A3 declares `mobileWeb.bundle.manifest` with `params: null`, so the generated catalog types its send params as `void` and the two call sites that pass an explicit `null` stopped compiling. `bindDeferredRpcOperation.request` already solved this: `RpcSendArguments` admits `null` exactly where the catalog declares no params, because `params: null` is not the frame that omits the key and narrowing it would rewrite bytes shipped senders already put on the wire. `runRpcOperation` was the one send entry point that never adopted the tuple, having had no params-less caller until now. The compile fence pins all three accepted shapes and that a params-bearing object is still refused. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb --- mobile/src/transport/rpc-operation-compile-fence.ts | 12 ++++++++++++ mobile/src/transport/rpc-operation-test-families.ts | 8 ++++++++ mobile/src/transport/rpc-operation.ts | 8 +++++--- 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/mobile/src/transport/rpc-operation-compile-fence.ts b/mobile/src/transport/rpc-operation-compile-fence.ts index ac14b326fd2..c57c4950a01 100644 --- a/mobile/src/transport/rpc-operation-compile-fence.ts +++ b/mobile/src/transport/rpc-operation-compile-fence.ts @@ -9,6 +9,7 @@ import { } from './rpc-operation' import { rpcResultVariants } from './rpc-operation-result-reader' import { + pushTestWithoutParams, workspaceListAtBarrier, workspaceListOrNull, workspaceRowsReader, @@ -146,6 +147,17 @@ export async function fenceBarrierAndParams(): Promise { ) } +// A method the catalog declares params-less keeps every shape a shipped sender may use. An +// explicit `null` is the one that matters: `params: null` is not the frame that omits the key, +// so narrowing this to omission would rewrite bytes main already puts on the wire. +export async function fenceParamlessSend(): Promise { + await runRpcOperation(client, pushTestWithoutParams, null) + await runRpcOperation(client, pushTestWithoutParams, undefined) + await runRpcOperation(client, pushTestWithoutParams) + // @ts-expect-error a method that declares no params accepts none + await runRpcOperation(client, pushTestWithoutParams, { path: 'main.js' }) +} + export async function fenceVerdictTypes(): Promise { // @ts-expect-error the probe's policy yields a boolean, not the other family's rows const rows: WorkspaceRows = await runRpcOperation(client, worktreePsProbe, {}) diff --git a/mobile/src/transport/rpc-operation-test-families.ts b/mobile/src/transport/rpc-operation-test-families.ts index 73554b7a4f8..a93c0ba1abf 100644 --- a/mobile/src/transport/rpc-operation-test-families.ts +++ b/mobile/src/transport/rpc-operation-test-families.ts @@ -54,6 +54,14 @@ export const worktreePsProbe = defineRpcOperation({ barrier: 'on-settle' }) +/** A method the catalog declares with no params at all, so `RpcSendParams` reads `void`. */ +export const pushTestWithoutParams = defineRpcOperation({ + name: 'test.pushTestWithoutParams', + method: 'notifications.testPush', + acceptance: 'method-not-found-refusal', + barrier: 'on-settle' +}) + export const terminalStreamOpener = defineRpcOperation({ name: 'test.terminalStreamOpener', method: 'terminal.subscribe', diff --git a/mobile/src/transport/rpc-operation.ts b/mobile/src/transport/rpc-operation.ts index a836dc429fb..620f1516a12 100644 --- a/mobile/src/transport/rpc-operation.ts +++ b/mobile/src/transport/rpc-operation.ts @@ -173,10 +173,12 @@ export async function runRpcOperation< >( client: UnvalidatedRpcRequestPort, operation: RpcOperation, - params: RpcSendParams, - options?: SendRequestOptions + // Shares the deferred sender's tuple so the two cannot disagree about what a params-less + // method may be called with: the catalog types those `void`, and an explicit `null` is the + // frame several shipped senders already put on the wire. + ...args: RpcSendArguments ): Promise> { - const outcome = await request(client, operation, params, options) + const outcome = await request(client, operation, args[0], args[1]) // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Preserve the established response shape at this boundary. return interpretRpcOutcome(operation, outcome) as RpcVerdict }