Skip to content

Commit 1ecbec9

Browse files
authored
feat: replay --save-script for self-updating e2e tests; add it assertions; add DSL with selectors (#24)
* feat: for self-healing e2e tests; assertions * rework ad scripts
1 parent 9a3570f commit 1ecbec9

31 files changed

Lines changed: 2496 additions & 261 deletions

AGENTS.md

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ Minimal operating guide for AI coding agents in this repo.
1414
- Use daemon session flow for interactions (`open` before interactions, `close` after).
1515
- Do not remove shared snapshot/session model behavior without full migration.
1616
- If Swift runner code changes, run `pnpm build:xcuitest`.
17+
- Do not add command logic to `daemon.ts` — it is a thin router. Use handler modules.
18+
- Use `inferFillText` and `uniqueStrings` from `src/daemon/action-utils.ts`. Do not duplicate.
19+
- Use `evaluateIsPredicate` from `src/daemon/is-predicates.ts` for assertion logic. Do not inline.
1720

1821
## Architecture In One Screen
1922

@@ -24,14 +27,18 @@ Minimal operating guide for AI coding agents in this repo.
2427
2. Daemon client transport:
2528
- `src/daemon-client.ts`
2629
3. Daemon server bootstrap/router:
27-
- `src/daemon.ts`
30+
- `src/daemon.ts` — thin router only, delegates to handler modules
2831
4. Daemon command families:
2932
- session/apps/appstate/open/close/replay: `src/daemon/handlers/session.ts`
33+
- click/fill/get/is: `src/daemon/handlers/interaction.ts`
3034
- snapshot/wait/alert/settings: `src/daemon/handlers/snapshot.ts`
3135
- semantic find actions: `src/daemon/handlers/find.ts`
3236
- record/trace: `src/daemon/handlers/record-trace.ts`
3337
5. Daemon shared domain:
3438
- session state + logs: `src/daemon/session-store.ts`
39+
- selector DSL (parse, resolve, build): `src/daemon/selectors.ts`
40+
- `is` predicate evaluation: `src/daemon/is-predicates.ts`
41+
- shared action helpers (inferFillText, uniqueStrings): `src/daemon/action-utils.ts`
3542
- snapshot tree shaping + label resolution: `src/daemon/snapshot-processing.ts`
3643
- handler context helpers: `src/daemon/context.ts`, `src/daemon/device-ready.ts`, `src/daemon/app-state.ts`
3744
6. Platform dispatch/backends:
@@ -55,34 +62,42 @@ Do not read both iOS and Android paths unless issue is explicitly cross-platform
5562

5663
- `session list`, `devices`, `apps`, `appstate`, `open`, `close`, `replay`:
5764
- `src/daemon/handlers/session.ts`
65+
- `click`, `fill`, `get`, `is`:
66+
- `src/daemon/handlers/interaction.ts`
5867
- `snapshot`, `wait`, `alert`, `settings`:
5968
- `src/daemon/handlers/snapshot.ts`
6069
- `find ...`:
6170
- `src/daemon/handlers/find.ts`
6271
- `record start|stop`, `trace start|stop`:
6372
- `src/daemon/handlers/record-trace.ts`
64-
- `click`, `fill`, `get`, generic passthrough:
65-
- `src/daemon.ts` (remaining fallback logic)
73+
- Generic passthrough (press, scroll, type, etc.):
74+
- `src/daemon.ts` (fallback after all handlers return null)
6675

6776
## Capability Source Of Truth
6877

6978
- Command/device support must come from `src/core/capabilities.ts`.
7079
- Do not scatter new support checks across handlers.
7180

81+
## Selector System Rules
82+
83+
All interaction commands (`click`, `fill`, `get`, `is`) and `wait` accept selectors in addition to `@ref`.
84+
The selector pipeline is: **parse → resolve → act → record selectorChain → heal on replay**.
85+
86+
- Selector DSL lives in `src/daemon/selectors.ts`. Do not duplicate parsing/matching logic elsewhere.
87+
- `buildSelectorChainForNode` generates fallback chains stored in action results. Always call it after resolving a node for an interaction — it powers replay healing.
88+
- When adding a new interaction command that targets a UI element: support both `@ref` and selector input, record `selectorChain`, and update replay healing (`healReplayAction` + `collectReplaySelectorCandidates` in `session.ts`).
89+
- When adding a new selector key: update `SelectorKey` type, `ALL_KEYS`/`TEXT_KEYS`/`BOOLEAN_KEYS` sets, `matchesTerm`, and `isSelectorToken` — all in `selectors.ts`.
90+
- When adding a new `is` predicate: update `IsPredicate` type and `evaluateIsPredicate` in `is-predicates.ts`, not in the handler.
91+
- `daemon.ts` must stay a thin router. Do not add command logic there — use the appropriate handler module.
92+
7293
## Testing Strategy
7394

7495
### Test placement policy
7596

7697
- Unit tests are colocated with source files under `src/**`.
7798
- Use `__tests__` folders colocated with the related source folder.
7899
- The `test/**` tree is integration-only (including smoke integration tests).
79-
80-
### Unit tests (default for all refactors, colocated)
81-
82-
- `src/core/__tests__/capabilities.test.ts`
83-
- `src/daemon/handlers/__tests__/find.test.ts`
84-
- `src/daemon/__tests__/snapshot-processing.test.ts`
85-
- `src/daemon/__tests__/session-store.test.ts`
100+
- Example: tests for `src/daemon/selectors.ts` go in `src/daemon/__tests__/selectors.test.ts`.
86101

87102
Add/extend colocated unit tests in the same PR for touched module logic.
88103

README.md

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,11 @@ npx agent-device open SampleApp
3333

3434
## Quick Start
3535

36+
Use refs for agent-driven exploration and normal automation flows.
37+
3638
```bash
3739
agent-device open Contacts --platform ios # creates session on iOS Simulator
38-
agent-device snapshot
40+
agent-device snapshot
3941
agent-device click @e5
4042
agent-device fill @e6 "John"
4143
agent-device fill @e7 "Doe"
@@ -75,7 +77,7 @@ Coordinates:
7577
## Command Index
7678
- `open`, `close`, `home`, `back`, `app-switcher`
7779
- `snapshot`, `find`, `get`
78-
- `click`, `focus`, `type`, `fill`, `press`, `long-press`, `scroll`, `scrollintoview`
80+
- `click`, `focus`, `type`, `fill`, `press`, `long-press`, `scroll`, `scrollintoview`, `is`
7981
- `alert`, `wait`, `screenshot`
8082
- `trace start`, `trace stop`
8183
- `settings wifi|airplane|location on|off`
@@ -117,14 +119,45 @@ Sessions:
117119
- If a session is already open, `open <app>` switches the active app and updates the session app bundle.
118120
- `close` stops the session and releases device resources. Pass an app to close it explicitly, or omit to just close the session.
119121
- Use `--session <name>` to manage multiple sessions.
120-
- Session logs are written to `~/.agent-device/sessions/<session>-<timestamp>.ad`.
121-
- With `--record-json`, JSON logs are written to `~/.agent-device/sessions/<session>-<timestamp>.json` by default.
122+
- Session scripts are written to `~/.agent-device/sessions/<session>-<timestamp>.ad` when recording is enabled with `--save-script`.
123+
- Deterministic replay is `.ad`-based; use `replay --update` (`-u`) to update selector drift and rewrite the replay file in place.
122124

123125
Find (semantic):
124126
- `find <text> <action> [value]` finds by any text (label/value/identifier) using a scoped snapshot.
125127
- `find text|label|value|role|id <value> <action> [value]` for specific locators.
126128
- Actions: `click` (default), `fill`, `type`, `focus`, `get text`, `get attrs`, `wait [timeout]`, `exists`.
127129

130+
Assertions:
131+
- `is` predicates: `visible`, `hidden`, `exists`, `editable`, `selected`, `text`.
132+
- `is text` uses exact equality.
133+
134+
Replay update:
135+
- `replay <path>` runs deterministic replay from `.ad` scripts.
136+
- `replay -u <path>` attempts selector updates on failures and atomically rewrites the same file.
137+
- Refs are the default/core mechanism for interactive agent flows.
138+
- Update targets: `click`, `fill`, `get`, `is`, `wait`.
139+
- Selector matching is a replay-update internal: replay parses `.ad` lines into actions, tries them, snapshots on failure, resolves a better selector, then rewrites that failing line.
140+
141+
Update examples:
142+
143+
```sh
144+
# Before (stale selector)
145+
click "id=\"old_continue\" || label=\"Continue\""
146+
147+
# After replay -u (rewritten in place)
148+
click "id=\"auth_continue\" || label=\"Continue\""
149+
```
150+
151+
```sh
152+
# Before (ref-based action from discovery)
153+
snapshot -i -c -s "Continue"
154+
click @e13 "Continue"
155+
156+
# After replay -u (upgraded to selector-based action)
157+
snapshot -i -c -s "Continue"
158+
click "id=\"auth_continue\" || label=\"Continue\""
159+
```
160+
128161
Android fill reliability:
129162
- `fill` clears the current value, then enters text.
130163
- `type` enters text into the focused field without clearing.

skills/agent-device/SKILL.md

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ description: Automates mobile and simulator interactions for iOS and Android dev
55

66
# Mobile Automation with agent-device
77

8+
For agent-driven exploration: use refs. For deterministic replay scripts: use selectors.
9+
810
## Quick start
911

1012
```bash
@@ -26,9 +28,9 @@ npx -y agent-device
2628
## Core workflow
2729

2830
1. Open app or just boot device: `open [app]`
29-
2. Snapshot: `snapshot` to get full XCTest accessibility tree snapshot
31+
2. Snapshot: `snapshot` to get refs from accessibility tree
3032
3. Interact using refs (`click @ref`, `fill @ref "text"`)
31-
4. Re-snapshot after navigation or UI changes
33+
4. Re-snapshot after navigation/UI changes
3234
5. Close session when done
3335

3436
## Commands
@@ -109,6 +111,8 @@ agent-device home
109111
agent-device app-switcher
110112
agent-device wait 1000
111113
agent-device wait text "Settings"
114+
agent-device is visible 'id="settings_anchor"' # selector assertions for deterministic checks
115+
agent-device is text 'id="header_title"' "Settings"
112116
agent-device alert get
113117
```
114118

@@ -120,6 +124,16 @@ agent-device get attrs @e1
120124
agent-device screenshot out.png
121125
```
122126

127+
### Deterministic replay and updating
128+
129+
```bash
130+
agent-device open App --save-script # Save session script (.ad) on close
131+
agent-device replay ./session.ad # Run deterministic replay from .ad script
132+
agent-device replay -u ./session.ad # Update selector drift and rewrite .ad script in place
133+
```
134+
135+
`replay` reads `.ad` recordings.
136+
123137
### Trace logs (AX/XCTest)
124138

125139
```bash
@@ -142,7 +156,8 @@ agent-device apps --platform android --user-installed
142156
## Best practices
143157

144158
- Pinch (`pinch <scale> [x y]`) is supported on iOS simulators and Android; scale > 1 zooms in, < 1 zooms out. On Android, pinch uses multi-touch `sendevent` injection.
145-
- Always snapshot right before interactions; refs invalidate on UI changes.
159+
- Snapshot refs are the core mechanism for interactive agent flows.
160+
- Use selectors for deterministic replay artifacts and assertions (e.g. in e2e test workflows).
146161
- Prefer `snapshot -i` to reduce output size.
147162
- On iOS, `xctest` is the default and does not require Accessibility permission.
148163
- If XCTest returns 0 nodes (foreground app changed), agent-device falls back to AX when available.
@@ -153,6 +168,7 @@ agent-device apps --platform android --user-installed
153168
- Use `fill` when you want clear-then-type semantics.
154169
- Use `type` when you want to append/enter text without clearing.
155170
- On Android, prefer `fill` for important fields; it verifies entered text and retries once when IME reorders characters.
171+
- If using deterministic replay scripts, use `replay -u` during maintenance runs to update selector drift in replay scripts. Use plain `replay` in CI.
156172

157173
## References
158174

skills/agent-device/references/session-management.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,18 @@ Sessions isolate device context. A device can only be held by one session at a t
1414
- Name sessions semantically.
1515
- Close sessions when done.
1616
- Use separate sessions for parallel work.
17+
- For deterministic replay scripts, prefer selector-based actions and assertions.
18+
- Use `replay -u` to update selector drift during maintenance.
1719

1820
## Listing sessions
1921

2022
```bash
2123
agent-device session list
2224
```
25+
26+
## Replay within sessions
27+
28+
```bash
29+
agent-device replay ./session.ad --session auth
30+
agent-device replay -u ./session.ad --session auth
31+
```

skills/agent-device/references/snapshot-refs.md

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
# Snapshot + Refs Workflow (Mobile)
1+
# Snapshot Refs and Selectors (Mobile)
22

33
## Purpose
44

5-
Refs let agents interact without repeating full UI trees. Snapshot -> refs -> click/fill.
5+
Refs are useful for discovery/debugging. For deterministic scripts, use selectors.
66

77
## Snapshot
88

@@ -21,17 +21,25 @@ App: com.apple.Preferences
2121
@e3 [button] "Privacy & Security"
2222
```
2323

24-
## Using refs
24+
## Using refs (discovery/debug)
2525

2626
```bash
2727
agent-device click @e2
2828
agent-device fill @e5 "test"
2929
```
3030

31+
## Using selectors (deterministic)
32+
33+
```bash
34+
agent-device click 'id="camera_row" || label="Camera" role=button'
35+
agent-device fill 'id="search_input" editable=true' "test"
36+
agent-device is visible 'id="camera_settings_anchor"'
37+
```
38+
3139
## Ref lifecycle
3240

33-
Refs become invalid when UI changes (navigation, modal, dynamic list updates).
34-
Always re-snapshot after any transition.
41+
Refs can become invalid when UI changes (navigation, modal, dynamic list updates).
42+
Re-snapshot after transitions if you keep using refs.
3543

3644
## Scope snapshots
3745

@@ -47,3 +55,8 @@ agent-device snapshot -i -s @e3
4755
- Ref not found: re-snapshot.
4856
- AX returns Simulator window: restart Simulator and re-run.
4957
- AX empty: verify Accessibility permission or use `--backend xctest` (XCTest is more complete).
58+
59+
## Replay note
60+
61+
- Prefer selector-based actions in recorded `.ad` replays.
62+
- Use `agent-device replay -u <path>` to update selector drift and rewrite replay scripts in place.

skills/agent-device/references/video-recording.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ agent-device record start ./recordings/ios.mov
1212

1313
# Perform actions
1414
agent-device open App
15-
agent-device snapshot
15+
agent-device snapshot -i
1616
agent-device click @e3
1717
agent-device close
1818

@@ -30,7 +30,7 @@ agent-device record start ./recordings/android.mp4
3030

3131
# Perform actions
3232
agent-device open App
33-
agent-device snapshot
33+
agent-device snapshot -i
3434
agent-device click @e3
3535
agent-device close
3636

src/cli.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,12 @@ export async function runCli(argv: string[]): Promise<void> {
9393
return;
9494
}
9595
}
96+
if (command === 'is') {
97+
const predicate = (response.data as any)?.predicate ?? 'assertion';
98+
process.stdout.write(`Passed: is ${predicate}\n`);
99+
if (logTailStopper) logTailStopper();
100+
return;
101+
}
96102
if (command === 'click') {
97103
const ref = (response.data as any)?.ref ?? '';
98104
const x = (response.data as any)?.x;

src/core/capabilities.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ const COMMAND_CAPABILITY_MATRIX: Record<string, CommandCapability> = {
2525
find: { ios: { simulator: true }, android: { emulator: true, device: true, unknown: true } },
2626
focus: { ios: { simulator: true }, android: { emulator: true, device: true, unknown: true } },
2727
get: { ios: { simulator: true }, android: { emulator: true, device: true, unknown: true } },
28+
is: { ios: { simulator: true }, android: { emulator: true, device: true, unknown: true } },
2829
home: { ios: { simulator: true }, android: { emulator: true, device: true, unknown: true } },
2930
'long-press': { ios: { simulator: true }, android: { emulator: true, device: true, unknown: true } },
3031
open: { ios: { simulator: true }, android: { emulator: true, device: true, unknown: true } },

src/core/dispatch.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,11 @@ export type CommandFlags = {
3333
snapshotScope?: string;
3434
snapshotRaw?: boolean;
3535
snapshotBackend?: 'ax' | 'xctest';
36+
saveScript?: boolean;
3637
noRecord?: boolean;
37-
recordJson?: boolean;
3838
appsFilter?: 'launchable' | 'user-installed' | 'all';
3939
appsMetadata?: boolean;
40+
replayUpdate?: boolean;
4041
};
4142

4243
export async function resolveTargetDevice(flags: CommandFlags): Promise<DeviceInfo> {

0 commit comments

Comments
 (0)