# Driving real apps: gotchas for agents
A long real-app E2E session surfaces a recurring set of traps that every agent
re-discovers the hard way. This page collects them, with the workaround each one
now has built into `fluttersdk_dusk`. Read it once before driving a non-trivial
app; it will save you a dozen dead-end round trips.
---
## Table of contents
- [1. Refs go stale on rebuild](#1-refs-go-stale-on-rebuild)
- [2. Text fields may snapshot nested](#2-text-fields-may-snapshot-nested)
- [3. dusk:console needs (less than) you think](#3-duskconsole)
- [4. dusk:exceptions is cumulative](#4-duskexceptions-is-cumulative)
- [5. restart preserves the CDP port](#5-restart-preserves-the-cdp-port)
- [6. Overlays get stuck](#6-overlays-get-stuck)
---
## 1. Refs go stale on rebuild
`e` refs from `dusk:snap` freeze the widget at snapshot time. A navigation,
a modal open/close, a `setState`, or any significant rebuild invalidates them:
the next action on a stale `e` fails with a not-found or stale envelope.
**Workaround:**
- Re-snap (`dusk:snap`) after any action that changes the screen, and use the
fresh refs.
- For a target that survives re-renders (a stable `Text`, accessibility label,
or `Key`), prefer `dusk:find` / `dusk:observe`, which mint a re-resolvable
`q` handle that re-walks the live tree on every action call. `q`
handles survive rebuild, route push, and snapshot disposal as long as the
predicates still match.
- Pointer verbs (`dusk:tap`, `dusk:hover`, `dusk:drag`, ...) now dispatch at the
element's LIVE rect, not the cached snapshot rect, so a target that merely
shifted slots (same `Element`/`RenderObject`) is still hit correctly. The
false-success class (gate passes, pointer lands on the stale position,
`onTap` never fires) is gone for slot shifts; genuine rebuilds still need a
re-snap.
---
## 2. Text fields may snapshot nested
A wind `WInput` (and any `TextField` wrapped in `Semantics(textField: true)`)
historically snapshotted as TWO nested `textbox` nodes, because `RenderEditable`
unconditionally owns its own `textField` Semantics node and `MergeSemantics`
cannot absorb it. Agents naturally targeted the inner leaf, where `dusk:type`
threw a `-32000`.
**Workaround:**
- `dusk:snap` now collapses the nested pair (by render-object containment, never
label/value equality, so two sibling fields sharing a label stay distinct) and
emits a single ref for the outer node, marked `typeable: true`. Target the
node carrying `typeable: true`.
- Better: use `dusk:fill --ref=[ --text=`, which focuses, clears,
types, and settles in one call (and retries once on a stale handle). It
resolves the right editable for you and composes the gated focus/clear/type
handlers, so you do not re-build the focus + clear + type + settle dance.
---
## 3. dusk:console captures debugPrint in-package now
`dusk:console` historically surfaced full structured logs only when
`fluttersdk_telescope` was installed and wired.
**Workaround:**
- `DuskPlugin.install()` now chains a `debugPrint` override that records every
`debugPrint(...)` / `print(...)` call into a bounded in-package ring buffer,
so those entries appear in `dusk:console` even without telescope.
- When telescope IS installed it enriches the output with `Logger.root.onRecord`
entries and its other watchers. Direct `dart:developer log()` calls that
bypass `debugPrint` still require telescope's `LogWatcher`.
---
## 4. dusk:exceptions is cumulative
`dusk:exceptions` returns the full exception history by default, so a single
pre-existing error keeps re-appearing after every action and produces false
positives when you are checking whether YOUR action raised something new.
**Workaround:**
- Record the current time before the action, then pass
`dusk:exceptions --since=` afterwards to get only exceptions raised
strictly after that timestamp. Unparseable `since` values are treated as
absent (full list).
---
## 5. restart preserves the CDP port
When artisan was started with `--cdp-port` (the web path that backs
`dusk:screenshot` CDP fallback, `dusk:resize`, `dusk:device`), a naive restart
used to drop the port, breaking those CDP-routed tools until you restarted with
the flag again.
**Workaround:**
- `artisan restart` (and bare `start`) now re-read the prior `cdpPort` from
`~/.artisan/state.json` and reuse it as the default, so CDP stays wired across
restarts. An explicit `--cdp-port` still wins.
---
## 6. Overlays get stuck
A left-over dialog, bottom sheet, dropdown menu, or barrier modal blocks the
next screen from rendering, and a single `dusk:modal` (dismiss-modals) does not
clear overlays that are not `PopupRoute`s.
**Workaround:**
- Use `dusk:reset_overlays`, which runs three idempotent layers: dismiss every
`PopupRoute`, press `Escape` (for shortcut-driven overlays), then tap a
Cancel/Dismiss/Close/OK/Done affordance as a last resort. Safe to call
speculatively between flows; the response (`popped`, `escaped`,
`dismissTapped`) tells you which layer cleared the screen.
---
## See also
- [dusk:fill](../commands/dusk-fill.md), [dusk:reset_overlays](../commands/dusk-reset-overlays.md)
- [dusk:tap](../commands/dusk-tap.md) (`--verify`, `--until`)
- [dusk:exceptions](../commands/index.md), [dusk:find](../commands/dusk-find.md)
- [Reference: Actionability gate](../reference/actionability-gate.md)
]