fix(wait): retire native selector bypass and recover text observations - #2440
Conversation
|
Size Report
Startup median (7 runs, lower is better):
|
thymikee
left a comment
There was a problem hiding this comment.
Thermo-nuclear code quality review
The behavioral fix is right and the live validation is convincing. But the PR retires an operation by disabling it rather than deleting it, and it leaves the identical defect live on the sibling port. Requesting changes on two structural items.
1. findSelector is now globally dead. Delete the operation, don't flip a fact to false.
After this diff there is not one producer of findSelector left in the repo. Every owner declares it unavailable:
| owner | declaration |
|---|---|
| apple | nativeSelectorUnavailable (this PR) |
| android | snapshotKindUnavailable |
| harmonyos | snapshotKindUnavailable |
| web | openTargetKindUnavailable |
| limrun | customSnapshotUnavailable / liveSessionUnavailable |
All five are unconditional constants, not device-dependent cells. Interactor.findSelector? has zero implementers. PlatformRuntimeOperations.findSelector has zero binders. So waitOperations?.findSelector is now permanently undefined, which makes dispatchConditionalWaitIfNeeded in wait-runtime.ts a permanent no-op and wait-conditional-selector.ts unreachable in its entirety.
The PR description says "Other runtime owners retain their observation contracts." They retain a declaration that the operation does not exist. That is not a contract, it is a row.
What survives, alive and unreachable:
src/daemon/wait-conditional-selector.ts— the whole 64-line modulesrc/daemon/wait-runtime.ts—dispatchConditionalWaitIfNeededplus its import and call sitesrc/daemon/selector-operation-binding.ts—BoundNativeSelectorReadand theselectedSelectorhalf ofselectWaitObservationOperations, which then collapses to a one-key projection shaped exactly likeselectElementTextOperationright above itsrc/daemon/selector-capture-binding.tsandsrc/daemon/snapshot-runtime-binding.ts— three threading lines eachpackages/contracts—FindSelectorInput,FindSelectorResult,FindSelectorRuntimeOperations, the facts key, the operation name, bothconditional: ['findText', 'findSelector']plan uses, and the unavailable-table rowpackages/contracts/src/interactor-types.ts— the optionalfindSelector?methodpackages/platform-apple/src/runner-demand.ts— the demand classification- six owner fact declarations
That is roughly 200 lines of production plumbing kept alive to express "nobody does this," plus a 201-line test file (below) that will guard it indefinitely.
The code-judo move is to delete the operation end to end. This repo has already done exactly that once: the note in ios-world.ts records that R37 retired the same querySelector shortcut for is, and the scripted transcript is the proof the bypass is gone. Finish the same job for wait. The deletion is mechanical, the type checker walks you through it, and it is strictly smaller than the state this PR leaves behind.
To be precise about scope, this is not an argument to delete the runner's querySelector command or queryAppleRunnerSelector. Both stay: the direct-iOS tap path and the offscreen target probe still use them legitimately. Only the runtime operation is dead.
2. findText is the surviving twin of this exact bug, and the PR does not close the class.
interactor.findText calls runAppleRunnerCommand({ command: 'findText' }) — the XCTest runner, same as the retired querySelector did. Nothing between bindAppleFindTextRuntime and wait-text.ts catches a throw:
runtime-snapshot.tsawaitsinteractor.findText(...)bareselector-runtime-backend.tsforwards it barewait-text.tsawaitsruntime.backend.findText(...)bare, inside the poll loop
So on a live-but-poisoned runner — precisely the state in #2404, where the error was XCTest recorded a failure while executing querySelector — a wait text <value> still fails outright while a capture would have satisfied it. hasLiveRunnerSession gates startup, not health, so the admission check does not cover this.
The doc block on bindAppleFindTextRuntime states the invariant explicitly: "All report found: false — 'not proven here' — never an error, so the caller's canonical tree remains the complete path (ADR 0019 section 2)." Today that invariant holds only for the admission conditions the author remembered to enumerate. A runner-level throw walks straight past it.
Right now the invariant is enforced by taste. Make it structural: the one place that forwards a native observation should be total — any failure degrades to { found: false } and the poll consults the canonical tree. One try/catch at the single forwarding seam, and the defect becomes unrepresentable for every current and future native find port, instead of being re-fixed port by port.
Related, and probably the actual mechanism worth putting in the PR description: runner-demand.ts classifies both findText and findSelector as 'simulator', meaning "served by simctl or the host AX bridge and never waits for runner readiness." Both implementations call the runner. readTextAtPoint, which also calls the runner, is correctly 'runner'. That misclassification is a plausible root cause for the wait plan never guaranteeing a healthy runner in the first place. If it is, the same question applies to the rest of the table.
3. wait-conditional-selector.test.ts now tests a shape the product cannot produce.
The 201-line harness hand-forges a DeviceBinding with findSelector available and bound. No owner can produce that binding any more. The new case added here, "an unavailable native selector query cannot fail a capture-backed wait," asserts findSelector was not called — which is now true by construction for every input, so the test can no longer fail.
This is what keeps dead code alive: the module is unreachable, but the suite proves it works, so nothing flags it. Delete the file along with the module in item 1.
4. The docs line documents a transient state.
"Apple selector waits use snapshot capture, including steps inside replay" singles out Apple and implies other platforms differ. After this PR no platform has a native selector wait, so the Apple qualifier is wrong the moment it lands. Either state the behavior platform-neutrally, or drop the sentence — with the operation deleted, capture-backed is simply what wait <selector> means, and there is nothing left to contrast it with.
Verdict
The behavior change is correct and well validated. But per the approval bar: there is a visible path that deletes a whole category of complexity instead of rearranging it, and it is not taken; and the fix addresses one instance of a defect whose sibling is still live and reachable. Items 1 and 2 are blocking. Items 3 and 4 fall out of item 1.
daf47bc to
985701a
Compare
|
Addressed all four review items in 985701a; the three inline threads have detailed replies. For item 2, the sibling bug is now reproduced and fixed: standalone and replayed Also corrected Validation: |
|
No remaining findings at 985701a. Selector waits and replay now use canonical capture; native text failures defer to capture without hiding cancellation or capture errors. The reported iOS verification covers the changed route, and CI is green. Ready for human review. |
Summary
Selector waits could enter XCTest
querySelectorbefore canonical capture and fail whileissucceeded. Replay inherited the normal wait route.Delete the unused
findSelectorruntime operation end to end: contracts, owner facts, binding plumbing, dispatch, and unreachable tests. Selector waits now use canonical capture across platforms. The runner's interaction-facing selector command remains intact.Native text-observation failures now defer to capture at the shared wait binding; cancellation and canonical capture failures remain visible. Correct
findTextrunner-demand classification without adding conditional observations to required execution plans.33 files changed, primarily removal of dead plumbing. Closes #2404.
Validation
985701a340:pnpm check:affected --runpassed, including 3,753 related tests, 12 documentation tests, format, lint, typecheck, layering, DI, Fallow, and build.