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 }