diff --git a/mobile/rpc-foundation/goldens/matrix-worktree.catalog-snapshot-worktree.ps-1.json b/mobile/rpc-foundation/goldens/matrix-worktree.catalog-snapshot-worktree.ps-1.json index 2a6b18e423d..1fbcc645d8f 100644 --- a/mobile/rpc-foundation/goldens/matrix-worktree.catalog-snapshot-worktree.ps-1.json +++ b/mobile/rpc-foundation/goldens/matrix-worktree.catalog-snapshot-worktree.ps-1.json @@ -6,7 +6,7 @@ "baseline": "e7206f62a827f4fe0a2badf3eecd2167b8ac4285", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", "recorderSha256": "e044443e01f282566a5e0eff110d0861d2a1047ca8d8696a2e7ff914f51957f3", - "adapterSha256": "6119d409e1958877e4a04b3901f94a3609ac0420f5ca02af20eb8dae5cbf3408", + "adapterSha256": "4e942ddfbaa0ba6bfc2993969276987f6528ac53765d125e99a830e261f93a8e", "scenarioSha256": "95ca47f382997c412da974e564a46b1ae0c20d6e0f3ca14258d33c8d8b51a160", "platform": "darwin", "scenarioVersion": 1, @@ -112,12 +112,19 @@ "admitted": "unadmitted", "fetched": "unfetched" }, - "2d40d1d38104": { - "name": "unhandled-rejection", + "3ef9b57ea9ad": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, "value": { - "category": "Error", - "isRpcDeliveryUnknown": false, - "message": "Unsupported observation: function" + "kind": "response", + "pending": { + "admission": { + "kind": "invalid" + }, + "client": "logical-client", + "hostId": "host-1" + } } }, "4262ba495b1b": { @@ -258,6 +265,29 @@ "status": "pending", "startedAt": 0 }, + "9948855e8b8d": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "kind": "response", + "pending": { + "admission": { + "kind": "full", + "snapshotId": "snapshot-1", + "worktrees": [ + { + "displayName": "One", + "repo": "Repo", + "worktreeId": "w-1" + } + ] + }, + "client": "logical-client", + "hostId": "host-1" + } + } + }, "a0fab6bf1fb0": { "name": "worktree.ps#1", "args": [ @@ -584,10 +614,10 @@ "sender": ["227f9e3de4fa"], "payloads": ["a87f1f91dc98"], "settlements": { - "fetch": "9270aeb7d9c6" + "fetch": "9948855e8b8d" }, "state": "ab1a9ba6301c", - "effects": ["2d40d1d38104"] + "effects": [] } }, { @@ -596,10 +626,10 @@ "sender": ["ad584cc963bb"], "payloads": ["a87f1f91dc98"], "settlements": { - "fetch": "9270aeb7d9c6" + "fetch": "3ef9b57ea9ad" }, "state": "50c4271e912d", - "effects": ["2d40d1d38104"] + "effects": [] } }, { @@ -608,10 +638,10 @@ "sender": ["b670d230caf2"], "payloads": ["a87f1f91dc98"], "settlements": { - "fetch": "9270aeb7d9c6" + "fetch": "3ef9b57ea9ad" }, "state": "50c4271e912d", - "effects": ["2d40d1d38104"] + "effects": [] } }, { @@ -620,10 +650,10 @@ "sender": ["4262ba495b1b"], "payloads": ["a87f1f91dc98"], "settlements": { - "fetch": "9270aeb7d9c6" + "fetch": "3ef9b57ea9ad" }, "state": "50c4271e912d", - "effects": ["2d40d1d38104"] + "effects": [] } }, { @@ -632,10 +662,10 @@ "sender": ["5a288976750e"], "payloads": ["a87f1f91dc98"], "settlements": { - "fetch": "9270aeb7d9c6" + "fetch": "3ef9b57ea9ad" }, "state": "50c4271e912d", - "effects": ["2d40d1d38104"] + "effects": [] } }, { @@ -644,10 +674,10 @@ "sender": ["f5d207eddd1d"], "payloads": ["a87f1f91dc98"], "settlements": { - "fetch": "9270aeb7d9c6" + "fetch": "3ef9b57ea9ad" }, "state": "50c4271e912d", - "effects": ["2d40d1d38104"] + "effects": [] } }, { diff --git a/mobile/rpc-foundation/goldens/matrix-worktree.home-catalog-worktree.ps-1.json b/mobile/rpc-foundation/goldens/matrix-worktree.home-catalog-worktree.ps-1.json index ef6259f9a9e..0e193d6da21 100644 --- a/mobile/rpc-foundation/goldens/matrix-worktree.home-catalog-worktree.ps-1.json +++ b/mobile/rpc-foundation/goldens/matrix-worktree.home-catalog-worktree.ps-1.json @@ -6,7 +6,7 @@ "baseline": "e7206f62a827f4fe0a2badf3eecd2167b8ac4285", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", "recorderSha256": "e044443e01f282566a5e0eff110d0861d2a1047ca8d8696a2e7ff914f51957f3", - "adapterSha256": "6119d409e1958877e4a04b3901f94a3609ac0420f5ca02af20eb8dae5cbf3408", + "adapterSha256": "4e942ddfbaa0ba6bfc2993969276987f6528ac53765d125e99a830e261f93a8e", "scenarioSha256": "fa0e28a167a5fba6fe7ffebb9f4ad28dd413d601c07116a24a7781d156f54beb", "platform": "darwin", "scenarioVersion": 1, diff --git a/mobile/rpc-foundation/goldens/matrix-worktree.retired-names-worktree.listretirednames-1.json b/mobile/rpc-foundation/goldens/matrix-worktree.retired-names-worktree.listretirednames-1.json index 22a3c430775..8fac9985a5e 100644 --- a/mobile/rpc-foundation/goldens/matrix-worktree.retired-names-worktree.listretirednames-1.json +++ b/mobile/rpc-foundation/goldens/matrix-worktree.retired-names-worktree.listretirednames-1.json @@ -6,7 +6,7 @@ "baseline": "e7206f62a827f4fe0a2badf3eecd2167b8ac4285", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", "recorderSha256": "e044443e01f282566a5e0eff110d0861d2a1047ca8d8696a2e7ff914f51957f3", - "adapterSha256": "6119d409e1958877e4a04b3901f94a3609ac0420f5ca02af20eb8dae5cbf3408", + "adapterSha256": "4e942ddfbaa0ba6bfc2993969276987f6528ac53765d125e99a830e261f93a8e", "scenarioSha256": "d321d6c17e67ae90f6ceefb775495ff765a86a35423ed33a212e71ec5e9e94aa", "platform": "darwin", "scenarioVersion": 1, diff --git a/mobile/rpc-foundation/goldens/worktree-catalog-snapshot.json b/mobile/rpc-foundation/goldens/worktree-catalog-snapshot.json index 5c5c7e51c14..73aba616be1 100644 --- a/mobile/rpc-foundation/goldens/worktree-catalog-snapshot.json +++ b/mobile/rpc-foundation/goldens/worktree-catalog-snapshot.json @@ -6,7 +6,7 @@ "baseline": "e7206f62a827f4fe0a2badf3eecd2167b8ac4285", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", "recorderSha256": "e044443e01f282566a5e0eff110d0861d2a1047ca8d8696a2e7ff914f51957f3", - "adapterSha256": "6119d409e1958877e4a04b3901f94a3609ac0420f5ca02af20eb8dae5cbf3408", + "adapterSha256": "4e942ddfbaa0ba6bfc2993969276987f6528ac53765d125e99a830e261f93a8e", "scenarioSha256": "d2947158840576cbd0f0604ed3d37b0f446c63c6d439b4d1b7def7fe8524523d", "platform": "darwin", "scenarioVersion": 1, @@ -60,18 +60,33 @@ "admitted": "unadmitted", "fetched": "unfetched" }, - "2d40d1d38104": { - "name": "unhandled-rejection", - "value": { - "category": "Error", - "isRpcDeliveryUnknown": false, - "message": "Unsupported observation: function" - } - }, "9270aeb7d9c6": { "status": "pending", "startedAt": 0 }, + "9948855e8b8d": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "kind": "response", + "pending": { + "admission": { + "kind": "full", + "snapshotId": "snapshot-1", + "worktrees": [ + { + "displayName": "One", + "repo": "Repo", + "worktreeId": "w-1" + } + ] + }, + "client": "logical-client", + "hostId": "host-1" + } + } + }, "a87f1f91dc98": { "name": "worktree.ps#1", "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"worktree.ps\",\"params\":{\"limit\":10000,\"afterSnapshotId\":null,\"supportsWorktreeVisibilitySourceDefaults\":true}}" @@ -153,10 +168,10 @@ "sender": ["227f9e3de4fa"], "payloads": ["a87f1f91dc98"], "settlements": { - "fetch": "9270aeb7d9c6" + "fetch": "9948855e8b8d" }, "state": "ab1a9ba6301c", - "effects": ["2d40d1d38104"] + "effects": [] } } ] diff --git a/mobile/rpc-foundation/goldens/worktree-home-catalog.json b/mobile/rpc-foundation/goldens/worktree-home-catalog.json index 81c34b37327..205b3f1751c 100644 --- a/mobile/rpc-foundation/goldens/worktree-home-catalog.json +++ b/mobile/rpc-foundation/goldens/worktree-home-catalog.json @@ -6,7 +6,7 @@ "baseline": "e7206f62a827f4fe0a2badf3eecd2167b8ac4285", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", "recorderSha256": "e044443e01f282566a5e0eff110d0861d2a1047ca8d8696a2e7ff914f51957f3", - "adapterSha256": "6119d409e1958877e4a04b3901f94a3609ac0420f5ca02af20eb8dae5cbf3408", + "adapterSha256": "4e942ddfbaa0ba6bfc2993969276987f6528ac53765d125e99a830e261f93a8e", "scenarioSha256": "4749bb3b871275ba08f026f9b6bcfd383605f443e7bba70a7f89175b91db6fa5", "platform": "darwin", "scenarioVersion": 1, diff --git a/mobile/rpc-foundation/goldens/worktree-retired-names.json b/mobile/rpc-foundation/goldens/worktree-retired-names.json index d6e508514d4..d327e416f94 100644 --- a/mobile/rpc-foundation/goldens/worktree-retired-names.json +++ b/mobile/rpc-foundation/goldens/worktree-retired-names.json @@ -6,7 +6,7 @@ "baseline": "e7206f62a827f4fe0a2badf3eecd2167b8ac4285", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", "recorderSha256": "e044443e01f282566a5e0eff110d0861d2a1047ca8d8696a2e7ff914f51957f3", - "adapterSha256": "6119d409e1958877e4a04b3901f94a3609ac0420f5ca02af20eb8dae5cbf3408", + "adapterSha256": "4e942ddfbaa0ba6bfc2993969276987f6528ac53765d125e99a830e261f93a8e", "scenarioSha256": "2faa07ee5f12b3ed584117359d3b7aeb9c78a04e8fc49e753372f8c3927a3740", "platform": "darwin", "scenarioVersion": 1, diff --git a/mobile/src/test-support/rpc-recording/adapters/worktree-catalog-mount-adapters.ts b/mobile/src/test-support/rpc-recording/adapters/worktree-catalog-mount-adapters.ts index 41283b2a41d..82bb78851a3 100644 --- a/mobile/src/test-support/rpc-recording/adapters/worktree-catalog-mount-adapters.ts +++ b/mobile/src/test-support/rpc-recording/adapters/worktree-catalog-mount-adapters.ts @@ -55,7 +55,8 @@ export function worktreeCatalogMountAdapters( fetched = result // Admitting is what advances the snapshot token a later poll sends back. admitted = snapshots.admit(result.kind === 'response' ? result.pending : null) - return result + // The pending catalog carries the live client, which the recorder cannot observe. + return projectObservable(result) }), state: () => projectObservable({ fetched, admitted }), dispose: () => {} diff --git a/mobile/src/test-support/rpc-recording/golden-recorder-failure-absence.test.ts b/mobile/src/test-support/rpc-recording/golden-recorder-failure-absence.test.ts new file mode 100644 index 00000000000..90b7494de85 --- /dev/null +++ b/mobile/src/test-support/rpc-recording/golden-recorder-failure-absence.test.ts @@ -0,0 +1,79 @@ +import { readdirSync } from 'node:fs' +import { resolve } from 'node:path' +import { describe, expect, it } from 'vitest' +import { readGolden } from './golden-recording' +import { OBSERVATION_FIELDS } from './golden-value-pool' +import type { Observation } from './recording-scenario' +import type { RecordedValue } from './recording-values' + +const root = resolve(import.meta.dirname, '../../../..') +const directory = + process.env.RPC_FOUNDATION_GOLDENS ?? resolve(root, 'mobile/rpc-foundation/goldens') + +/** `captureValue`'s two refusals. Neither is ever product behaviour. */ +const PROJECTION_REFUSALS = [ + 'Unsupported observation', + 'Observation requires an explicit projection' +] + +function refusalText(value: RecordedValue): boolean { + if (typeof value === 'string') { + return PROJECTION_REFUSALS.some((refusal) => value.includes(refusal)) + } + if (Array.isArray(value)) { + return value.some(refusalText) + } + return typeof value === 'object' && value !== null && Object.values(value).some(refusalText) +} + +function failures(at: string, observation: Observation): string[] { + const found: string[] = [] + for (const field of OBSERVATION_FIELDS) { + if (refusalText(observation[field])) { + found.push(`${at}.${field}: recorder refused to project a value`) + } + } + return found +} + +/** + * A refused projection settles as data — the throw is captured as an effect and the action stays + * `pending` — so `--record` writes it and the suite goes green over it. Two adapters shipped that + * way (#20667, and the worktree catalog), and a revert plus a re-record would restore either one + * silently. A detached rejection is not banned here: recording one is how a real main bug gets + * pinned, and `unhandled-recording.test.ts` pins the capture itself. + */ +describe('recorder failures never reach a golden', () => { + it('records no refused projection in any observation field', () => { + const ids = readdirSync(directory) + .filter((file) => file.endsWith('.json')) + .map((file) => file.replace(/\.json$/, '')) + + // Positive control: absence proves nothing unless the detector fires. `effects` carries the + // shape both real defects took — the refusal captured as the message of a recorded error. + const seeded: Observation = { + sender: [], + payloads: [], + settlements: { fetch: { status: 'pending' } }, + state: { fetched: 'Observation requires an explicit projection for non-plain objects' }, + effects: [ + { name: 'unhandled-rejection', value: { message: 'Unsupported observation: function' } } + ] + } + expect(failures('seeded', seeded)).toEqual([ + 'seeded.state: recorder refused to project a value', + 'seeded.effects: recorder refused to project a value' + ]) + + let checkpoints = 0 + const found = ids.flatMap((id) => + readGolden(directory, id).recording.checkpoints.flatMap((checkpoint) => { + checkpoints++ + return failures(`${id}/${checkpoint.id}`, checkpoint.observation) + }) + ) + expect(found).toEqual([]) + expect(ids.length).toBeGreaterThan(0) + expect(checkpoints).toBeGreaterThan(ids.length) + }) +})