test(mobile): record the real worktree catalog snapshot result in the RPC recorder (#20873)

* test(rpc-recording): record the real worktree catalog snapshot result

`worktree.catalog-snapshot`'s action returned `WorktreeCatalogSnapshotClient.fetch`'s
raw result. That value nests the live `RpcClient` under `pending.client`, so
`captureValue` threw `Unsupported observation: function`: both goldens baked an
`unhandled-rejection` effect and left the action's settlement `pending`, proving
nothing about what the fetch returns.

Project the result through `projectObservable` on the settlement path, the same
way `state()` already shows it and the same way #20667 fixed
`transport.pairing-race`. Rejections still propagate unchanged, so the two
transport-rejection matrix partitions keep their recorded errors.

The matrix golden now discriminates all eleven reply partitions: a full
admission, six invalid ones, three `request_failed` codes and two rejections.
`recorderSha256` does not move; `adapterSha256` moves on the six goldens mounted
through this adapter module, four of which have no other change.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(rpc-recording): fail the suite when a golden records a recorder failure

A projection refusal settles as data, not as a failed run: `captureValue` throws
inside the action, the recorder captures it as an `unhandled-rejection` effect,
and `--record` writes a green golden whose action never settled. The class has
landed twice — `transport.pairing-race` in #20667 and `worktree.catalog-snapshot`
in the previous commit — and reverting either one plus a re-record would go green
with the broken golden back.

Read every golden and fail on an `unhandled-rejection` effect or on either of
`captureValue`'s refusal texts in any observation field. A positive control in the
same case asserts both detectors fire, so the absence claim is load-bearing rather
than vacuous.

Not a recording driver, so `recorderSha256` excludes it and no golden moves.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(rpc-recording): ban only the recorder's own refusals, not product bugs

The gate also failed on any effect named `unhandled-rejection`, which bans a real
observation: detached-rejection capture exists to pin a main bug in a golden, and
`unhandled-recording.test.ts` pins the capture precisely because no golden carries
one today. Banning the name would make recording a genuine product failure a test
failure.

Drop that detector. `refusalText` alone catches all seven checkpoints of the
worktree-catalog regression, because the refusal is the message of the captured
error rather than the effect's name. The positive control now seeds that shape —
the refusal inside a recorded error under `effects` — so the surviving detector is
still proven to fire. Vacuity guards on golden and checkpoint counts are unchanged.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(rpc-recording): re-record the worktree catalog goldens over main's engine digest

#20874 re-digested `recorderSha256` on all 509 goldens, so the six goldens mounted
through the worktree catalog adapter had to be recorded again from the pin rather
than merged. Bodies and `adapterSha256` are unchanged from the pre-merge recording.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
This commit is contained in:
Jinwoo Hong
2026-09-15 18:37:10 -04:00
committed by GitHub
parent 1457d3966c
commit 005616171d
8 changed files with 159 additions and 34 deletions
@@ -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": []
}
},
{
@@ -6,7 +6,7 @@
"baseline": "e7206f62a827f4fe0a2badf3eecd2167b8ac4285",
"lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571",
"recorderSha256": "e044443e01f282566a5e0eff110d0861d2a1047ca8d8696a2e7ff914f51957f3",
"adapterSha256": "6119d409e1958877e4a04b3901f94a3609ac0420f5ca02af20eb8dae5cbf3408",
"adapterSha256": "4e942ddfbaa0ba6bfc2993969276987f6528ac53765d125e99a830e261f93a8e",
"scenarioSha256": "fa0e28a167a5fba6fe7ffebb9f4ad28dd413d601c07116a24a7781d156f54beb",
"platform": "darwin",
"scenarioVersion": 1,
@@ -6,7 +6,7 @@
"baseline": "e7206f62a827f4fe0a2badf3eecd2167b8ac4285",
"lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571",
"recorderSha256": "e044443e01f282566a5e0eff110d0861d2a1047ca8d8696a2e7ff914f51957f3",
"adapterSha256": "6119d409e1958877e4a04b3901f94a3609ac0420f5ca02af20eb8dae5cbf3408",
"adapterSha256": "4e942ddfbaa0ba6bfc2993969276987f6528ac53765d125e99a830e261f93a8e",
"scenarioSha256": "d321d6c17e67ae90f6ceefb775495ff765a86a35423ed33a212e71ec5e9e94aa",
"platform": "darwin",
"scenarioVersion": 1,
@@ -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": []
}
}
]
@@ -6,7 +6,7 @@
"baseline": "e7206f62a827f4fe0a2badf3eecd2167b8ac4285",
"lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571",
"recorderSha256": "e044443e01f282566a5e0eff110d0861d2a1047ca8d8696a2e7ff914f51957f3",
"adapterSha256": "6119d409e1958877e4a04b3901f94a3609ac0420f5ca02af20eb8dae5cbf3408",
"adapterSha256": "4e942ddfbaa0ba6bfc2993969276987f6528ac53765d125e99a830e261f93a8e",
"scenarioSha256": "4749bb3b871275ba08f026f9b6bcfd383605f443e7bba70a7f89175b91db6fa5",
"platform": "darwin",
"scenarioVersion": 1,
@@ -6,7 +6,7 @@
"baseline": "e7206f62a827f4fe0a2badf3eecd2167b8ac4285",
"lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571",
"recorderSha256": "e044443e01f282566a5e0eff110d0861d2a1047ca8d8696a2e7ff914f51957f3",
"adapterSha256": "6119d409e1958877e4a04b3901f94a3609ac0420f5ca02af20eb8dae5cbf3408",
"adapterSha256": "4e942ddfbaa0ba6bfc2993969276987f6528ac53765d125e99a830e261f93a8e",
"scenarioSha256": "2faa07ee5f12b3ed584117359d3b7aeb9c78a04e8fc49e753372f8c3927a3740",
"platform": "darwin",
"scenarioVersion": 1,
@@ -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: () => {}
@@ -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)
})
})