diff --git a/mobile/src/session/mobile-clipboard-image.ts b/mobile/src/session/mobile-clipboard-image.ts index 5eae5293d85..7551ca55725 100644 --- a/mobile/src/session/mobile-clipboard-image.ts +++ b/mobile/src/session/mobile-clipboard-image.ts @@ -146,7 +146,12 @@ async function uploadMobileClipboardImageTransaction( throw new Error(startResponse.error.message) } - const { uploadId } = clipboardImageUploadStart.interpret(startResponse) as { uploadId: string } + // Why the raw result rather than the interpretation: a success carrying no result throws a + // TypeError here, and V8 puts the destructured expression's source text in its message — which + // the composer then shows. Reading the slot off the accepted payload would rewrite that sentence + // for every user who hits a malformed reply, which is the one change this migration must not make. + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the same cast main made, kept so the thrown message is the same one. + const { uploadId } = startResponse.result as { uploadId: string } try { for ( let offset = 0; diff --git a/mobile/src/test-support/rpc-recording/mutants/operation-mutations.ts b/mobile/src/test-support/rpc-recording/mutants/operation-mutations.ts index 1b52c817536..27dbcca49ea 100644 --- a/mobile/src/test-support/rpc-recording/mutants/operation-mutations.ts +++ b/mobile/src/test-support/rpc-recording/mutants/operation-mutations.ts @@ -7,10 +7,21 @@ import type { OperationMutation } from '../operation-module-loader' */ export const OPERATION_MUTATIONS = { // Loses the generation comparison, so a stale workspace response poisons the search cache. + // Drops the delivery-unknown arm of a native-chat send, so an ack lost after the frame was + // written reads as a definite rejection and invites the user to send the same message twice. + 'native-chat-send-delivery-unknown': { + file: 'mobile-native-chat-send.ts', + before: ` return isRpcDeliveryUnknown(error) || isLogicalClientCutoverError(error) + ? 'unknown' + : 'rejected'`, + after: ` return isLogicalClientCutoverError(error) ? 'unknown' : 'rejected'` + }, + // Re-anchored where the operation migration moved the acceptance read; the defect it injects — + // a stale workspace response poisoning the search cache — is unchanged. race: { file: 'use-mobile-native-chat-file-search.ts', - before: '!response.ok || generationRef.current !== generation', - after: '!response.ok' + before: '!accepted.accepted || generationRef.current !== generation', + after: '!accepted.accepted' }, // Accepts a null result envelope instead of rejecting it. The guard is repeated for three // mutations in this file; the anchor carries the message so only the recorded one is edited. @@ -108,15 +119,13 @@ export const OPERATION_MUTATIONS = { }, // Checks the sibling's refusal before the operation's own, so a correlated refusal reports the // sibling. Invisible to every scenario whose sibling succeeds or rejects at the transport. + // Re-anchored where the operation migration moved both reads; the reorder it injects — the + // detection refusal deciding the error before the settings read is interpreted — is unchanged. 'new-tab-refusal-order': { file: 'mobile-new-tab-agent-loader.ts', - before: ` const readSettings = newTabSettingsRead.interpret(settingsResponse) - if (!detectedResponse.ok) { - throw new Error((detectedResponse as RpcFailure).error.message) - }`, - after: ` if (!detectedResponse.ok) { - throw new Error((detectedResponse as RpcFailure).error.message) - } + before: ` const readSettings = newTabSettingsRead.interpret(settingsResponse)`, + after: ` const detected0 = detectedAgents.interpret(detectedAgents.reply) + void detected0 const readSettings = newTabSettingsRead.interpret(settingsResponse)` }, // Publishes an unaccepted read, blanking settings a refusal should have left alone. Invisible diff --git a/mobile/src/test-support/rpc-recording/mutants/pilot-mutants.test.ts b/mobile/src/test-support/rpc-recording/mutants/pilot-mutants.test.ts index acd9840e4f0..8d65addd858 100644 --- a/mobile/src/test-support/rpc-recording/mutants/pilot-mutants.test.ts +++ b/mobile/src/test-support/rpc-recording/mutants/pilot-mutants.test.ts @@ -28,7 +28,8 @@ const mutants: Record = { 'settings-task-hydration-fulfilled': 'task-hydration-envelope', 'settings-task-write': 'task-preferences-optimistic', 'settings-workspace-submit-fulfilled': 'workspace-submit-envelope', - 'settings-task-workspace-fulfilled': 'task-workspace-envelope' + 'settings-task-workspace-fulfilled': 'task-workspace-envelope', + 'native-chat-write-delivery-unknown': 'native-chat-send-delivery-unknown' } /** * The archived tree's visible state, pinned per seed: b1 serves the poisoned empty inventory, b2