Skip to content

fix(snapshot): clip oversized system alert content - #3857

Merged
thymikee merged 6 commits into
mainfrom
t3/clip-dynamic-type-snapshots
Oct 21, 2026
Merged

thymikee merged 6 commits into
mainfrom
t3/clip-dynamic-type-snapshots

Conversation

@thymikee

@thymikee thymikee commented Oct 20, 2026 •

Copy link
Copy Markdown
Member

Summary

Clip regular iOS and Android snapshot geometry to the cumulative visible area, omit fully outside nodes, and publish typed clipping counts independently of capture quality. Raw snapshots retain reported geometry. Refs #3801 and #1973.

The runner wire now carries sibling snapshotQualityNotes; modal queries invent no verdict/backend. Genuine capture-plan verdicts remain intact. Complete scoped-modal evidence permits an empty projection. Shared annotation parsing bounds notes.

45 files across presenters, conformance, metadata, tests, and docs. Scope includes Android omission and end-to-end metadata carry.

Examples: agent-device snapshot -i --json; agent-device snapshot --raw.

Validation

Tested SHA: c8ec2a4b914140e93971c717cb82bef97cffdad9.

Reader regression: base a3893a57 2 failed/24 passed; fixed 26 passed. Provider scope regression failed before the follow-up; fixed suite 16 passed. Projection suites: 66 passed. Native wire tests: 4 passed. Fresh independent Sol review: no findings after fixing scoped admission.

Live iPhone17e/iOS27: ordinary modal no verdict; clipped modal independent counts; raw geometry retained; genuine app verdict retained; denial action succeeded.

Exact affected receipt and live evidence. CI owns full coverage, provider integration, native/device lanes. Note-population policy remains pending; existing population is unchanged. iOS17.5/action sheets were not rerun.

@thymikee

thymikee commented Oct 20, 2026 •

Copy link
Copy Markdown
Member Author

Live verification and regression evidence:

  • Base: f8c60ca188f62e110bdc1d3e6225b134960886c0 (isolated checkout, frozen install/build, dedicated base runner/daemon).
  • Runtime head: 3c478a708771bcc8ea448ef5216d5956ed1e656e. Follow-up 7815df2bc0b650b038e7a5f346c7fe5b65b25637 adds only JSON/digest carry-through fixes and tests; final a3893a57bc4165532961bbe25c378285e050240a uses the shared annotation builder for state quality metadata to satisfy the complexity gate.

iOS changed route

Device: iPhone 17e, iOS 27.0, F25C7515-D67E-4838-BEAC-0AD08DB056EC.
Native verification app com.callstack.clip3801 requests ATTrackingManager.requestTrackingAuthorization after its first scene appears, with NSUserTrackingUsageDescription set.

xcrun simctl ui F25C7515-D67E-4838-BEAC-0AD08DB056EC content_size accessibility-extra-extra-extra-large
xcrun simctl privacy F25C7515-D67E-4838-BEAC-0AD08DB056EC reset all com.callstack.clip3801
agent-device open com.callstack.clip3801 --relaunch
agent-device alert wait 10000
agent-device snapshot -i --json

The last command fails on the named base with reason: regular-node-outside-cumulative-clip, index 3, frame {x:48,y:170.33333333333337,width:294,height:621.3333333333333}, clip {x:18,y:70.66666666666669,width:354,height:716}.

The changed source runner/daemon returns a regular snapshot successfully. snapshotQualityNotes reports [{code:"regular-geometry-clipped",clippedNodeCount:1,droppedNodeCount:1}]; both visible ATT buttons stay hittable. The overflowing text height becomes 616.3333333333334. Raw retains the reported height and outside content and carries no clipping note.

agent-device find text 'Ask App Not to Track' click
agent-device wait text 'Tracking status: 2'

Both succeed: find taps @e2 at (195,544), and wait confirms the app's denied tracking state (45 ms on the final run).

Android changed route

Device: ReactNative_API_35, Android 15, emulator-5554.
A native accessibility provider exposes a scroll container [50,200][650,700], a visible action, a partially clipped action [70,650][400,750], and an entirely outside action [70,800][400,900] (positive reported frames, visible-to-user=true).

Source capture uses androidSnapshot.backend: android-helper, helperVersion: 0.21.30-dev (matches package.json).
Regular output clips partial height 100 to 50, omits the outside action, and publishes the same typed counts (1 clipped, 1 dropped). Raw retains heights 100 and both actions, with no clipping note.

All app-driving commands used device_open's exact launcher/config/session/target flags, with an endpoint override to the isolated source daemon. The installed 0.21.25 CLI predates the new public field, so a two-step batch (regular/raw snapshot) preserved the source daemon JSON for metadata inspection. Final CLI JSON/digest serialization is covered by the regression below.

Tests and review

  • swift test --package-path apple/snapshot-presentation: 22 passed.
  • Unit-enabled iOS runner build: succeeded.
  • Targeted simulator XCTest: 3 passed (testBlockingSystemModalClipsOverflowAndDisclosesItWithoutRecovery, testResolvedBlockingSystemModalPayloadStatesAPresentedSurface, testSnapshotPresentationMatchesSharedRawToPresentedFixture). The shared table checks geometry and clipping-note counts.
  • Focused TypeScript runtime/Android suites: 53 passed; Apple runner presentation suite: 25 passed.
  • New Android conformance row on unchanged base: 1 failed (outside action wrongly retained); head: passed.
  • New host-iOS notes regression on unchanged base: 1 failed (notes absent); revised head: passed across Simulator AX bridge, Appium, and Limrun inputs, state assembly, and public annotation round-trip, without invented verdicts.
  • JSON/digest regressions at 3c478a7087: 2 failed / 46 passed (notes dropped); at 7815df2bc0: 48 passed.
  • Independent Sol review rounds 1 and 2: all three P2 findings accepted and fixed. Sol round 3 reviewed 3c478a7..7815df2 and its annotation/client/CLI/digest interactions: no findings. Final Sol round 4 reviewed 7815df2..a3893a5: no findings; omitted/empty notes, healthy verdicts, and raw semantics remain unchanged.

Cleanup: both exact owned sessions closed; isolated source/base daemons stopped; global claims empty, hiddenStaleClaims=0. Both panels detached without shutdown. The disposable base checkout was removed. No tags pushed, no merge or release performed.

Exact-head gate receipt summary (all runnable checks passed; 1,364 files / 12,199 tests):

{
  "version": 1,
  "status": "passed",
  "exitCode": 0,
  "refs": {
    "base": "origin/main",
    "head": "a3893a57bc4165532961bbe25c378285e050240a",
    "resolvedBase": "f8c60ca188f62e110bdc1d3e6225b134960886c0",
    "resolvedHead": "a3893a57bc4165532961bbe25c378285e050240a",
    "mergeBase": "f8c60ca188f62e110bdc1d3e6225b134960886c0"
  },
  "checkoutChanged": false,
  "exactCommit": true,
  "results": [
    {
      "id": "format",
      "status": "passed",
      "exitCode": 0
    },
    {
      "id": "lint",
      "status": "passed",
      "exitCode": 0
    },
    {
      "id": "typecheck",
      "status": "passed",
      "exitCode": 0
    },
    {
      "id": "layering",
      "status": "passed",
      "exitCode": 0
    },
    {
      "id": "fallow",
      "status": "passed",
      "exitCode": 0
    },
    {
      "id": "build",
      "status": "passed",
      "exitCode": 0
    },
    {
      "id": "ios-snapshot-differential",
      "status": "skipped",
      "reason": "GitHub-authoritative (jobs: Apple / Smoke Tests)"
    },
    {
      "id": "vitest-related",
      "status": "passed",
      "exitCode": 0
    },
    {
      "id": "unit",
      "status": "deduplicated",
      "reason": "covered by related tests"
    },
    {
      "id": "coverage",
      "status": "skipped",
      "reason": "GitHub-authoritative (jobs: Coverage)"
    },
    {
      "id": "provider-integration",
      "status": "deduplicated",
      "reason": "covered by related tests"
    },
    {
      "id": "swift-runner-ios",
      "status": "skipped",
      "reason": "GitHub-authoritative (jobs: Apple / Smoke Tests, Conformance Differential / iOS Conformance Differential, XCTest Nightly / iOS Runner Full XCTest Suite)"
    },
    {
      "id": "swift-runner-macos",
      "status": "skipped",
      "reason": "GitHub-authoritative (jobs: Apple / Smoke Tests)"
    },
    {
      "id": "web-smoke",
      "status": "skipped",
      "reason": "GitHub-authoritative (jobs: Apple / Smoke Tests, Integration Tests, Lint, Typecheck & Package)"
    },
    {
      "id": "docs-links",
      "status": "passed",
      "exitCode": 0
    },
    {
      "id": "docs-prose",
      "status": "passed",
      "exitCode": 0
    },
    {
      "id": "xctest-selection",
      "status": "passed",
      "exitCode": 0
    },
    {
      "id": "packaged-runner-swift",
      "status": "passed",
      "exitCode": 0
    },
    {
      "id": "replay-ios",
      "status": "skipped",
      "reason": "parked, workflow_dispatch only (Replay Manual / iOS Replay Suite)"
    },
    {
      "id": "replay-ios-device",
      "status": "skipped",
      "reason": "parked, workflow_dispatch only (Replay Manual / iOS Replay Suite)"
    },
    {
      "id": "replay-macos",
      "status": "skipped",
      "reason": "GitHub-authoritative (jobs: Apple / Smoke Tests)"
    },
    {
      "id": "replay-linux",
      "status": "skipped",
      "reason": "GitHub-authoritative (jobs: Linux / Smoke Tests)"
    },
    {
      "id": "linux-command-evidence",
      "status": "skipped",
      "reason": "GitHub-authoritative (jobs: Linux / Smoke Tests)"
    },
    {
      "id": "replay-android",
      "status": "skipped",
      "reason": "parked, workflow_dispatch only (Replay Manual / Android Full Emulator Suite)"
    }
  ]
}

@github-actions

github-actions Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-10-21 08:21 UTC

@github-actions

github-actions Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Size Report

Metric Base Current Diff
Installed (including dependencies) 5.48 MB 5.48 MB +5.3 kB
Package (unpacked) 5.48 MB 5.48 MB +5.3 kB
Package (download) 1.66 MB 1.66 MB +1.4 kB

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 39.7 ms 39.7 ms +0.0 ms
CLI --help 79.5 ms 82.8 ms +3.3 ms

@thymikee

thymikee commented Oct 20, 2026 •

Copy link
Copy Markdown
Member Author

Independent smaller-design assessment for f8c60ca188f62e110bdc1d3e6225b134960886c0..a3893a57bc4165532961bbe25c378285e050240a, required by the CI Size report: +5.5 kB installed/unpacked and +1.4 kB downloaded.

A separate Sol reviewer examined the complete range in an isolated checkout and found no actionable smaller-design findings. Exact diff scope accounting:

  • Production: 25 files, +270/-51, net +219 lines.
  • Tests: 14 files, +224/-35, net +189.
  • Conformance fixture: +164/-1, net +163.
  • Docs: +17/-2, net +15.

The reviewer compared these alternatives:

  • An alert-only clamp would duplicate geometry policy; the modal now reuses the existing regular/raw presenters.
  • Nesting notes in a verdict would require host iOS/Android producers to invent verdict/backend facts. Independent notes preserve those semantics.
  • Reconstructing notes after presentation loses dropped nodes and original geometry. Counting inside existing folds avoids another traversal or retained tree.
  • Replacing the small Swift nodes-plus-notes carrier with tuples/output parameters gives negligible savings.
  • Removing the bounded metadata validator weakens the untrusted-input boundary; inlining only relocates its checks.
  • Public projections already extend the existing capture-annotation, CLI, state, and digest owners. A generic helper for the remaining local comparisons/counters adds another abstraction.

No new dependencies, production modules, or competing presentation owners were introduced. The production growth is justified by the required cross-runtime geometry/disclosure contract. Exact per-file attribution of the package-byte delta was not measured. This was source/design review only; no source changes or additional test/build/device runs were made. Existing validation remains attributed to its recorded SHAs.

@thymikee

Copy link
Copy Markdown
Member Author

Thanks for the PR. At a3893a5 the clipping fix looks right, but the system-alert path now reports a backend it does not use. CI is green: 19 checks, none failing. Two things still need to happen before merge: fix the modal verdict below, and answer the question about the note population.

In RunnerTests+SystemModal.swift:98, blockingSystemModalPayload now attaches snapshotQuality {state: healthy, backend: 'tree'} to every blocking-modal payload, with or without notes. It does this only because the runner wire nests notes inside SnapshotQuality (RunnerTests+SnapshotCapturePlan.swift:42, read at snapshot-presentation.ts:94). The modal probe builds its nodes from XCUIElement queries, so it is not the recursive-tree backend, and before this PR the modal payload had no verdict. Every system-alert snapshot now reports a false backend in JSON and session state. That breaks the ADR 0004 rule that a producer without a verdict gets no invented state or backend. The fake verdict also suppresses the legacy detectors that run when no verdict exists. It also feeds snapshotSurfaceComparisonKey (interaction-surface-signature.ts:38) and the backend match in tap corroboration (interaction-ios-tap-outcome.ts:207), so a modal capture can look comparable to a real tree-backend app capture. The rule to satisfy is that notes travel independently of the verdict at every hop, the runner wire included. Please carry the notes as their own DataPayload key beside snapshotQuality, read that key in readAppleSnapshotResult, and drop the synthesized SnapshotQuality from blockingSystemModalPayload. Then no producer has to make up a verdict to carry notes.

Could we keep the modal probe routed through the shared presenters, send the notes as that top-level key, and decide once, in the conformance table, whether ordinary viewport clipping produces a note? If it does not, the counters shrink to overflow past an ancestor clip and most snapshots never carry the field. That needs a maintainer decision first: all clipping, or only overflow past a container clip. ADR 0004 should then be amended to match.

Not blocking, and you can take or leave these: the counter in SnapshotVisibilityFold.swift:192 also counts ordinary viewport and scroll-anchor clipping, so the maintainers should confirm that is the intended note population (or count only overflow past a non-viewport ancestor clip) in the conformance table, so all three runtimes follow one rule; the help text in src/commands/capture/snapshot.ts:79 and snapshots.md:246 say only regular iOS snapshots clip, while Android regular snapshots now also omit framed nodes with no visible area (ui-hierarchy-builder.ts:290), so both should name both families and the CHANGELOG should get a line for the Android omission; the new sameRect in geometry.ts:221 duplicates rectsEqual in ios-snapshot-engine/scroll.ts:220, sameRect in scroll-edge-state/selection.ts:215, and an inline copy in ui-hierarchy-builder.ts:310-316, so one helper exported from @agent-device/kernel/rect could serve all four sites; and readSerializedSnapshotCaptureAnnotations in snapshot-capture-annotations.ts:102 casts snapshotQualityNotes unchecked while the runner path validates the same shape with readSnapshotQualityNotes, so moving the bounded reader to contracts or kernel would let both call it.

I read the code and did not run the tests or a device. I judged regression validity by reading the pre-change code. The live iOS evidence in the PR covers iOS 27.0 only, and the iOS 17.5 case with the 5.5 pt overflow was not re-run. I did not trace how far the false modal verdict reaches in stability and corroboration decisions, so that impact is inferred from the consumers I read. I also did not check whether action-sheet modals place actions outside the resolved root frame, since using the modal root as the derived viewport would drop them. I did not check whether the Android Appium backend shares ui-hierarchy-builder and so also gets the omission.

@thymikee

thymikee commented Oct 20, 2026 •

Copy link
Copy Markdown
Member Author

Addressed the runner-wire blocker in the maintainer review.

Tested candidate: c8ec2a4b914140e93971c717cb82bef97cffdad9 (wire change 495c210be81118be80d681eb86fd44acc2cc2169, scoped-admission follow-up c8ec2a4b914140e93971c717cb82bef97cffdad9).

DataPayload.snapshotQualityNotes is now a sibling of snapshotQuality. The capture plan stamps genuine verdicts and notes separately. The modal query producer keeps the shared regular/raw presenters and supplies no invented verdict. readAppleSnapshotResult reads the sibling key. The existing bounded note reader now belongs to contracts and also validates serialized public annotations; the unchecked array cast is removed.

Fresh independent Sol review round 1 found a real P2: removing the fabricated verdict exposed the Simulator's empty-scope admission guard. The follow-up accepts complete unscoped modal evidence without a verdict; sparse verdicts, absent/empty/truncated evidence, missing modal evidence and unscoped empty results remain rejected. Shared presentation validation still runs. Fresh independent Sol round 2 traced the new admission, actual Swift modal wire, genuine verdict stamping, raw output, legacy detectors, comparison/corroboration, transport/state/client/CLI/digest and bounded parser: no actionable findings. Both reviews were static, read-only reviews and did not independently run tests/devices.

Regression evidence

  • On unchanged production base a3893a57bc4165532961bbe25c378285e050240a plus the reader test patch, pnpm exec vitest run --project apple-runner packages/platform-apple/src/runner/__tests__/snapshot-presentation.test.ts: 2 failed / 24 passed, because top-level notes are lost. With the wire fix: 26 passed. The disposable base checkout had its own frozen install/build and was removed.
  • The new provider-path scope regression against 495c210 production failed at the zero-node guard (1 failed / 15 skipped). After the admission fix, the full provider suite: 16 passed.
  • Focused annotation/verdict/CLI JSON/digest suites: 66 passed.
  • Unit-enabled iOS runner build succeeded. Four selected native XCTest cases passed: ordinary modal encoding without quality, clipped modal encoding/geometry/raw omission, presented-surface disclosure, and genuine capture-plan verdict/independent-note encoding.

Live changed route

Device: iPhone 17e / iOS 27.0 / F25C7515-D67E-4838-BEAC-0AD08DB056EC. Source built in this isolated worktree at c8ec2a4; explicit build:xcuitest:ios artifact and own dual-mode source daemon, state /tmp/agent-device-3857-wire-state. App: com.callstack.clip3801, native ATT verification app from the earlier evidence.

All app commands used device_open's exact launcher, UDID, config and session t3-4a897e8d0c7c0406cd75077c, with the own-daemon endpoint/token passed privately. The installed launcher is 0.21.25 and predates the new snapshot JSON field, so batch snapshot steps preserved source-daemon metadata for inspection; final source CLI JSON/digest are covered by the tests above.

  • content_size large: ordinary ATT regular snapshot has 5 nodes, no snapshotQuality and no snapshotQualityNotes. A missing scope succeeds with 0 nodes and no verdict/notes.
  • content_size accessibility-extra-extra-extra-large: regular ATT snapshot has 4 nodes, no snapshotQuality, and independent snapshotQualityNotes: [{code:"regular-geometry-clipped",clippedNodeCount:1,droppedNodeCount:1}]. Title height is clipped to 616.3333333333334; both buttons remain hittable.
  • Raw has 5 nodes, title height 621.3333333333333, retains the entirely outside informative text, and has neither verdict nor clipping notes.
  • A missing scope on the clipped modal succeeds with 0 nodes and the independent counts, without a verdict.
  • find text 'Ask App Not to Track' click succeeds at @e2 (195,544). wait text 'Tracking status: 2' succeeds (20 ms).
  • After dismissal, app regular and raw captures both retain a genuine healthy/tree verdict, with no clipping notes.

These runs cover iOS 27.0 ATT only; iOS 17.5 and action-sheet layouts were not rerun. Android behavior is unchanged by this follow-up and retains its earlier attributed evidence.

The first setup attempt failed before capture when a gate rebuild replaced generated bundle chunks used by an already-started daemon. Subsequent setup detected macOS cache metadata from using the both-target build command with one explicit override; the live run above used a stable completed TypeScript build and a separate explicit iOS build. Neither failed setup is counted as product capture evidence.

Cleanup: own session closed, own source daemons exited, owned exec completion confirmed, iPhone panel detached without shutdown, no owned claims, hiddenStaleClaims=0. The other worker's live Android claim was preserved. Source CLI daemon stop --clean could not verify the directly launched node process identity; only the known owned daemon PIDs were signalled. No claim/lock files were deleted. No tags, merge, release, approval or label change.

Clipping-note population remains a pending maintainer decision. This follow-up preserves the existing viewport/scroll/ancestor clipping population. It does not claim to settle the all-clipping versus ancestor-overflow question or its dependent ADR/conformance update.

Exact-commit receipt

pnpm check:affected --run --head c8ec2a4b914140e93971c717cb82bef97cffdad9: all runnable checks passed (1,363 files / 12,185 tests). Local receipt: .tmp/validation/run-20ebfJ/receipt.json. The earlier 495c210 receipt was intentionally cancelled before source edits (exit 130), not passed.

{
  "version": 1,
  "status": "passed",
  "exitCode": 0,
  "refs": {
    "base": "origin/main",
    "head": "c8ec2a4b914140e93971c717cb82bef97cffdad9",
    "resolvedBase": "bd3619b05153542c5eb94b3e8a3079608cd96f41",
    "resolvedHead": "c8ec2a4b914140e93971c717cb82bef97cffdad9",
    "mergeBase": "f8c60ca188f62e110bdc1d3e6225b134960886c0"
  },
  "checkoutChanged": false,
  "exactCommit": true,
  "results": [
    {
      "id": "format",
      "status": "passed",
      "exitCode": 0
    },
    {
      "id": "lint",
      "status": "passed",
      "exitCode": 0
    },
    {
      "id": "typecheck",
      "status": "passed",
      "exitCode": 0
    },
    {
      "id": "layering",
      "status": "passed",
      "exitCode": 0
    },
    {
      "id": "fallow",
      "status": "passed",
      "exitCode": 0
    },
    {
      "id": "build",
      "status": "passed",
      "exitCode": 0
    },
    {
      "id": "ios-snapshot-differential",
      "status": "skipped",
      "reason": "GitHub-authoritative (jobs: Apple / Smoke Tests)"
    },
    {
      "id": "vitest-related",
      "status": "passed",
      "exitCode": 0
    },
    {
      "id": "unit",
      "status": "deduplicated",
      "reason": "covered by related tests"
    },
    {
      "id": "coverage",
      "status": "skipped",
      "reason": "GitHub-authoritative (jobs: Coverage)"
    },
    {
      "id": "provider-integration",
      "status": "deduplicated",
      "reason": "covered by related tests"
    },
    {
      "id": "swift-runner-ios",
      "status": "skipped",
      "reason": "GitHub-authoritative (jobs: Apple / Smoke Tests, Conformance Differential / iOS Conformance Differential, XCTest Nightly / iOS Runner Full XCTest Suite)"
    },
    {
      "id": "swift-runner-macos",
      "status": "skipped",
      "reason": "GitHub-authoritative (jobs: Apple / Smoke Tests)"
    },
    {
      "id": "web-smoke",
      "status": "skipped",
      "reason": "GitHub-authoritative (jobs: Apple / Smoke Tests, Integration Tests, Lint, Typecheck & Package)"
    },
    {
      "id": "docs-links",
      "status": "passed",
      "exitCode": 0
    },
    {
      "id": "docs-prose",
      "status": "passed",
      "exitCode": 0
    },
    {
      "id": "xctest-selection",
      "status": "passed",
      "exitCode": 0
    },
    {
      "id": "packaged-runner-swift",
      "status": "passed",
      "exitCode": 0
    },
    {
      "id": "replay-ios",
      "status": "skipped",
      "reason": "parked, workflow_dispatch only (Replay Manual / iOS Replay Suite)"
    },
    {
      "id": "replay-ios-device",
      "status": "skipped",
      "reason": "parked, workflow_dispatch only (Replay Manual / iOS Replay Suite)"
    },
    {
      "id": "replay-macos",
      "status": "skipped",
      "reason": "GitHub-authoritative (jobs: Apple / Smoke Tests)"
    },
    {
      "id": "replay-linux",
      "status": "skipped",
      "reason": "GitHub-authoritative (jobs: Linux / Smoke Tests)"
    },
    {
      "id": "linux-command-evidence",
      "status": "skipped",
      "reason": "GitHub-authoritative (jobs: Linux / Smoke Tests)"
    },
    {
      "id": "replay-android",
      "status": "skipped",
      "reason": "parked, workflow_dispatch only (Replay Manual / Android Full Emulator Suite)"
    }
  ]
}

Pushed head: c8ec2a4b914140e93971c717cb82bef97cffdad9. Immediately before the single fast-forward push, origin was still a3893a57bc4165532961bbe25c378285e050240a. Push used git -c push.followTags=false push origin HEAD:refs/heads/t3/clip-dynamic-type-snapshots.

Publication verification: GitHub and git ls-remote both report c8ec2a4b914140e93971c717cb82bef97cffdad9. New-head CI is running (including Coverage, Integration Tests, Lint/Typecheck/Package); no current-head CI success is claimed here. Parent thread owns watches. Final source CLI device status reports claims: [], hiddenStaleClaims: 0; worktree is clean.

@thymikee

Copy link
Copy Markdown
Member Author

The code at c8ec2a4 looks right, and all 19 checks pass with none failing. There are no conflicts.

The earlier review at a3893a5 waited on evidence. That is now met by your quoted iOS 27.0 ATT run against a source daemon at c8ec2a4. I did not run the tests or a device myself. The client launcher in your run was 0.21.25, so the final CLI JSON rendering of snapshotQualityNotes is covered only by the snapshot-serialization tests. I did not re-run iOS 17.5 or the action-sheet layouts.

Not blocking, and you can take or leave it: Android now uses the same clipping as iOS through ui-hierarchy-builder.ts. It omits framed nodes that fall fully outside the clip, where the base kept them with a 0x0 rect. It also publishes a regular-geometry-clipped note. The help text and the snapshots.md paragraph say only "Regular iOS snapshots", so they could name Android (helper backend) or be platform-neutral. If the Appium Android route does not share ui-hierarchy-builder, please say which backends the sentence covers. I did not trace that route in this delta.

One open question is which clipping produces the note. The conformance fixture and ADR 0004 already record that viewport-edge clips do, so one explicit maintainer confirmation would settle it.

One item still blocks merge. Android snapshots now omit nodes that fall fully outside the clip, and the only live run is on iOS. Please post one live Android snapshot at this head that shows a clipped node omitted and the regular-geometry-clipped note.

@thymikee

Copy link
Copy Markdown
Member Author

Android live follow-up for the blocking request above: passed at c8ec2a4b914140e93971c717cb82bef97cffdad9 on ReactNative_API_35, Android 15, emulator-5554 (2026-10-21). The earlier Android evidence was at 3c478a7087; this run supplies the requested current-head evidence. GitHub and the remote branch still report c8 after verification.

Clean isolated checkout: t3-abf68c87, branch verify/pr3857-android-c8. pnpm install --frozen-lockfile, pnpm build, and pnpm build:android passed before capture. No source edits, commits, push, or broad-gate rerun. Current CI is 17 success / 1 skipped / 1 neutral; the existing exact-head gate receipt remains attributed to its original run.

Reused the installed native com.callstack.clip3801 accessibility-provider fixture: scroll clip {x:50,y:200,width:600,height:500}, partial button {x:70,y:650,width:330,height:100}, outside button {x:70,y:800,width:330,height:100}. Both buttons report visibleToUser:true and positive frames.

Commands: open com.callstack.clip3801 --relaunch --json, then batch --steps-file steps.json --json, with these actual batch inputs:

[{"command":"snapshot","input":{}},{"command":"snapshot","input":{"raw":true}},{"command":"snapshot","input":{"interactiveOnly":true}}]

Every app-driving command used /Users/michal/.t3/userdata/device/bin/agent-device with device_open's --platform android --serial emulator-5554 --config /Users/michal/.t3/userdata/device/hosts/25bf8e1a2393f1108d37029b.json --session t3-fe4aba9fcc998279beed68c2. A private Node argv wrapper added --daemon-base-url / --daemon-auth-token from the owned daemon metadata. Source daemon: AGENT_DEVICE_STATE_DIR=/tmp/agent-device-3857-android-abf68c87/state AGENT_DEVICE_DAEMON_SERVER_MODE=dual node dist/src/internal/daemon.js in the c8 checkout.

All three steps returned ok:true. Actual response excerpts (selected fields):

Regular:

{
  "nodes": [
    {
      "label": "Partial action",
      "rect": {
        "x": 70,
        "y": 650,
        "width": 330,
        "height": 50
      },
      "visibleToUser": true
    }
  ],
  "snapshotQuality": {
    "state": "healthy",
    "backend": "android-helper"
  },
  "snapshotQualityNotes": [
    {
      "code": "regular-geometry-clipped",
      "clippedNodeCount": 1,
      "droppedNodeCount": 1
    }
  ],
  "androidSnapshot": {
    "backend": "android-helper",
    "helperVersion": "0.21.30-dev",
    "helperTransport": "persistent-session"
  }
}

Raw:

{
  "nodes": [
    {
      "label": "Partial action",
      "rect": {
        "x": 70,
        "y": 650,
        "width": 330,
        "height": 100
      },
      "visibleToUser": true
    },
    {
      "label": "Outside scroll action",
      "rect": {
        "x": 70,
        "y": 800,
        "width": 330,
        "height": 100
      },
      "visibleToUser": true
    }
  ],
  "snapshotQuality": {
    "state": "healthy",
    "backend": "android-helper"
  },
  "androidSnapshot": {
    "backend": "android-helper",
    "helperVersion": "0.21.30-dev",
    "helperTransport": "persistent-session"
  }
}

Interactive (snapshot -i equivalent):

{
  "nodes": [
    {
      "label": "Partial action",
      "rect": {
        "x": 70,
        "y": 650,
        "width": 330,
        "height": 50
      },
      "visibleToUser": true
    }
  ],
  "snapshotQuality": {
    "state": "healthy",
    "backend": "android-helper"
  },
  "snapshotQualityNotes": [
    {
      "code": "regular-geometry-clipped",
      "clippedNodeCount": 1,
      "droppedNodeCount": 1
    }
  ],
  "androidSnapshot": {
    "backend": "android-helper",
    "helperVersion": "0.21.30-dev",
    "helperTransport": "persistent-session"
  }
}

Regular and interactive node arrays contain no Outside scroll action; the partial button height is 50. Raw retains both buttons at height 100 and contains no snapshotQualityNotes field. These assertions passed against the saved live response. The healthy Android verdict and clipping notes are separate siblings.

The installed helper APK SHA-256 exactly matches the newly built c8 artifact: 7762443002e31cf3b6749568edfc497cd1d5dff1b32c5b72477bd6c5d840e375. Response backend/version are android-helper / 0.21.30-dev, matching package.json. First capture installed the mismatched helper; subsequent steps reused the current persistent helper session.

Transport limit: T3's launcher is 0.21.25. Its standalone snapshot -i --json succeeded with the same omitted/clipped geometry but dropped snapshotQualityNotes; the batch response above preserves the source-daemon snapshot step data. This is live public-field/wire evidence, not final standalone CLI JSON-renderer proof. No Appium device route was run.

Source trace for the optional wording question: local Android helper capture calls buildUiHierarchySnapshot. Android Appium instead calls parseWebDriverAndroidSnapshotNodes, which uses parseWebDriverSourceFacts, without that builder or its clipping-count production. The provider snapshot binding returns this Android result directly. Android inclusion in that sentence should therefore be scoped to the helper backend, not all Android providers. Prose remains unchanged in this verification round.

Cleanup / reservation END: exact owned session closed; source CLI operator device status --json (without --session) returned claims:[], hiddenStaleClaims:0 before and after. daemon stop initially reported identity-unverified; after Ctrl-C through owned exec session 23797, exec completed, PID exited, daemon.json disappeared, and its HTTP port refused connections. Panel detached without emulator shutdown. No other device/session/claim/lock was changed. PR linked in both threads; child watch false, parent's four watches unchanged. Note-population product confirmation remains pending.

@thymikee

Copy link
Copy Markdown
Member Author

Thanks, this clears the last blocker at c8ec2a4. The Android helper run on Android 15 shows the outside button omitted, the partial button clipped to 50, and the regular-geometry-clipped note in regular and interactive snapshots, while raw keeps both buttons and has no note. The code verdict from the earlier review stands, all checks pass, and there are no conflicts.

The run went through a 0.21.25 launcher, so the standalone CLI JSON rendering of the note is still covered only by the serialization tests, and the Appium Android route was not run. Neither blocks this PR.

@thymikee thymikee added the ready-for-human Valid work that needs human implementation, judgment, or maintainer merge label Oct 21, 2026
@thymikee
thymikee merged commit 702048a into main Oct 21, 2026
19 checks passed
@thymikee
thymikee deleted the t3/clip-dynamic-type-snapshots branch October 21, 2026 08:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-human Valid work that needs human implementation, judgment, or maintainer merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant