Skip to content

fix(e2e): identify recorded controls by test id, labels only by shape - #637

Closed
okwasniewski wants to merge 9 commits into
mainfrom
oskar/cache-relocate-label-drift
Closed

okwasniewski wants to merge 9 commits into
mainfrom
oskar/cache-relocate-label-drift

Conversation

@okwasniewski

@okwasniewski okwasniewski commented Sep 29, 2026 •

Copy link
Copy Markdown
Member

What and why

  • Replay relocated a recorded control by its exact accessible name. In an app whose labels carry state (Like (0 likes), a row named after its contents and · now) that handed every second step back to the model. Relocation now degrades from the strongest identity it recorded: test id, then an authored element id (web now projects the id attribute), then the exact label, then the label's shape, then place among unnamed twins.
  • An element id counts as authored only when it is not a framework's counter: an id that ends in a digit (mat-input-2, mui-3, headlessui-menu-button-1) or reads like a hash (:r3:, input-1739) is skipped. A counter names whichever control rendered in that place this time, and an id rung that trusted it typed into the field beside the right one.
  • A label's shape folds tallies and relative times only: a count governing a noun (0 replies, 3 followers, 2 matches) and now, 2m, yesterday. A number that names something stays as it reads, so Delete item 3 never finds Delete item 4, Page 2 never finds Page 3, Order #1234 never finds Order #9876. now is a time only on its own (Bob · now), not the adverb of Buy now, and a one-letter unit is lower case only (3 M is a size).
  • Twins resolve by the recorded position only, never by whichever label happens to match now (the liked button is the one that stopped reading Like). Any rung that finds another number of them than were recorded passes to the next rung, and a lone survivor of a recorded set is ambiguous, not found: among label twins it is whichever one still reads as recorded, which after the step acted on the recorded one is exactly the wrong one.
  • End anchors compare their labels as they read, with only relative times folded. A count is usually the very effect the anchor stands for, so a recorded Count: 1, Step 2 of 3, or Cart (2 items) does not pass on a screen left at Count: 0, Step 1 of 3, Cart (1 item). Anchors are the leaves that appeared, ranked by durability, and skip relative times and social tallies; an unreadable end screen reports every anchor as missing; a stable container outranks volatile leaves.
  • An end-mismatch hand-off names the anchors the screen did not show, in step.cache.missingAnchors and in the notice the agent gets. That is how the shell-chrome anchors were found.
  • @e2e-dev/mobile: every settings request names the worker's device through one settings() helper. Without it the daemon resolved clear app state against every booted device, and one engine failure failed every later test of a run with "2 devices match this request equally".
  • REPLAY_POLICY_VERSION goes to conservative/7; recordings re-record once on their next passing run.
flowchart LR
  R[recorded target] --> T{test id recorded?}
  T -- yes --> I[nodes with that test id]
  T -- no --> E
  I -- none --> E{element id recorded?}
  E -- yes --> J[nodes with that element id]
  E -- no --> L
  J -- none --> L[label equal]
  L -- none --> S[label shape equal]
  S -- none --> N[not found: hand off]
  I -- one --> F[found]
  J -- one --> F
  L -- one --> F
  S -- one --> F
  I -- several --> P{same count as recorded?}
  J -- several --> P
  L -- several --> P
  S -- several --> P
  P -- yes --> F
  P -- no --> A[ambiguous: hand off]
Loading

packages/e2e/src/cache/relocate.ts (matchingTiers, labelShape) and packages/e2e/src/cache/anchors.ts (anchorMatches) share SEMANTIC_ID_FIELDS and sameLabels, so the two can no longer drift; anchors ask for the times fold, relocation for counts.

Verified

Ran it locally: yes

  • Bluesky iOS suite on the published canary versus this branch, the three files with the most hand-offs (composer, home, thread; 10 tests), one recording pass then a replay pass on an iPhone 17 Pro simulator:
Measure main this branch delta
Steps replayed clean 26 34 +8
Steps handed off 8 1 -7
Model calls per run 54 19 -65%
Wall clock 879s 574s -35%
home.e2e.ts 228s, 4 tests with model calls 67s, zero model calls
  • The one remaining hand-off is pull-to-refresh, which the mobile tool pack records as an unreplayable swipe tool; a grammar action for it is a separate change.
  • A stress probe through the real CLI against a scratch web app whose state drifts between the recording run and the replay run, on main, on the first version of this branch, and on this branch as it stands:
Scenario main first version this branch
Increment click is a no-op on replay, status reads Count: 0 handed off, agent reports the count did not move replayed clean, anchor Count: 1 taken by shape handed off, agent reports the count did not move
Delete item 3 recorded, only Delete item 4 on screen handed off replayed clean onto Delete item 4 target-not-found, agent runs live and leaves item 4
Email recorded with id mat-input-2; on replay that id is Phone, Email is mat-input-3 replays onto Email by label replayed clean into Phone replays onto Email by label, no model call
  • Web benchmark agentic suite with the committed conservative/7 recordings under --strict-cache: 32 passed, 25 replayed, no REPLAY_STALE; the hand-offs and misses are the designed gap and unique-param cases.
  • Testbed agent suite, a recording pass then a replay pass: 24 passed both times, 21 steps replayed with no action model call on the replay.
  • iOS benchmark agentic suite re-recorded on an iPhone 17 Pro simulator under conservative/7, then a read-only replay pass: 22 passed, 13 steps from cache, 8 handed off (tool gaps, native sheets), the same profile the previous entries had in CI.
  • missingAnchors in a real hand-off, before the leaf-only rule:
    ["other \"Test harness control, not part of the app\"", "other \"Home\"", "scroll-view \"Following\"", "other \"New post\""] with the post text itself on screen.
  • Mobile fix: a run with an iOS simulator and an Android emulator both booted no longer cascades after an engine failure.

Checklist

  • Changeset added (pnpm changeset) for any change to a published package, or the change is testbed, docs, or CI only.
  • Breaking change: title carries !, the changeset body names what breaks and what replaces it, and the deprecation warning landed a window earlier (stability policy in CONTRIBUTING.md).
  • Docs updated in the same PR: the docs/**/*.mdx page for the behavior, and skills/e2e/ if the skill describes it.
  • Contract change: emitted .d.ts reviewed, tests/types/sdk-types.ts updated; wire change edits the schema and both fixtures.
  • Engine contract change (e2e/engine): changesets for e2e, @e2e-dev/web, and @e2e-dev/mobile.

@github-actions

github-actions Bot commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

🟢 e2e web: 90 passed

All 90 tests in 41 files
Test Time
🟢 tests/aria-hidden-flow.e2e.ts · 2 passed 500ms
🟢 aria hidden flow › picks the Pro plan by its visible text and confirms it 363ms
🟢 aria hidden flow › another plan shows the error and keeps the picker open 137ms
🟢 tests/async-states.e2e.ts · 2 passed 7.1s
🟢 async states › reads the code off the transient toast and verifies it once the button enables 2.3s
🟢 async states › a wrong code is rejected after the toast has gone 4.8s
🟢 tests/canvas-only.e2e.ts · 2 passed 199ms
🟢 canvas only › taps square, circle, then diamond at their pixel coordinates 105ms
🟢 canvas only › a shape out of order paints the error and resets progress 94ms
🟢 tests/canvas-whiteboard.e2e.ts · 2 passed 317ms
🟢 canvas whiteboard › drags both shapes into their slots and saves 183ms
🟢 canvas whiteboard › bounces a shape off the wrong slot and refuses to save early 134ms
🟢 tests/cart-totals.e2e.ts · 2 passed 296ms
🟢 cart totals › increasing a quantity updates the line total but the order summary stays stale (planted bug) 169ms
🟢 cart totals › a quantity never drops below one 127ms
🟢 tests/checkout-review.e2e.ts · 1 passed 150ms
🟢 shows a raw greeting token and an undefined delivery date (planted bug), yet places the order 150ms
🟢 tests/context-menu-trap.e2e.ts · 2 passed 464ms
🟢 context menu trap › right-click opens nothing; the row menu renames the file 234ms
🟢 context menu trap › the decoy menu items refuse and keep the file 230ms
🟢 tests/control-inventory.e2e.ts · 7 passed 3.0s
🟢 control inventory › check sets the checkbox and the radio without flipping a set one 194ms
🟢 control inventory › a long press and a double click are told apart from a tap 933ms
🟢 control inventory › the file menu opens on right-click only and Rename renames the file 188ms
🟢 control inventory › setInputFiles attaches files named from the project root 103ms
🟢 control inventory › the details view is left through the browser history 140ms
🟢 control inventory › scrollIntoView brings the footnote into view 238ms
🟢 control inventory › every exercise done shows the success message 1.2s
🟢 tests/css-content-ui.e2e.ts · 2 passed 267ms
🟢 css content ui › the tile labelled Ship it advances and confirming ships 143ms
🟢 css content ui › a wrong tile shows the CSS-rendered error and keeps the picker 124ms
🟢 tests/date-picker.e2e.ts · 2 passed 527ms
🟢 date picker › navigates three months ahead and books the required date 311ms
🟢 date picker › confirming without the exact date shows the error 216ms
🟢 tests/debounced-search.e2e.ts · 2 passed 2.8s
🟢 debounced search › a decoy one character off is refused 1.4s
🟢 debounced search › picking the exact item adds it to the cart 1.4s
🟢 tests/deferred-dialog.e2e.ts · 1 passed 1.0s
🟢 one submit opens one confirm, even when double-clicked 1.0s
🟢 tests/div-soup.e2e.ts · 2 passed 568ms
🟢 div soup › a wrong code is rejected 252ms
🟢 div soup › the right code grants access after the delete tile fixes a typo 316ms
🟢 tests/drag-and-drop.e2e.ts · 2 passed 564ms
🟢 drag and drop › an out-of-order item resets the dropzone 286ms
🟢 drag and drop › dropping Banana then Cherry passes 278ms
🟢 tests/filter-deep-link.e2e.ts · 2 passed 194ms
🟢 filter deep link › filtering writes the query to the URL 107ms
🟢 filter deep link › opening a shared link restores the filter 87ms
🟢 tests/gift-card-purchase.e2e.ts · 3 passed 506ms
🟢 gift card purchase › refuses to buy without an amount 124ms
🟢 gift card purchase › refuses a recipient without an at sign 181ms
🟢 gift card purchase › sends the gift card 201ms
🟢 tests/hover-menu.e2e.ts · 2 passed 396ms
🟢 hover menu › a decoy leaf is refused 179ms
🟢 hover menu › hovering Account, then Billing, reaches Redeem voucher 217ms
🟢 tests/iframe-form.e2e.ts · 2 passed 545ms
🟢 iframe form › a wrong code is refused by the host page 264ms
🟢 iframe form › the code read from one frame applies through the other 281ms
🟢 tests/image-only-ui.e2e.ts · 3 passed 353ms
🟢 image only ui › the icons have no names, so the bare svg locator is ambiguous 86ms
🟢 image only ui › a wrong icon shows the error image and keeps the icons 121ms
🟢 image only ui › tapping the star, then the heart, verifies the icons 146ms
🟢 tests/infinite-scroll.e2e.ts · 1 passed 3.9s
🟢 scrolls the feed until the Golden Ticket loads, then claims it 3.9s
🟢 tests/kanban-board.e2e.ts · 3 passed 580ms
🟢 kanban board › pointer drags move both cards into the goal layout 313ms
🟢 kanban board › a swipe along a path drags a card as well 117ms
🟢 kanban board › a plain tap moves nothing and the untouched board is rejected 150ms
🟢 tests/login-form.e2e.ts · 4 passed 691ms
🟢 login form › rejects a malformed email 153ms
🟢 login form › rejects a short password 148ms
🟢 login form › rejects an unknown account 154ms
🟢 login form › signs in and logs out 236ms
🟢 tests/lying-labels.e2e.ts · 4 passed 640ms
🟢 lying labels › the button whose accessible name says Pay now is the wrong one 142ms
🟢 lying labels › a text match for Pay now catches the decoy phrase too 104ms
🟢 lying labels › the field whose accessible name says Amount is the wrong one 214ms
🟢 lying labels › visible text drives the payment through 180ms
🟢 tests/native-dialogs.e2e.ts · 3 passed 471ms
🟢 native dialogs › answers the confirm, prompt, and alert chain 142ms
🟢 native dialogs › dismissing the confirm cancels the order 148ms
🟢 native dialogs › a wrong gift code is rejected 181ms
🟢 tests/newsletter-signup.e2e.ts · 1 passed 2.3s
🟢 subscribe renders no feedback and logs a TypeError (planted bug) 2.3s
🟢 tests/onboarding-wizard.e2e.ts · 5 passed 4.4s
🟢 onboarding wizard › back keeps what was typed and the brief comes back with it 279ms
🟢 onboarding wizard › the address step takes the brief through its bare-div dropdown 322ms
🟢 onboarding wizard › the profile step steers the calendar three decades back 1.1s
🟢 onboarding wizard › the preferences step flips the toggles the brief names and leaves the trap off 1.3s
🟢 onboarding wizard › the review step repeats the brief and finishes with the PIN 1.3s
🟢 tests/order-history.e2e.ts · 2 passed 1.5s
🟢 order history › overview and settings render their panels 210ms
🟢 order history › the orders tab never leaves its loading state (planted bug) 1.2s
🟢 tests/otp-auto-advance.e2e.ts · 3 passed 1.3s
🟢 otp auto-advance › focus advances with each digit and the full code verifies 451ms
🟢 otp auto-advance › backspace on an empty box clears and refocuses the previous one 105ms
🟢 otp auto-advance › a wrong code clears every box and starts over 743ms
🟢 tests/overlay-trap.e2e.ts · 1 passed 680ms
🟢 continues only after the banner and the overlay are gone 680ms
🟢 tests/playbook-cleanup.e2e.ts · 2 passed 397ms
🟢 playbook cleanup › deleting the draft removes it from an exact-name search 195ms
🟢 playbook cleanup › cancelling the dialog keeps the draft searchable 202ms
🟢 tests/price-sorting.e2e.ts · 3 passed 298ms
🟢 price sorting › opens in the featured order with sale prices struck through 86ms
🟢 price sorting › low to high sorts by the original price, not the paid one (planted bug) 97ms
🟢 price sorting › high to low is the same wrong order reversed (planted bug) 115ms
🟢 tests/product-reviews.e2e.ts · 1 passed 1.1s
🟢 the running-shoe page lists coffee-grinder reviews (planted bug) 1.1s
🟢 tests/promo-storefront.e2e.ts · 1 passed 1.4s
🟢 buys a deal once the promo chrome is out of the way 1.4s
🟢 tests/rich-text-editor.e2e.ts · 2 passed 374ms
🟢 rich text editor › bolding exactly the required word passes the check 191ms
🟢 rich text editor › bold that spills into the previous word fails the check 183ms
🟢 tests/saved-addresses.e2e.ts · 3 passed 472ms
🟢 saved addresses › adds an address and clears the form 165ms
🟢 saved addresses › ignores an add with an empty field 139ms
🟢 saved addresses › delete shows its toast but keeps the address, on retry too (planted bug) 168ms
🟢 tests/shadow-dom-form.e2e.ts · 2 passed 478ms
🟢 shadow DOM form › rejects a wrong access code 226ms
🟢 shadow DOM form › grants access with the code from the hint inside the root 252ms
🟢 tests/smoke.e2e.ts · 1 passed 344ms
🟢 home lists the scenarios and opens one 344ms
🟢 tests/stale-dom.e2e.ts · 1 passed 2.1s
🟢 taps the moving target three times across rebuilds of the list 2.1s
🟢 tests/sticky-chrome.e2e.ts · 1 passed 184ms
🟢 accepts the terms once the button clears the sticky footer 184ms
🟢 tests/virtualized-table.e2e.ts · 1 passed 1m 5s
🟢 scrolls the windowed table to the Golden Row and claims it 1m 5s
🟢 tests/wishlist.e2e.ts · 2 passed 1.4s
🟢 wishlist › saving toggles the button and the tab badge 183ms
🟢 wishlist › the wishlist tab renders the empty state despite the badge count (planted bug) 1.2s

e2e 0.15.0-canary-20260928184528 · 1m 50s · web · run artifacts

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 3 potential issues.

2 flags not posted on this PR by your GitHub settings — view them in Devin Review. (Configure)

Devin Review

Comment thread packages/e2e/src/cache/anchors.ts Resolved
Comment thread packages/e2e/src/cache/relocate.ts Resolved
Comment thread packages/e2e/src/cache/anchors.ts Resolved
@github-actions

github-actions Bot commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

🟢 e2e web agent: 32 passed

33 agent steps · 25 replayed from cache · 19 model calls · 71.5k tokens (91% cached) · $0.0034

All 32 tests in 5 files
Test Agent Time
🟢 tests-agent/control-inventory.e2e.ts · 6 passed 6 steps 7.2s
🟢 control inventory › check sets the checkbox and the radio 1 step 524ms
🟢 control inventory › a long press and a double click are chosen over a plain tap 1 step 879ms
🟢 control inventory › a right-click opens the file menu and Rename renames the file 1 step 2.6s
🟢 control inventory › upload attaches the named project files 1 step 2.5s
🟢 control inventory › back returns from the details view to the inventory 1 step 328ms
🟢 control inventory › scroll_to brings the footnote into view 1 step 396ms
🟢 tests-agent/login-form.e2e.ts · 1 passed 1 step 311ms
🟢 act signs in with the benchmark account 1 step 311ms
🟢 tests-agent/planted-bugs.e2e.ts · 3 passed 4 steps · 7 calls 13.7s
🟢 cart totals: a judgment catches the stale order summary 2 steps · 3 calls 4.3s
🟢 checkout review: a judgment catches the broken copy 1 step · 1 call 3.8s
🟢 wishlist: a judgment catches the lost saved items 1 step · 3 calls 5.6s
🟢 tests-agent/scenarios.e2e.ts · 21 passed 21 steps · 12 calls 2m 29s
🟢 act completes shadow-dom-form 1 step · 3 calls 5.3s
🟢 act completes lying-labels 1 step · 3 calls 5.4s
🟢 act completes iframe-form 1 step · 3 calls 4.4s
🟢 act completes infinite-scroll 1 step 5.4s
🟢 act completes virtualized-table 1 step 1m 43s
🟢 act completes overlay-trap 1 step 889ms
🟢 act completes sticky-chrome 1 step 941ms
🟢 act completes async-states 1 step · 3 calls 6.5s
🟢 act completes debounced-search 1 step 2.3s
🟢 act completes hover-menu 1 step 461ms
🟢 act completes drag-and-drop 1 step 483ms
🟢 act completes kanban-board 1 step 610ms
🟢 act completes context-menu-trap 1 step 2.6s
🟢 act completes stale-dom 1 step 477ms
🟢 act completes otp-auto-advance 1 step 780ms
🟢 act completes date-picker 1 step 887ms
🟢 act completes rich-text-editor 1 step 679ms
🟢 act completes playbook-cleanup 1 step 723ms
🟢 act completes promo-storefront 1 step 2.3s
🟢 act completes gift-card-purchase 1 step 4.5s
🟢 act filters the list and the URL carries the filter 1 step 208ms
🟢 tests-agent/unique-cache.e2e.ts · 1 passed 1 step 276ms
🟢 act adds an address with a run-unique label 1 step 276ms

e2e 0.15.0-canary-20260928184528 · 2m 51s · web · run artifacts

@github-actions

github-actions Bot commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

🟢 e2e ios: 50 passed, 1 skipped

All 51 tests in 7 files
Test Time
⏭️ tests/controls.e2e.ts · 21 passed, 1 skipped 3m 51s
🟢 control inventory › a label query resolves a labeled text field 13.6s
🟢 control inventory › a labeled field answers name, display value, value reads, fill, and clear 24.0s
🟢 control inventory › a secure field takes a fill and denies value reads 11.5s
🟢 control inventory › press sends a character and Enter through the soft keyboard 11.1s
🟢 control inventory › a field autofocuses on mount and focus() moves the focus 13.4s
🟢 control inventory › a switch checks, unchecks, and reports its state 13.1s
🟢 control inventory › checkbox and radio roles answer getByRole, and a checkbox toggles by tap 11.3s
🟢 control inventory › checkbox and radio roles check, uncheck, and report their state 17.2s
🟢 control inventory › a radio group reports the checked option 17.0s
🟢 control inventory › buttons report disabled, enabled, and a state that arrives later 12.2s
🟢 control inventory › a display: none element is absent rather than hidden, its visible twin attached 9.9s
🟢 control inventory › a header answers its accessible name 6.2s
🟢 control inventory › a tab row answers count, all, texts, filters, positions, and switching 9.3s
🟢 control inventory › a list answers counts, list-form text, reads, and boxes in order 10.0s
⏭️ control inventory › a row contains its texts (skipped: React Native flattens a plain View out of the accessibility tree on both platforms: XCTest and the Android helper report a View with a testID as a leaf beside its children, so filter({ has }) and a query scoped to it match nothing; only Sc…)
🟢 control inventory › a long press lands as one 10.7s
🟢 control inventory › pointer verbs land at the point asked for 13.2s
🟢 control inventory › an ambiguous locator fails at once 7.7s
🟢 control inventory › screenshot, restart, and clearState reopen the app on its home list 17.7s
🟢 refusals › verbs a device never declares are refused before the engine 7ms
🟢 refusals › keys the soft keyboard cannot send are refused by the engine 932ms
🟢 refusals › a missing locator fails as not found once its timeout passes 1.2s
🟢 tests/device-fixture.e2e.ts · 9 passed 1m 39s
🟢 device fixture › setAppearance flips the color scheme the app reads 18.2s
🟢 device fixture › setOrientation rotates the window the app measures 12.1s
🟢 device fixture › the clipboard reads back what was written 10.7s
🟢 device fixture › home leaves the app, openApp brings it back where it was 7.5s
🟢 device fixture › closeApp then openApp relaunches on the home list 10.6s
🟢 device fixture › foregroundApp after closeApp reports the closed session as APP_NOT_OPEN 5.8s
🟢 device fixture › device.back pops the scenario 7.1s
🟢 device fixture › setLocation and clearLocation change what the app reads 14.1s
🟢 device fixture › enrollBiometrics and setBiometrics answer a biometric prompt 12.9s
🟢 tests/device.e2e.ts · 2 passed 1m 4s
🟢 sequential onboarding echoes the exact values 47.8s
🟢 onboarding rejects a malformed email before advancing 15.7s
🟢 tests/flows-lists.e2e.ts · 7 passed 4m 8s
🟢 infinite scroll reaches the target item 31.8s
🟢 product catalog adds exactly two of the right variant 12.6s
🟢 gestures: long-press, then swipe left 10.8s
🟢 sticky chrome: accept below the fold, then continue 13.7s
🟢 async states: load, pull to refresh, claim through the toast 14.2s
🟢 huge virtualized list reaches row 512 2m 29s
🟢 web view coupon form applies the code shown on the page 15.2s
🟢 tests/flows.e2e.ts · 7 passed 1m 24s
🟢 text inputs unlock the submit button 18.5s
🟢 filled text inputs report their values 14.0s
🟢 modal flow confirms through the native alert 11.0s
🟢 bottom tabs act inside the last tab 8.3s
🟢 error recovery retries, then confirms the delete 10.7s
🟢 choice controls place the exact order 9.5s
🟢 debounced search selects the target once results arrive 11.7s
🟢 tests/login-form.e2e.ts · 3 passed 52.6s
🟢 login form › rejects a malformed email 16.0s
🟢 login form › rejects an unknown account 14.0s
🟢 login form › signs in and logs out 22.7s
🟢 tests/smoke.e2e.ts · 1 passed 12.4s
🟢 home lists the scenarios and opens one 12.4s

e2e 0.15.0-canary-20260928184528 · 6m 51s · ios-simulator · run artifacts

@github-actions

github-actions Bot commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

🟢 e2e android: 54 passed, 1 skipped

All 55 tests in 7 files
Test Time
⏭️ tests/controls.e2e.ts · 24 passed, 1 skipped 2m 52s
🟢 control inventory › a label query resolves a labeled text field 22.0s
🟢 control inventory › a labeled field answers name, display value, value reads, fill, and clear 13.3s
🟢 control inventory › a placeholder query and attribute reads find the field 5.7s
🟢 control inventory › a secure field takes a fill and denies value reads 6.6s
🟢 control inventory › press sends a character and Enter through the soft keyboard 8.3s
🟢 control inventory › a field autofocuses on mount and focus() moves the focus 9.9s
🟢 control inventory › toBeFocused follows the focus 8.6s
🟢 control inventory › a switch checks, unchecks, and reports its state 10.2s
🟢 control inventory › checkbox and radio roles answer getByRole, and a checkbox toggles by tap 6.5s
🟢 control inventory › checkbox and radio roles check, uncheck, and report their state 11.9s
🟢 control inventory › a tab role answers getByRole 3.2s
🟢 control inventory › a radio group reports the checked option 8.5s
🟢 control inventory › buttons report disabled, enabled, and a state that arrives later 8.1s
🟢 control inventory › a display: none element is absent rather than hidden, its visible twin attached 6.2s
🟢 control inventory › a header answers its accessible name 3.5s
🟢 control inventory › a tab row answers count, all, texts, filters, positions, and switching 4.7s
🟢 control inventory › a list answers counts, list-form text, reads, and boxes in order 5.0s
⏭️ control inventory › a row contains its texts (skipped: React Native flattens a plain View out of the accessibility tree on both platforms: XCTest and the Android helper report a View with a testID as a leaf beside its children, so filter({ has }) and a query scoped to it match nothing; only Sc…)
🟢 control inventory › a long press lands as one 7.0s
🟢 control inventory › a double tap lands inside the 300 ms window 7.9s
🟢 control inventory › an ambiguous locator fails at once 4.7s
🟢 control inventory › screenshot, restart, and clearState reopen the app on its home list 8.1s
🟢 refusals › verbs a device never declares are refused before the engine 2ms
🟢 refusals › keys the soft keyboard cannot send are refused by the engine 1.3s
🟢 refusals › a missing locator fails as not found once its timeout passes 1.1s
🟢 tests/device-fixture.e2e.ts · 8 passed 1m 4s
🟢 device fixture › setOrientation rotates the window the app measures 4.5s
🟢 device fixture › home leaves the app, openApp brings it back where it was 5.7s
🟢 device fixture › foregroundApp sees the home screen after home() 3.9s
🟢 device fixture › closeApp then openApp relaunches on the home list 5.8s
🟢 device fixture › foregroundApp after closeApp reports the closed session as APP_NOT_OPEN 4.0s
🟢 device fixture › device.back pops the scenario 4.5s
🟢 device fixture › setNetwork and setAirplaneMode change what the app sees 17.9s
🟢 device fixture › setLocation and clearLocation change what the app reads 17.8s
🟢 tests/device.e2e.ts · 4 passed 35.3s
🟢 permission prompt › allowing the system dialog enables the microphone 6.5s
🟢 permission prompt › a denied prompt recovers once the permission is granted outside 12.0s
🟢 sequential onboarding echoes the exact values 9.9s
🟢 onboarding rejects a malformed email before advancing 6.9s
🟢 tests/flows-lists.e2e.ts · 7 passed 2m 5s
🟢 infinite scroll reaches the target item 17.4s
🟢 product catalog adds exactly two of the right variant 9.4s
🟢 gestures: long-press, then swipe left 8.1s
🟢 gestures: double-tap completes the flow 9.1s
🟢 sticky chrome: accept below the fold, then continue 10.9s
🟢 async states: load, pull to refresh, claim through the toast 11.3s
🟢 huge virtualized list reaches row 512 59.2s
🟢 tests/flows.e2e.ts · 7 passed 49.5s
🟢 text inputs unlock the submit button 6.1s
🟢 filled text inputs report their values 5.5s
🟢 modal flow confirms through the native alert 7.8s
🟢 bottom tabs act inside the last tab 6.3s
🟢 error recovery retries, then confirms the delete 8.2s
🟢 choice controls place the exact order 7.6s
🟢 debounced search selects the target once results arrive 8.1s
🟢 tests/login-form.e2e.ts · 3 passed 28.6s
🟢 login form › rejects a malformed email 8.4s
🟢 login form › rejects an unknown account 8.2s
🟢 login form › signs in and logs out 12.0s
🟢 tests/smoke.e2e.ts · 1 passed 5.2s
🟢 home lists the scenarios and opens one 5.2s

e2e 0.15.0-canary-20260928184528 · 8m 1s · android-emulator · run artifacts

@github-actions

github-actions Bot commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

🟢 e2e android agent: 1 flaky, 19 passed

24 agent steps · 110 model calls · 601.1k tokens (85% cached) · $0.0365

⚠️ 1 flaky test passed on a retry

⚠️ act completes Gestures
tests-agent/scenarios.e2e.ts:257

ACTION_FAILED at step 4 of 4: agent.act "Complete this scenario as the screen instructs: long-press the first target, swipe the card left, then double-tap the last target", after 14.5s and 5 model calls

agent.act failed: Long-pressing the first target advanced to Step 2. Two left swipes across the visible “Swipe me left” card produced no screen change, so the swipe step did not complete and the last target was not reached.

  • Turn 2: screenshot({}) → [screenshot] The screen changed substantially since revision b3. Current screen (revision b5, path dev.e2e.benchmark, 18 nodes):
  • Turn 3: swipe({"from":{"x":250,"y":460},"to":{"x":80,"y":460}}) → [swipe] Swiped from (250, 460) to (80, 460).
  • Turn 4: swipe({"from":{"x":170,"y":460},"to":{"x":20,"y":460}}) → [swipe] Swiped from (170, 460) to (20, 460).
  • Screen: dev.e2e.benchmark

Evidence: screenshot, log

All 20 tests in 1 file
Test Agent Time
⚠️ tests-agent/scenarios.e2e.ts · 1 flaky, 19 passed 24 steps · 110 calls 8m 51s
🟢 act completes Login Form 1 step · 3 calls 32.0s
🟢 act completes Infinite Scroll List 1 step · 3 calls 26.6s
🟢 act completes Modal Flow 1 step · 4 calls 12.5s
🟢 act completes Bottom Tabs 1 step · 3 calls 14.6s
🟢 act completes Text Input Variations 1 step · 5 calls 12.9s
🟢 act completes Broken Accessibility 2 steps · 6 calls 27.7s
🟢 act completes Vision Only 2 steps · 6 calls 29.7s
⚠️ act completes Gestures (1 failed attempt first) 1 step · 8 calls 29.0s
🟢 act completes Async States 1 step · 8 calls 29.6s
🟢 act completes Debounced Search 1 step · 4 calls 9.3s
🟢 act completes Huge Virtualized List 1 step · 3 calls 1m 13s
🟢 act completes Flattened Registration Form 2 steps · 8 calls 42.7s
🟢 act completes Flattened Login 2 steps · 6 calls 29.4s
🟢 act completes Sticky Chrome Target 1 step · 4 calls 16.3s
🟢 act completes WebView Accessibility 1 step · 5 calls 18.6s
🟢 act completes Permission Prompt 1 step · 3 calls 9.1s
🟢 act completes Product Catalog 1 step · 6 calls 15.0s
🟢 act completes Error Recovery 1 step · 7 calls 14.3s
🟢 act completes Choice Controls 1 step · 6 calls 28.7s
🟢 act completes Sequential Onboarding 1 step · 12 calls 1m 1s

e2e 0.15.0-canary-20260928184528 · 9m 12s · android-emulator · run artifacts

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 19 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread packages/e2e/src/cache/anchors.ts Resolved
Comment thread packages/e2e/src/cache/relocate.ts Resolved
Comment thread packages/e2e/src/agent/step-cache.ts Resolved
Comment thread packages/e2e/src/cache/relocate.ts Resolved
@github-actions

github-actions Bot commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

🟢 e2e ios agent: 2 flaky, 19 passed, 1 skipped

25 agent steps · 11 replayed from cache · 62 model calls · 158.9k tokens (88% cached) · $0.0094

⚠️ 2 flaky tests passed on a retry

⚠️ act completes Login Form
tests-agent/scenarios.e2e.ts:257

ENVIRONMENT_UNAVAILABLE at step 5 of 5: agent.act "Complete this scenario as the screen instructs: log in with the given username and password", after 1m 5s and 8 model calls

agent.act blocked: Entered the provided username and password. The login screen remained visible; after dismissing the keyboard, the iOS automation runner repeatedly failed to present an accessibility snapshot due to an invalid viewport, so I could not verify or complete login.

  • Turn 5: press({"target":"n854","key":"Enter"}) → [press] Pressed Enter on #n854.
  • Turn 6: tap({"target":"n923"}) → [tap] Action failed: snapshot failed: the iOS automation runner could not present the accessibility snapshot (session e2e-mobile-benchmark-ios-0 on 5D7DA1AF-6E…
  • Turn 7: tap({"target":"n923"}) → [tap] Action failed: snapshot failed: the iOS automation runner could not present the accessibility snapshot (session e2e-mobile-benchmark-ios-0 on 5D7DA1AF-6E…

⚠️ act completes Gestures
tests-agent/scenarios.e2e.ts:257

ACTION_FAILED at step 4 of 4: agent.act "Complete this scenario as the screen instructs: long-press the first target, swipe the card left, then double-tap the last target", after 38.5s and 8 model calls

agent.act failed: The screen is still on Step 2 of 3 with the “Swipe me left” card visible after repeated leftward swipes, so the swipe did not register. I could not proceed to the final double-tap target.

  • Failed on 3 attempts: ASSERTION_FAILED at step 4, then ACTION_FAILED at step 4 (2 times).
  • Turn 5: screenshot({}) → [screenshot] The screen changed substantially since revision b8. Current screen (revision b10, path dev.e2e.benchmark / Benchmark Examples, 12 nodes):
  • Turn 6: swipe({"from":{"x":175,"y":465},"to":{"x":75,"y":465}}) → [swipe] Swiped from (175, 465) to (75, 465).
  • Turn 7: screenshot({}) → [screenshot] The screen changed substantially since revision b10. Current screen (revision b12, path dev.e2e.benchmark / Benchmark Examples, 12 nodes):
  • Screen: dev.e2e.benchmark / Benchmark Examples

Evidence: screenshot, log

All 22 tests in 1 file
Test Agent Time
⚠️ tests-agent/scenarios.e2e.ts · 2 flaky, 19 passed, 1 skipped 25 steps · 62 calls 11m 30s
⚠️ act completes Login Form (1 failed attempt first) 1 step · 10 calls 1m 26s
🟢 act completes Infinite Scroll List 1 step 51.3s
🟢 act completes Modal Flow 1 step 13.1s
🟢 act completes Bottom Tabs 1 step 11.3s
🟢 act completes Text Input Variations 1 step 25.0s
🟢 act completes Broken Accessibility 2 steps · 5 calls 41.4s
🟢 act completes Vision Only 2 steps · 1 call 17.8s
⚠️ act completes Gestures (3 failed attempts first) 1 step · 8 calls 34.9s
🟢 act completes Async States 1 step · 8 calls 32.3s
🟢 act completes Debounced Search 1 step · 3 calls 20.5s
🟢 act completes Flattened Registration Form 2 steps · 1 call 39.3s
🟢 act completes Flattened Login 2 steps · 10 calls 1m 14s
🟢 act completes Sticky Chrome Target 1 step 23.2s
🟢 act completes WebView Accessibility 1 step 16.5s
🟢 act completes Permission Prompt 1 step · 2 calls 30.7s
🟢 act completes Photo Picker 1 step · 2 calls 31.6s
🟢 act completes Product Catalog 1 step 13.9s
🟢 act completes Error Recovery 1 step 14.2s
🟢 act completes Choice Controls 1 step · 6 calls 42.9s
🟢 act completes Stripe PaymentSheet 1 step · 6 calls 57.2s
🟢 act completes Apple Pay 1 step 13.7s
⏭️ act completes Apple Pay Billing Address (skipped: the CI simulator's Apple Pay sheet skips the billing-address step, so the scenario cannot be completed there)

e2e 0.15.0-canary-20260928184528 · 15m 18s · ios-simulator · run artifacts

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 11 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread packages/e2e/src/agent/actions.ts Resolved
Comment thread packages/e2e/src/cache/anchors.ts Resolved
@okwasniewski

Copy link
Copy Markdown
Member Author

Bot findings, and what changed for each (commit 2bd1068):

  • Unchanged counters pass cached steps (Devin, cubic P1): sameShape compares a label exactly when its shape is nothing but a number or a time (isBareState), so a Counter left at 0 is not the recorded 1; a count inside a phrase still compares by shape. Test: relocate-testid.test.ts "bare counts".
  • Disappearing ids redirect twin actions (Devin, cubic P1): a position recorded among id twins now holds on the label rungs: a label rung that finds a different number of controls is skipped, so the step hands off rather than acting on a swapped twin. Test: "a recorded position after the id churned".
  • ies to y mangles movies (cubic P3): counted nouns are reduced to a stem their singular and plural share (countedStem: drop a trailing s, fold ie/y to i), so 1 movie and 3 movies shape alike. Test added.
  • missingAnchors absent when no screen could be read (cubic P2): endScreen now tells unreadable from elsewhere; an unreadable screen reports every recorded anchor missing, another route reports none, matching the ReplayedPrefix docblock.
  • Changing leaf displaces a stable container (Devin): anchors are chosen stable leaves first, then stable containers, then volatile leaves, then containers. Test: "prefers a stable container over leaves that are all volatile".

Also in that commit, per review of the direction: relocation degrades through an explicit ladder, test id, authored element id (web now projects id; minted ids are skipped), exact label, label shape, place among unnamed twins. The PR body's diagram and docs/cache.mdx describe it.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 71 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

A recorded test id is the identity of a control; twins sharing one resolve by
recorded position, never by label. Controls without an id, or whose id churned,
match by label exactly and then by shape (digits, plurals, relative times
folded). End anchors follow the same rule, are leaves ranked by durability,
and skip relative times and social tallies. An end-mismatch hand-off names the
anchors the screen did not show (step.cache.missingAnchors, agent notice).

@e2e-dev/mobile: every settings request names the worker's device through one
settings() helper, so a second booted device no longer fails clear-app-state
with an ambiguous match.
…findings

Relocation degrades from the strongest recorded identity: test id, then an
authored element id (web now projects the id attribute), then the exact label,
then the label's shape, then place among unnamed twins. A position recorded
among id twins holds on the label rungs. A bare count compares exactly, so an
unchanged counter is not the recorded effect; counted nouns stem the same in
singular and plural; an unreadable end screen reports every anchor missing;
stable containers outrank volatile leaves as anchors.
…escriptor now carries

The testbed's controls have authored ids, so anchors and targets record an
elementId; the assertions match on the fields they mean. The mid-step
divergence sabotage churns the ids as well as the label, since an id would
otherwise still find the button.
…nservative/6

The policy bump keyed every committed recording out, so the web and iOS
agentic jobs ran live and paid for it in model calls and flakiness. Both
suites re-recorded locally (web: 32 passed, iOS simulator: 22 passed); the
policy/5 entries are gone. Android has no committed recordings, as before.
…ey read, skip counter ids

A stress probe through the CLI found three places where replay passed a
step clean that main hands off, each acting on the wrong control or
accepting an effect that did not happen:

- `labelShape` folded every digit run, so `Delete item 3` found
  `Delete item 4`, `Page 2` found `Page 3`, `Order #1234` found another
  order. It now folds a count only where it governs a noun (`0 replies`,
  `3 followers`) and a relative time; a number that names something
  stays as it reads. `now` is a time only on its own, not in `Buy now`,
  a one-letter unit is lower case only (`3 M` is a size), and `-es`
  plurals stem with their singular (`match`/`matches`, `box`/`boxes`).
- End anchors compared by the same shape, so a recorded `Count: 1`
  passed on a screen left at `Count: 0`, `Step 2 of 3` at `Step 1 of 3`,
  `Cart (2 items)` at `Cart (1 item)`. Anchors now fold relative times
  only: a count is usually the very effect the anchor stands for.
- `stableElementId` let framework counters through (`mat-input-2`,
  `mui-3`, `headlessui-menu-button-1`), and the id rung outranks the
  label, so a field added above shifted the counter and the recorded
  `Email` was typed into `Phone` while `Email` sat beside it. Any
  trailing digit now disqualifies an id.

A recorded position now holds on every rung, not only below an id rung:
a rung that finds another number of twins than were recorded passes to
the next, and a lone survivor of a recorded set is ambiguous with its
candidates rather than found, since among label twins it is whichever
one still reads as recorded, the wrong one after the step acted on it.

Also the rebase onto main: the `settings.update` expectation main added
carries the device selection, and the orphaned JSDoc above `settings()`
in the mobile surface is back on `command()`. The invalid report fixture
exercises `missingAnchors`.
@okwasniewski
okwasniewski force-pushed the oskar/cache-relocate-label-drift branch from c72cb4c to cc6fe85 Compare September 30, 2026 11:31
… the replay policy to conservative/7

The element id went into the descriptor straight from the attribute; it
now passes through the same redaction and bounding as the other fields,
and an id redaction touched is no id. The matching rules changed since
the recordings under conservative/6 were made, so the policy version
moves and those entries cold-start. The `describeAnchors` docstring
names the order the code follows: stable leaves, stable containers,
then whatever appeared.
…nservative/7

Web: 29 entries, replays 25 of 31 steps under --strict-cache, the rest
designed gaps. iOS, on an iPhone 17 Pro simulator: 22 entries, a
read-only replay pass then runs 13 steps from cache and hands 8 off, as
the previous entries did in CI. No Android entries exist yet.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 54 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

@okwasniewski okwasniewski left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On 6b3cddc: one issue with end anchors, inline on anchors.ts:181.

When every anchor a step produced is volatile, so describeAnchors keeps them anyway, anchorMatches folds relative times and ages with sameLabels(..., 'times'). The pre step state can then satisfy its own anchor. A step that changes Due today to Due tomorrow, or Snooze: 5 minutes to Snooze: 10 minutes, records the new label as its anchor. Both labels fold to the same shape (due <age>), so if the step silently does nothing on replay, the screen left at the old value still passes and nothing hands off to the model. Before this change the comparison was exact, so this case handed off. The smallest fix may be to compare exactly for anchors that were only kept because no stable anchor existed, or to refuse a match whose label was already on the start screen. Is this reachable in practice, or does something upstream stop a date or duration change from being the only evidence of a step?

reviewed 6b3cddc by Hermes on behalf of @okwasniewski

const semantic = withoutIds(anchor);
const sameId = (anchor.testId !== undefined && candidate.testId === anchor.testId) || (anchor.elementId !== undefined && candidate.elementId === anchor.elementId);
const identified = sameId || !isAnonymous(semantic);
return identified && fieldsEqual(semantic, candidate, SEMANTIC_ID_FIELDS) && sameLabels(anchor, candidate, ['name', 'text'], 'times');

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sameLabels(..., 'times') folds Due today and Due tomorrow to the same due <age>. When only volatile anchors were kept, the pre step screen matches its own anchor and a silently failed step passes from cache. Should volatile only anchors compare exactly?

@okwasniewski

Copy link
Copy Markdown
Member Author

[claude-opus-5-5] responding on behalf of okwasniewski

Superseded by #805, which keeps this PR's label fold (narrowed to leading counts that agree with their noun) and twins rule, and leaves out the element id rung. Closing as discussed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant