Commit 2bdbef3
authored
fix(daemon): distrust post-gesture stability that matches the pre-gesture baseline (#1563)
* fix(daemon): distrust post-gesture stability that matches the pre-gesture baseline
#1542 defect 2: post-gesture-stabilization.ts treated two consecutive
matching AX-signature polls as proof the screen settled. On iOS's AX-free
synthesized gesture lane, XCTest's tree can serve a stale-but-internally-
consistent read for a window after a scroll/swipe, so that "match" can be
false: the daemon then evaluates pre-gesture node positions on the very
next interaction.
Fix: capture the interaction-surface signature before the gesture
dispatches (reusing session.snapshot, no extra capture), and when a quiet
poll-to-poll match still equals that baseline, don't trust it — keep
polling past the normal 1.5s deadline up to a bounded 3.5s cap. On cap
expiry with the signature still identical, accept the result (a genuine
no-op gesture is the honest answer) but flag it via a new
post_gesture_snapshot_stale_accept diagnostic so a stale-accept is
observable in ndjson.
Baseline comparison is subset-tolerant (interactionSurfaceMatchesBaseline)
rather than whole-array equality: the pre-gesture baseline and the
post-gesture capture are routinely fetched with different snapshot scopes,
so naive equality reported "changed" from scope drift alone and never
caught the real staleness on first implementation — live-verified and
fixed before shipping.
Platform-scoped to Apple only (requiresPostGestureBaselineDistrust):
Android's persistent helper clears its accessibility-node cache before
every capture (AccessibilityTreeCapture.capture, #1254/#1259), so an
Android post-gesture read is fresh by construction and never computes a
baseline signature — latency and semantics unchanged, confirmed live
(checkout-form-android.ad + gesture-lab-android.ad 2/2 on Pixel_7_CI).
Does not close #1542: live validation on checkout-form.ad still fails at
step 11, but now for a distinct reason this fix correctly surfaces rather
than causes — a corrupted ScrollView-ancestor viewport frame
((18,381,366,109) vs the true (18,62,366,729)) that the off-screen guard's
findNearestScrollableAncestorRect trusts, independent of whether the
signature matches the baseline. gesture-lab.ad (iOS) remains 2/2 clean,
confirming no regression on the passing scenario.
_Generated by Claude Code_
* refactor(daemon): decompose the stabilization loop's diagnostics and capture pair
The distrust integration pushed capturePostGestureStabilizedResult over the
complexity gate (cyclomatic 15, cognitive 24); extracting the settle-diagnostic
branching and the capture+signature pair restores a clean fallow pass with no
behavior change.
* fix(daemon): require discriminating overlap for a post-gesture baseline match
PR review on #1563 (P1): interactionSurfaceMatchesBaseline returned true
whenever ANY shared entry was frozen, including the application/window
viewport root, whose rect is invariant under any gesture. In the exact
scope-drift case this PR supports, a broad pre-gesture baseline and a
narrow post-gesture selector capture can share only that root after a
real, successful scroll — the boolean predicate called that a baseline
match and extended the interaction to the 3.5s stale-read cap on zero
real evidence.
Fix: replace the boolean with classifyBaselineSurfaceEvidence, a
subset-tolerant classifier reusing this module's existing
InteractionSurfaceChange vocabulary ('changed' | 'unchanged' |
'ambiguous') instead of a bespoke boolean or an Application-only special
case. An entry only counts as evidence when it is `discriminating` —
excludes the viewport root (minimal local equivalent of
snapshot-occlusion.ts's isViewportRoot) and keyboard chrome (minimal
local equivalent of snapshot-chrome.ts's keyboard-container check), both
computed once at signature-build time since the flat signature-entry
representation has no ref/parentIndex to reuse those modules' full
ancestor-walk classifiers directly. Zero discriminating overlap is now
'ambiguous' (insufficient evidence) rather than a match, and
decidePostGestureStabilityVerdict falls through 'ambiguous' to 'trust' —
the safe default, same as 'changed'.
Tests: the reviewer's exact shape (signatures sharing only the
Application root, with the real content swapped) at three layers —
classifyBaselineSurfaceEvidence directly, decidePostGestureStabilityVerdict,
and the full capturePostGestureStabilizedResult async loop (proving no
cap-tax: settles in 2 capture attempts, not 3.5s). Also: root+one real
element both frozen still matches (guards against over-excluding), and
keyboard chrome excluded from discriminating overlap. All prior tests
kept green unchanged.
Counterfactual: reverted to the old boolean predicate and reran — 5
tests went red, including the async regression test, which didn't just
fail an assertion but timed out after 5s because the boolean predicate
extended the interaction to the 3.5s distrust cap the test's 1s timer
advance never covered — exactly the "extends to cap" failure mode the
review predicted. Restored, 37/37 green.
_Generated by Claude Code_
* fix(daemon): exclude keyboard descendants (not just the container) from baseline evidence
PR review on #1563 (two findings, blocking merge):
1. isKeyboardChromeKind excluded only the [Keyboard] container node itself.
collectKeyboardChrome (src/core/snapshot-chrome.ts, the established
source of truth) classifies the WHOLE keyboard window/subtree — keys,
AND the "Next keyboard"/"Dictate" assistant buttons, which are documented
siblings of the container, not descendants, so a container-descendant
walk alone provably misses them. In the scope-drift case this PR
supports, a successful scroll can leave only those keyboard descendants
shared between a baseline and a later capture, and the narrower check
called that a baseline match — extending a fresh result to the 3.5s
stale-read cap.
Fixed by exporting a narrow predicate, collectKeyboardChromeRefs(nodes),
from snapshot-chrome.ts (returns collectKeyboardChrome(nodes).refs, no
Android union — this caller has no appBundleId in scope and only needs
the iOS half). buildInteractionSurfaceSignature computes it once per
signature build and threads it into buildInteractionSurfaceEntry, so
discriminating is now `!isViewportRootKind(node) && !keyboardChromeRefs
.has(node.ref)` — reusing the real ancestor-walk classification instead
of a per-node type check, no ancestry needed in the signature entries
themselves.
2. post-gesture-stabilization.test.ts had grown to 550 LOC, past the
repository's 500-line extraction tripwire (AGENTS.md: "past 500,
extract before adding behavior... Tests are not exempt"). Split along
subject lines: the pure decidePostGestureStabilityVerdict coverage
moved to a new sibling post-gesture-stabilization-verdict.test.ts, and
shared fixtures (pickupSnapshot, deliverySnapshot, applicationRootNode,
keyboardWindowNodes, makeSession) moved to a new non-test
post-gesture-stabilization-fixtures.ts. The async capturePostGesture-
StabilizedResult loop tests stay in the original file. Assertions
unchanged, only relocation, plus the new regression tests below.
Resulting LOC: post-gesture-stabilization.test.ts 381, -verdict.test.ts
208, -fixtures.ts 129 (interaction-outcome-policy.test.ts grew to 413,
still under the tripwire).
Tests: the reviewer's exact regression — a shared overlap consisting only
of keyboard descendants (a key + the "Next keyboard" sibling button, NOT
the container) plus real content that changed (Pickup -> Delivery) — at
three layers: classifyBaselineSurfaceEvidence directly (ambiguous), the
verdict function (trust, elapsedMs: 0), and the full async capture loop
(settles in 2 attempts, no cap tax).
Counterfactual: reverted isNonDiscriminatingSurfaceNode to a container-only
check (normalizeType(node.type) === 'keyboard') and reran — 3 of the new
tests went red across all three layers, including the async test, which
timed out after 5s (not just a failed assertion) because the container-only
exclusion genuinely extended the interaction to the 3.5s distrust cap the
test's 1s timer advance never covers — the same "extends to cap" failure
shape as the review's finding 1. Restored, 40/40 green.
_Generated by [Claude Code](https://claude.ai/code)_1 parent 1235216 commit 2bdbef3
9 files changed
Lines changed: 1258 additions & 35 deletions
File tree
- src
- core
- daemon
- __tests__
- handlers/__tests__
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
325 | 325 | | |
326 | 326 | | |
327 | 327 | | |
| 328 | + | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
| 335 | + | |
| 336 | + | |
| 337 | + | |
| 338 | + | |
| 339 | + | |
| 340 | + | |
| 341 | + | |
| 342 | + | |
| 343 | + | |
| 344 | + | |
| 345 | + | |
| 346 | + | |
| 347 | + | |
328 | 348 | | |
329 | 349 | | |
330 | 350 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
3 | 3 | | |
4 | 4 | | |
5 | 5 | | |
| 6 | + | |
6 | 7 | | |
7 | 8 | | |
8 | 9 | | |
| |||
38 | 39 | | |
39 | 40 | | |
40 | 41 | | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
41 | 302 | | |
42 | 303 | | |
43 | 304 | | |
| |||
128 | 389 | | |
129 | 390 | | |
130 | 391 | | |
| 392 | + | |
| 393 | + | |
| 394 | + | |
| 395 | + | |
| 396 | + | |
| 397 | + | |
| 398 | + | |
| 399 | + | |
| 400 | + | |
| 401 | + | |
| 402 | + | |
| 403 | + | |
| 404 | + | |
| 405 | + | |
| 406 | + | |
| 407 | + | |
| 408 | + | |
| 409 | + | |
| 410 | + | |
| 411 | + | |
| 412 | + | |
| 413 | + | |
0 commit comments