test(verify): autonomous capture-mode toggle verifier - #167
Merged
Conversation
Adds photo_mode_loop.py, the verifier for the `photo-mode` sub-feature of
features/photo-capture.md: the top-right CAMERA | VIDEO selector and what a tap
on it actually does. It drives Video -> Camera -> Video on the installed debug
APK and asserts each state out of a SINGLE uiautomator dump, two ways at once:
* the shutter's contentDescription (`Start recording` <-> `Take photo`) — the
ability being switched, and
* the tapped segment's `checked` flag — the only indicator of which mode is
armed, which is why the single toggling icon became a segmented control
(issue #126).
Half a flip fails: a highlight that moves while the shutter still records, or a
shutter that flips while the pill stays put. Re-dumping between the two reads
would be a race, so both come off one dump. Nothing is captured — no still, no
clip, no gallery write.
verify_common gains `checkable`/`checked` on UiNode (both parsers). Compose puts
`selectable` on a full-height wrapper around the visible pill, and only that
wrapper reports `checked` — the label leaf and the RadioButton-class leaf both
read checked="false" whichever segment is selected — so the loop finds the
wrapper by bounds containment.
No registry change needed: run-verification-loops.py globs `*_loop.py`, so sweep
gate 5b picks this up automatically.
Verified on emulator-5584 (Pixel_8, API 34): PASS in 26s from .claude and 21s
from the synced .cursor copy, with video-baseline / camera-mode / video-restored
XML + PNG evidence. Re-parsing that evidence confirms each state matches its own
mode and fails the other.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01249no1TrwKLR1aorGqaf7C
…hat a step INVENTORY gains a row for the capture-mode toggle (`automated 2026-09-01`) — the surface photo_mode_loop.py actually drives. "Photo stills mode" deliberately stays `mapped`: taking the still is still a manual control.ps1 recipe, and the table's own legend makes `automated` a claim about what a loop drives. Marking that row was ad-hoc; the create-verifier skill now names it as step 6, after the loop has been seen passing, with the harness sync moved to the end so it picks up the INVENTORY edit too. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01249no1TrwKLR1aorGqaf7C
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Adds the autonomous installed-APK verifier for the capture-mode toggle — the top-right
CAMERA | VIDEO selector on the viewfinder — and writes "mark INVENTORY when it passes" into the
create-verifier process, which until now happened by hand.
No shipped app code changes: verifier plus skill docs only.
The verifier
verify-openloop/helpers/photo_mode_loop.pydrives the real APK on an emulator throughVideo → Camera → Video, and asserts each state out of a single uiautomator dump, two ways at
once:
contentDescriptionCamerasegmentVideosegmentStart recordingchecked=falsechecked=trueTake photochecked=truechecked=falseStart recordingchecked=falsechecked=trueThe shutter label is the ability being switched — whether the next tap records a clip or takes
a picture. The
checkedflag is the indicator — which segment carries the lime fill, and thereason the single toggling icon became a segmented control at all (issue #126: with one icon, the
only state indicator sat under the fingertip at the exact moment it changed). Half a flip fails: a
highlight that moves while the shutter still records, or a shutter that flips while the pill stays
put.
Both reads come off one dump on purpose. A dump costs seconds on this screen, so polling for the
label and then re-dumping for the flag is a race the harness loses intermittently — and the two
halves disagreeing is exactly the bug being looked for.
Nothing is captured: no still, no clip, no gallery write, no DataStore reset. The loop runs in
about 25 s.
Where selection state actually lives
verify_common.pynodes now carrycheckable/checked(in both parsers). Finding the flagmeant reading the real hierarchy rather than guessing at it: Compose puts the
selectableon afull-height wrapper around the visible pill, and only that wrapper reports
checked. The labelleaf and the
android.widget.RadioButtonleaf inside it both reportchecked="false"whicheversegment is selected — so a verifier that reached for the obvious-looking RadioButton node would
have asserted a constant and passed forever.
uiautomator returns a flat node list, so the loop finds the wrapper geometrically: the smallest
checkable node whose bounds contain the label's. On the shipped screen that resolves to exactly two
nodes,
[523,175][786,301]and[786,175][1027,301].Evidence, and proof the assertions discriminate
Run on emulator-5584 (Pixel_8, API 34):
PASS serial=emulator-5584 selector=Camera|Video video->photo->video took=26sfrom.claude,and 21 s from the synced
.cursorcopy — which is the onerun-verification-loops.pyglobs.other. A verifier that passes while asserting nothing is the failure mode worth ruling out
explicitly, not assuming away.
path carries the new fields too.
video-baseline,camera-mode,video-restoredas XML + PNG.Sweep gate 5b picked the loop up with no registry change, since
run-verification-loops.pyglobs*_loop.py:PASS loops=onboarding,photo_mode,record_clip.INVENTORY, and making that a step
INVENTORY.mdgains a row for the capture-mode toggle asautomated 2026-09-01. "Photo stillsmode" deliberately stays
mapped— this loop drives the toggle, not the capture, and the table'sown legend makes
automateda claim about what a loop drives. Overstating it there is how a greentable starts lying.
Marking that row was ad-hoc (#163 did it by hand as well), so
create-verifier/SKILL.mdnow namesit as step 6 — only after the loop has been seen passing — with the harness sync moved to the end
so it picks up the INVENTORY edit too.
Manual QA / not verified here
control.ps1recipe; this loop stops at the toggle.CameraScreen), andsetCaptureModerefusesa switch mid-recording. Neither guard is driven here — that is
record-clip/photo-boothterritory.
Checklist
🧪 My changes have been verified locally and work as expected.
🔍 I have performed a self-review of my own code.
✍️ I have commented my code, particularly in hard-to-understand areas.
📖 My changes generate no compile warnings or errors (
allWarningsAsErrorsis on).🧹 Pre-PR sweep GREEN on the final commit (
.\scripts\pre-pr-sweep.ps1→build/sweep-receipt.json), run as-SkipInspectCode -SkipConnected. Two gates are thereforeNOT in the receipt, and both need the owner before merge:
which is not available headless on this machine.
-SkipConnectedskips both. This is a host limit, not a code one: this machine cannot hold a full sweep and the
AVD at once (the Gradle daemon reaches ~3 GB and does not give it back), and the owner chose
this over freeing memory. What was actually observed at this same commit, across three full
sweep attempts, is below.
PASS loops=onboarding,photo_mode,record_clipANDROID_SERIALunsetPASS loops=onboarding,photo_mode,record_clipPASS loops=onboarding,photo_mode,record_clipNeither gate-5 red is an assertion failing. Attempt 2's was
BoomerangEditorScreenTest#selectedLook_reportsSelectedSemanticstiming out at exactly 45 s inActivityScenarioRule.afterwaiting forDESTROYED— teardown, not the one-line test body — with2.6 GB free; stopping the Gradle daemons took the host to 6.0 GB and that class then ran 25/25
green in isolation. Attempt 3 died with
DeadSystemRuntimeException: DeadSystemException, theemulator's own
system_servergoing down with 1.1 GB free.connectedDebugAndroidTeststillneeds one clean run on a machine with headroom before merge.
🏪 Play-facing docs aligned: no permission, data-collection, storage or user-facing change
— verifier and repo tooling only.
🧹 The git branch is clean.
🤖 Generated with Claude Code
https://claude.ai/code/session_01249no1TrwKLR1aorGqaf7C
Note
Low Risk
Verifier tooling and documentation only; no production app, auth, or data-handling changes.
Overview
Adds an installed-APK verification loop for the viewfinder Camera | Video segmented control (
photo_mode_loop.py), synced across the three harness skill trees. The loop drives Video → Camera → Video on an emulator without capturing media, and from each uiautomator dump asserts both the shutter accessibility label (Start recordingvsTake photo) and the selected segment’scheckedstate so a half-updated toggle fails.Harness plumbing:
verify_common.UiNodeand both XML parsers now exposecheckable/checked, with geometry-based lookup of the Compose wrapper that actually reports selection. Docs/process:photo-capture.mddocuments the autonomous run command;INVENTORY.mdmarks the capture-mode toggleautomated 2026-09-01while photo still capture staysmapped. create-verifier workflow moves harness sync to a new step 6 (after a passing run and INVENTORY update) instead of immediately after updating the feature recipe.No changes to shipped Android app code.
Reviewed by Cursor Bugbot for commit e551a20. Bugbot is set up for automated code reviews on this repo. Configure here.