verikun 0.19.0 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/verikun/SKILL.md +535 -0
- package/CHANGELOG.md +965 -0
- package/README.md +26 -754
- package/dist/capture.js +29 -0
- package/dist/cli.js +106 -17
- package/dist/companion/manager.js +397 -0
- package/dist/companion/protocol.js +111 -0
- package/dist/companion/sock-client.js +22 -0
- package/dist/drivers/adb.js +105 -1
- package/dist/errors.js +23 -1
- package/dist/image.js +40 -0
- package/dist/run.js +4 -1
- package/dist/update-check.js +189 -0
- package/dist/version.js +1 -1
- package/example/README.md +100 -0
- package/example/example-test-devicestate.md +53 -0
- package/example/example-test.md +40 -0
- package/package.json +7 -3
- package/tools/verikun-companion/prebuilt/verikun-companion.jar +0 -0
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: verikun
|
|
3
|
+
description: >-
|
|
4
|
+
Drive and verify a connected Android device/emulator the way Puppeteer drives a
|
|
5
|
+
browser: tap, type, swipe, screenshot, and — most importantly — inspect the UI
|
|
6
|
+
hierarchy by semantic identifiers (resource-id, visible text, accessibility
|
|
7
|
+
label) to confirm what is on screen. Use whenever a task means interacting with
|
|
8
|
+
or asserting the state of a native app on a device/emulator: "tap the login
|
|
9
|
+
button", "type into the email field", "verify the screen shows X", "scroll down
|
|
10
|
+
and check Y", "automate this signup flow", "screenshot the current screen",
|
|
11
|
+
"is the spinner gone yet". Prefer this over raw adb. Selector commands auto-wait
|
|
12
|
+
~5s for elements to appear, so you rarely need explicit waits (`--no-wait` opts
|
|
13
|
+
out). Recorded actions form a
|
|
14
|
+
test run you can archive to a JUnit + HTML report (`vk run archive`) — use when
|
|
15
|
+
asked to "test", "verify the flow", or "produce a report". Run a whole known
|
|
16
|
+
flow in one call with `vk batch` (commands piped on stdin or via --file); run a
|
|
17
|
+
directory of natural-language tests as a gated suite with `vk suite <dir>`
|
|
18
|
+
(overview report + non-zero exit on failure — the CI gate). A remote device is
|
|
19
|
+
reachable via `--server <url>` (ai/suite/install) against a `vk server` running
|
|
20
|
+
next to it. iOS (--ios): full parity via idb (tap/type/swipe/`ui` +
|
|
21
|
+
screenshot/launch/stop), on simulators and devices; install idb and see
|
|
22
|
+
`vk doctor --ios`.
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
# verikun — drive & verify mobile apps
|
|
26
|
+
|
|
27
|
+
`vk` operates a connected Android device/emulator and reads its screen as
|
|
28
|
+
structured, **semantic** elements, so you can act and then *verify* — like
|
|
29
|
+
Puppeteer for native apps. Prefer it over raw `adb`.
|
|
30
|
+
|
|
31
|
+
The command is `vk` (after `npm install -g verikun`, or `npm link` from a source
|
|
32
|
+
clone) or, if not linked, `node dist/bin/verikun.js` from the repo root. All
|
|
33
|
+
examples below use `vk`.
|
|
34
|
+
|
|
35
|
+
## Before you start (once per session)
|
|
36
|
+
|
|
37
|
+
Run `vk doctor` once before the first device command. Alongside the adb/device checks it
|
|
38
|
+
reports version staleness on stderr — it never fails on that, so treat it as information:
|
|
39
|
+
|
|
40
|
+
- **`claude-code-plugin: … behind this CLI`** → **this skill file is out of date.** Trust
|
|
41
|
+
`vk --help` over what you read here when they disagree, and tell the user to run
|
|
42
|
+
`claude plugin update verikun@verikun` and restart Claude Code.
|
|
43
|
+
- **`verikun: … npm has <newer>`** → tell the user `npm install -g verikun@latest`.
|
|
44
|
+
|
|
45
|
+
**Tell the user, don't run it.** Upgrading changes their machine, and the plugin path needs
|
|
46
|
+
a Claude Code restart to take effect. Mention it once and move on.
|
|
47
|
+
|
|
48
|
+
## The loop: act → inspect → assert
|
|
49
|
+
|
|
50
|
+
1. **See** the screen → `vk ui`
|
|
51
|
+
2. **Act** by semantic selector → `vk tap @login_button`
|
|
52
|
+
3. **Verify** the result → `vk assert text:"Welcome"` (or `vk ui` again)
|
|
53
|
+
|
|
54
|
+
Never guess coordinates. Reference elements by their identifiers and let `vk`
|
|
55
|
+
resolve the tap point. This is the whole point of the tool.
|
|
56
|
+
|
|
57
|
+
## Inspect — the core capability
|
|
58
|
+
|
|
59
|
+
- `vk ui` — compact list of every interactive/labeled element, one per line:
|
|
60
|
+
```
|
|
61
|
+
[3] Button "Sign in" @sign_in_btn (540,1020) tap
|
|
62
|
+
[4] EditText @email_input (540,720) focused
|
|
63
|
+
```
|
|
64
|
+
Fields: `[index] Type "text" @resource-id (centerX,centerY) flags`. The `@id`
|
|
65
|
+
token can be pasted straight back into a selector.
|
|
66
|
+
- `vk ui --tree` — indented to show nesting. Add `--all` to include layout nodes.
|
|
67
|
+
- `vk ui --json` — structured output for parsing.
|
|
68
|
+
- `vk find <selector>` — print matching elements; exit 1 if none.
|
|
69
|
+
- `vk assert <selector> [--text S] [--gone]` — exit 0 pass / 1 fail. For checks.
|
|
70
|
+
- `vk wait <selector> [--gone] [--timeout ms] [--interval ms]` — poll until the
|
|
71
|
+
element appears (or disappears with `--gone`). Essential for async UI.
|
|
72
|
+
- `vk current` — foreground app/activity.
|
|
73
|
+
- `vk log [package] [-n N]` — recent device logs (Android `logcat` snapshot).
|
|
74
|
+
Reach for this **after a failure** to read the crash/stack trace the screen
|
|
75
|
+
can't show you. **Inside a run it defaults to logs since the run started**, so
|
|
76
|
+
you only see this session's output (not stale logs from before); `-n N` gives
|
|
77
|
+
the last N lines instead, `--since '<MM-DD HH:MM:SS.mmm>'` sets an explicit
|
|
78
|
+
start, `--full` dumps everything. A `package` scopes to that app (system-wide
|
|
79
|
+
if it has already crashed). Unlike other inspect commands it is *recorded*, so
|
|
80
|
+
during a run the logs are pulled **into the archived report** next to the step.
|
|
81
|
+
|
|
82
|
+
## Act
|
|
83
|
+
|
|
84
|
+
Selector lookups **auto-wait up to 5s** (see [Auto-wait](#auto-wait)), so you
|
|
85
|
+
usually don't need a `wait` before an action — `vk tap @next` already polls for
|
|
86
|
+
`@next` to appear.
|
|
87
|
+
|
|
88
|
+
- `vk tap <selector|index>` · `vk tap --at x,y`
|
|
89
|
+
- `vk text <selector> "the text" [--clear] [--enter]` — focus the field, then type
|
|
90
|
+
- `vk type "text" [--enter]` — type into the already-focused field
|
|
91
|
+
- `vk swipe up|down|left|right [--on <selector>] [--distance f] [--duration ms]`
|
|
92
|
+
- `vk swipe --from x,y --to x,y [--duration ms]`
|
|
93
|
+
- `vk key <name|code>` · `vk back` · `vk home` · `vk enter`
|
|
94
|
+
- `vk screenshot [--out path] [--more] [--max px] [--full]` — saves a PNG (default
|
|
95
|
+
`./.verikun/screen.png`) and prints the path; then read that file to *see* the
|
|
96
|
+
screen. It's **downscaled to a 700px longest edge by default** to save tokens
|
|
97
|
+
(text stays legible); add `--more` if a screen reads too coarse, `--max px` for
|
|
98
|
+
an exact cap, or `--full` for the original.
|
|
99
|
+
- `vk launch <pkg> [--clear] [--no-restart]` · `vk stop <pkg>` · `vk clear <pkg>`
|
|
100
|
+
`vk launch` **restarts by default**: it force-stops the app first so a rerun starts
|
|
101
|
+
from a fresh launch instead of landing on whatever screen a still-running instance
|
|
102
|
+
was left on (re-issuing the launch intent to a live app just resurfaces its current
|
|
103
|
+
state). force-stop is a no-op if the app isn't running. `--no-restart` skips it (just
|
|
104
|
+
bring the existing instance forward).
|
|
105
|
+
`vk clear` (and `vk launch --clear`) additionally wipe the app's local data —
|
|
106
|
+
login/session, prefs, cache — so a flow starts from a clean, logged-out,
|
|
107
|
+
fresh-install state. Android only (`pm clear`, which also force-stops the app); iOS
|
|
108
|
+
has no per-app data reset, so `clear` exits 3 there.
|
|
109
|
+
|
|
110
|
+
## Change the device, not just the app
|
|
111
|
+
|
|
112
|
+
Some behaviour only appears when the device changes underneath the app — the offline
|
|
113
|
+
banner, the retry path, dark theme, a layout that breaks at accessibility text sizes.
|
|
114
|
+
|
|
115
|
+
- `vk device set <key>=<value> ...` · `vk device get [key]` · `vk device reset` · `vk device caps`
|
|
116
|
+
|
|
117
|
+
| key | values | Android | iOS simulator |
|
|
118
|
+
|---|---|---|---|
|
|
119
|
+
| `airplane` | `on\|off` | yes | **no** — a simulator has no radio |
|
|
120
|
+
| `dark` | `on\|off` | yes | yes |
|
|
121
|
+
| `font-scale` | `0.5`–`3.0`, `default` | yes | yes (nearest Dynamic Type category) |
|
|
122
|
+
| `rotation` | `portrait\|landscape\|portrait-reverse\|landscape-reverse\|auto` | yes | **no** |
|
|
123
|
+
| `stay-awake` | `on\|off` | yes | no-op (simulators don't sleep) |
|
|
124
|
+
|
|
125
|
+
Set several at once: `vk device set dark=on font-scale=1.3`. Each change is **verified by
|
|
126
|
+
reading it back**, so success means it actually landed — these device commands silently
|
|
127
|
+
no-op on some OEM skins. An unsupported key exits **3 before touching the device** and
|
|
128
|
+
names the manual equivalent; `vk device caps` prints the live matrix for your platform.
|
|
129
|
+
|
|
130
|
+
**Always `vk device reset` when the scenario is done.** It restores what the run changed,
|
|
131
|
+
from the snapshot taken before each change. `batch`, `ai` and `suite` reset automatically
|
|
132
|
+
even when the flow *fails* — but a bare `vk device set` from a shell stays applied until
|
|
133
|
+
you reset it, so don't leave someone's phone in airplane mode.
|
|
134
|
+
|
|
135
|
+
Two traps:
|
|
136
|
+
- **`airplane=off` brings the radio back, not the internet.** Follow it with
|
|
137
|
+
`vk assert <selector> --wait 10s`, never an immediate `tap`.
|
|
138
|
+
- **Over wireless adb, `airplane=on` is refused** (exit 2) — it would cut the connection
|
|
139
|
+
carrying your next command. Use USB, or `--allow-wireless` if you accept losing it.
|
|
140
|
+
|
|
141
|
+
## Selectors
|
|
142
|
+
|
|
143
|
+
| Form | Matches |
|
|
144
|
+
|---|---|
|
|
145
|
+
| `@login` | resource-id (full, `/suffix`, or short name) |
|
|
146
|
+
| `id:login` | same as `@login` |
|
|
147
|
+
| `text:Sign in` | visible text (case-insensitive; auto-heals) |
|
|
148
|
+
| `desc:Submit` | content-desc / accessibility label |
|
|
149
|
+
| `class:Button` | simplified type or full class name |
|
|
150
|
+
| `"Sign in"` | a bare string == `text:Sign in` |
|
|
151
|
+
|
|
152
|
+
**Matching auto-heals** — always case-insensitive, trying **exact → partial
|
|
153
|
+
(substring) → normalized** (ignore punctuation/whitespace/emoji), stopping at the
|
|
154
|
+
first tier that hits. So `text:sign up`, `text:SIGN UP`, and `text:signup` all
|
|
155
|
+
find a "Sign up" button. Exact always wins (a partial never shadows an exact
|
|
156
|
+
match); a non-exact hit is flagged in the output as `(healed: …)`. Ambiguity is
|
|
157
|
+
never auto-resolved — if the winning tier has >1 match, an action lists the
|
|
158
|
+
candidates and exits 2 rather than guess.
|
|
159
|
+
|
|
160
|
+
Modifiers: `--contains` forces substring (skips the exact tier); `--index N`
|
|
161
|
+
picks the Nth match (0-based) when a selector intentionally matches several;
|
|
162
|
+
`--no-scroll` stops an action scrolling its target into view (see below).
|
|
163
|
+
|
|
164
|
+
**State modifiers** require an a11y attribute, in both polarities. Unset means
|
|
165
|
+
*don't care*:
|
|
166
|
+
|
|
167
|
+
| Modifier | Matches | Negative |
|
|
168
|
+
|---|---|---|
|
|
169
|
+
| `--enabled` | actionable right now | `--not-enabled` |
|
|
170
|
+
| `--selected` | current option of a segmented control / tab / mode picker | `--not-selected` |
|
|
171
|
+
| `--checked` | ticked checkbox / switch / radio | `--not-checked` |
|
|
172
|
+
| `--focused` | holds input focus | `--not-focused` |
|
|
173
|
+
|
|
174
|
+
Reach for `--enabled` on any button the app keeps disabled until something else
|
|
175
|
+
is done — a Check/Submit/Continue that lights up only once an answer is picked
|
|
176
|
+
or a form validates. Such a button is *present* long before it is usable, so a
|
|
177
|
+
plain presence match taps a dead control, nothing happens, and the failure
|
|
178
|
+
surfaces several steps later as a puzzling timeout on whatever should have come
|
|
179
|
+
next. With auto-wait, `--enabled` means "wait until it is actually pressable".
|
|
180
|
+
|
|
181
|
+
Reach for the **negative** forms whenever tapping a control *toggles* it. A
|
|
182
|
+
segmented control whose options share one handler flips on any tap, so an
|
|
183
|
+
unconditional "tap the option I want" lands on the other one whenever it was
|
|
184
|
+
already chosen — exit 0, no warning, and the rest of the run exercises the wrong
|
|
185
|
+
mode. Check first, then act:
|
|
186
|
+
|
|
187
|
+
```sh
|
|
188
|
+
vk find "@mode_video --not-selected" --no-wait && vk tap @mode_video
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
A modifier is a flag **or** a suffix on the selector string (as above) — the
|
|
192
|
+
string form is what lets a `vk ai` control node carry one, since `if-present` /
|
|
193
|
+
`when` / `repeat` / `while-present` / `read` hold a bare selector with nowhere to
|
|
194
|
+
put a flag: `if-present "id:mode_video --not-selected" { tap id:mode_video }`.
|
|
195
|
+
|
|
196
|
+
**`--selected` and `--focused` do not exist on iOS.** `idb` emits no such key, so
|
|
197
|
+
using them with `--ios` exits **3** instead of matching nothing forever.
|
|
198
|
+
`--enabled` and `--checked` work on both platforms.
|
|
199
|
+
|
|
200
|
+
## Auto-wait
|
|
201
|
+
|
|
202
|
+
Selector commands (`tap`, `text`, `find`, `assert`, `swipe --on`) **retry the
|
|
203
|
+
lookup for up to 5s** instead of failing on the first miss — they re-capture the
|
|
204
|
+
hierarchy until it resolves. The screen is usually still settling after the prior
|
|
205
|
+
action, so this lets you act/verify directly without a `wait` in between, saving
|
|
206
|
+
round-trips and tokens.
|
|
207
|
+
|
|
208
|
+
- **Default:** 5s. `--wait <dur>` overrides it (`--wait 8s`, `--wait 800ms`, or a
|
|
209
|
+
bare ms count like `--wait 3000`).
|
|
210
|
+
- **`--no-wait` (or `--wait 0`):** fail immediately if the lenient lookup misses.
|
|
211
|
+
Use it for a pure existence probe where you want the answer *now*, e.g.
|
|
212
|
+
`vk find @spinner --no-wait` to check "is it there this instant".
|
|
213
|
+
- **Ambiguity is never waited on** — a present-but-plural match exits 2 right
|
|
214
|
+
away (the elements are already there); add `--index N` or refine the selector.
|
|
215
|
+
- **`vk assert <sel> --gone` waits for *disappearance*** — it polls until the
|
|
216
|
+
element is absent, so you don't need a separate `wait --gone`.
|
|
217
|
+
|
|
218
|
+
When you *do* want to block on a condition as an explicit step (e.g. a long
|
|
219
|
+
network wait beyond 5s), the `wait` command is still there with its own
|
|
220
|
+
`--timeout`/`--interval`; or just bump the inline window with `--wait`.
|
|
221
|
+
|
|
222
|
+
## Auto scroll-into-view
|
|
223
|
+
|
|
224
|
+
**You do not need to scroll before tapping.** `tap` and `text` bring their target
|
|
225
|
+
into the clear first — into its scroll container, and out from under anything
|
|
226
|
+
drawn over it (a sticky bottom bar, a floating button) — then act, reporting
|
|
227
|
+
`(scrolled into view: N swipes)`. So "scroll down to the card and tap it" is just
|
|
228
|
+
`vk tap @card`. Reach for an explicit `swipe` only when the scrolling itself is
|
|
229
|
+
the thing under test, or to make a lazy list build rows it has not built yet.
|
|
230
|
+
|
|
231
|
+
- `ui` / `find` / `assert` never scroll, and hide nothing: an element with no
|
|
232
|
+
pixel on screen is listed as usual with an `offscreen` marker.
|
|
233
|
+
- An element that cannot be reached is a **failure (exit 1)**, never a blind tap
|
|
234
|
+
on its coordinates. `--no-scroll` turns the scrolling off.
|
|
235
|
+
- Caveat worth knowing: a control covered by something the accessibility tree
|
|
236
|
+
does not contain is invisible to any tool reading that tree. verikun warns on
|
|
237
|
+
stderr when it presses an element it believes is covered — if a tap "succeeds"
|
|
238
|
+
and nothing happens, that warning is the first thing to look for.
|
|
239
|
+
|
|
240
|
+
## Be frugal: text over images, and remember identifiers
|
|
241
|
+
|
|
242
|
+
**Perceive with text, not pixels.** `vk ui` / `vk find` / `vk assert` return a
|
|
243
|
+
few hundred bytes; a screenshot read back as an image costs far more tokens.
|
|
244
|
+
Default to the textual hierarchy to see and verify state. Reach for `vk
|
|
245
|
+
screenshot` (then read the PNG) only when you genuinely need pixels — visual
|
|
246
|
+
layout, rendering/spacing bugs, or content with no text/id/desc. One image can
|
|
247
|
+
outweigh dozens of `vk ui` calls. When you do, `vk` already downscales the PNG
|
|
248
|
+
(700px longest edge) so the read stays cheap while text remains legible — add
|
|
249
|
+
`--more` if a screen is too coarse to read, or `--full` when you need exact detail.
|
|
250
|
+
|
|
251
|
+
**Two uses of a screenshot — keep them apart.** The cost above is about *reading a
|
|
252
|
+
screenshot back into context* to decide your next move; that is what to avoid (perceive
|
|
253
|
+
and verify with the hierarchy instead). A screenshot taken purely as **report evidence
|
|
254
|
+
and never read back** costs no tokens. So when you drive a flow to produce a report, **do**
|
|
255
|
+
`vk screenshot` around each significant transition (and before a risky or verification step)
|
|
256
|
+
— then leave it in the report, don't read the PNG back. A visual trail makes post-run review
|
|
257
|
+
far easier, and a failing step already auto-captures its own screen.
|
|
258
|
+
|
|
259
|
+
It is not free in wall clock, though: a capture is ~1.1s on a physical Android phone. That is
|
|
260
|
+
cheap next to a hierarchy read (~2.4s — see below) but it is not zero, so screenshot the
|
|
261
|
+
transitions worth reviewing rather than every step.
|
|
262
|
+
|
|
263
|
+
**Most of a run's time is reading the UI hierarchy.** Every selector command (`tap`, `text`,
|
|
264
|
+
`find`, `assert`, `swipe --on`) costs one read; measured on a physical mid-range Android
|
|
265
|
+
phone, one read is ~2.4s, nearly all of it fixed per-invocation cost inside `uiautomator`
|
|
266
|
+
that does not depend on how complex the screen is. iOS is ~10x cheaper. So prefer one
|
|
267
|
+
`vk assert` over a `vk ui` you have to scan, batch a known flow with `vk batch` rather than
|
|
268
|
+
re-checking between every step, and don't add a redundant `vk ui` just to confirm what an
|
|
269
|
+
`assert` already proved.
|
|
270
|
+
|
|
271
|
+
On Android this is handled for you: verikun keeps an accessibility connection alive on the
|
|
272
|
+
device (the *companion*), which cuts a read to ~0.2s. The first read on a device costs ~5.8s
|
|
273
|
+
to set it up, then every read after is fast. Nothing to enable.
|
|
274
|
+
|
|
275
|
+
It holds the device's single `UiAutomation` connection while it runs, so Appium and Layout
|
|
276
|
+
Inspector cannot attach. If the user needs those, tell them `VERIKUN_COMPANION=0` or
|
|
277
|
+
`vk companion stop` — don't disable it pre-emptively. A failure never breaks a run: verikun
|
|
278
|
+
falls back to the slower stock read on its own.
|
|
279
|
+
|
|
280
|
+
**Remember identifiers across runs.** After a flow succeeds, save the selectors
|
|
281
|
+
you found to memory — the mapping from human intent to selector, plus the screen
|
|
282
|
+
and step order, e.g.:
|
|
283
|
+
|
|
284
|
+
> Signup flow: "Get Started" → `@get_started`; intro slides → `@continue_btn`
|
|
285
|
+
> (tap ×2); plan picker → `text:"Free trial"`; account form → `@email_input`,
|
|
286
|
+
> then submit with `text:"Create account"`.
|
|
287
|
+
|
|
288
|
+
Next time a similar request comes in, **reuse the remembered selectors directly**
|
|
289
|
+
instead of re-inspecting from scratch — fewer round-trips, fewer tokens, faster
|
|
290
|
+
runs. Re-verify cheaply with `vk assert` / `vk find`; only fall back to a full
|
|
291
|
+
`vk ui` when a remembered selector stops resolving (the app changed — then update
|
|
292
|
+
the memory). Auto-healing selectors make remembered identifiers resilient to
|
|
293
|
+
small label/casing changes.
|
|
294
|
+
|
|
295
|
+
## Batch a known flow into one call
|
|
296
|
+
|
|
297
|
+
When you already know the steps (e.g. from a remembered flow), run them as a single
|
|
298
|
+
`vk batch` instead of one tool call per command — one process, far fewer
|
|
299
|
+
round-trips. Pipe newline-separated commands on **stdin**, or pass `--file <path>`:
|
|
300
|
+
|
|
301
|
+
```sh
|
|
302
|
+
vk batch <<'EOF'
|
|
303
|
+
launch com.example.app
|
|
304
|
+
text @email_input "user@example.com"
|
|
305
|
+
text @password_input "hunter2" --enter
|
|
306
|
+
assert text:"Welcome back" --wait 8s
|
|
307
|
+
run archive login-smoke
|
|
308
|
+
EOF
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
Each line runs **exactly as if called standalone** — same [auto-wait](#auto-wait),
|
|
312
|
+
same recording as a test-run step, same exit codes. The batch **streams each result
|
|
313
|
+
to stdout, then stops at the first non-zero exit and propagates that code**, so a
|
|
314
|
+
failed `tap`/`assert` halts the flow (its screenshot + hierarchy are captured in the
|
|
315
|
+
run, like any failed step). Blank lines and `#` comments are skipped, and the
|
|
316
|
+
`batch` call's `--device` / `--ios` / `--android` / `--json` apply to every line.
|
|
317
|
+
|
|
318
|
+
Reach for it once a flow is *known*; keep using single commands while you're still
|
|
319
|
+
**discovering** a screen (you need `vk ui` between steps anyway). If a batch halts,
|
|
320
|
+
read its stderr line (`batch stopped at line N (…)`) to see which command failed,
|
|
321
|
+
fix that selector, and re-run.
|
|
322
|
+
|
|
323
|
+
## Run a natural-language test (vk ai)
|
|
324
|
+
|
|
325
|
+
`vk ai <file>` is the inverse of driving the device yourself: instead of you
|
|
326
|
+
issuing `tap`/`text`/`assert` and inspecting between them, it hands a plain-English
|
|
327
|
+
test file to a model that **compiles it once into a deterministic plan**, then
|
|
328
|
+
replays that plan with **no model calls on the happy path**. The model is woken only
|
|
329
|
+
to *repair* a step whose selector stops resolving, and a green run caches the repaired
|
|
330
|
+
plan so the next run is free. Reach for it when the task is "run this whole English
|
|
331
|
+
test and give me a report"; keep using single commands while you're still
|
|
332
|
+
*discovering* a screen.
|
|
333
|
+
|
|
334
|
+
```sh
|
|
335
|
+
vk ai onboarding.md # compile (first run) or replay (cached)
|
|
336
|
+
vk ai onboarding.md --show-plan # print the compiled plan IR, do not run
|
|
337
|
+
vk ai onboarding.md --max-cost-usd 0.50 # tighten the spend cap (default $3)
|
|
338
|
+
vk ai onboarding.md --timeout 5m # tighten the run timeout (default 15m)
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
- Needs `ANTHROPIC_API_KEY` (Claude models) or `OPENAI_API_KEY` (OpenAI models) — **or no key
|
|
342
|
+
at all** with `--model codex-cli` / `--model cursor-cli`, which drive an already-logged-in
|
|
343
|
+
`codex` or `cursor-agent` CLI off your ChatGPT / Cursor subscription (run `codex login` or
|
|
344
|
+
`cursor-agent login` once; verikun just needs the binary on PATH). Default model
|
|
345
|
+
`claude-sonnet-4-6`; `--model` switches model **and** provider — Anthropic
|
|
346
|
+
(`claude-haiku-4-5` · `claude-sonnet-4-6` · `claude-opus-4-8` · `claude-fable-5`),
|
|
347
|
+
OpenAI (`gpt-5.4-mini` · `gpt-5.4` · `gpt-5.5` · `gpt-4.1` — cheaper than the default sonnet,
|
|
348
|
+
and non-reasoning, so `--effort` does not apply to it), or a CLI backend (`codex-cli` · `cursor-cli`).
|
|
349
|
+
- For the CLI backends, spend is on your subscription, not per token, so the cost line reads `$0`
|
|
350
|
+
and `--max-cost-usd` / `--cost-override` are no-ops (the run is still bounded by repairs +
|
|
351
|
+
`--timeout`). Each CLI chooses its own underlying model, and is run read-only in a scratch
|
|
352
|
+
directory so it never touches your working tree. Plans are provider-agnostic, so one compiled
|
|
353
|
+
by any model replays for free under a CLI backend; use `--recompile` to force a fresh compile
|
|
354
|
+
when comparing providers.
|
|
355
|
+
- The plan expresses **conditions** (`if-present`, for optional interstitials like a
|
|
356
|
+
permission dialog) and **bounded loops** (`repeat … until`, e.g. scroll-until) —
|
|
357
|
+
control flow `vk batch` cannot, so a flaky popup or a scroll-to-find no longer breaks
|
|
358
|
+
the flow. An `if-present` guard **waits for its selector to settle** (at least two looks
|
|
359
|
+
at the screen) before deciding the optional UI is absent, so a dialog that animates in a
|
|
360
|
+
beat after the transition is still caught. An absent guard costs about one extra UI dump;
|
|
361
|
+
`VERIKUN_GUARD_SETTLE_MS=0` restores the old single-shot probe.
|
|
362
|
+
- **Progress streams to stderr** (never silent in CI); **stdout is the report path**
|
|
363
|
+
(`--json` for a structured summary). It records like any flow, so it ends with the
|
|
364
|
+
same JUnit + HTML report — including the token/cost line and **suggested improvements**
|
|
365
|
+
you can fold back into the English to stabilize the test and cut future tokens.
|
|
366
|
+
- **Review screenshots are automatic.** The compiler sprinkles `screenshot` steps around
|
|
367
|
+
transitions (and inside loops) so the report shows a before/after visual trail for
|
|
368
|
+
post-run review — dumped, never read back by the model, so they add no token cost on
|
|
369
|
+
replay and never gate the test (a capture that hiccups is logged and skipped, not a
|
|
370
|
+
failure).
|
|
371
|
+
- **Bounded by default:** the run aborts if the estimated spend crosses **$3**
|
|
372
|
+
(`--max-cost-usd`) or the wall-clock passes **15m** (`--timeout`), so a runaway
|
|
373
|
+
compile/repair loop can't spend or hang without limit.
|
|
374
|
+
- Exit `0` pass · `1` a step failed (or the budget/timeout was hit) · `2` usage · `3` environment
|
|
375
|
+
(e.g. the model's API key — `ANTHROPIC_API_KEY` / `OPENAI_API_KEY` — unset, or the `codex` /
|
|
376
|
+
`cursor-agent` CLI missing / not logged in for `--model codex-cli` / `cursor-cli`).
|
|
377
|
+
- **`3` also means the device toolchain is broken**, checked *before* anything is compiled
|
|
378
|
+
(missing `adb`/`idb`, no device, an ambiguous target) and again if it breaks mid-run. Treat
|
|
379
|
+
it as "fix the machine", never as a failing test — the message carries the install hint.
|
|
380
|
+
|
|
381
|
+
## Run a suite of tests (vk suite)
|
|
382
|
+
|
|
383
|
+
When the task is "run all the tests" / "run the test directory", use
|
|
384
|
+
`vk suite <dir>` instead of looping `vk ai` yourself:
|
|
385
|
+
|
|
386
|
+
```sh
|
|
387
|
+
vk suite tests/ --app com.example.app # reset app data between tests
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
- Runs every `*.md` in the directory (lexicographic — `01-…`, `02-…` sequences
|
|
391
|
+
them; `README.md` is skipped) through the `vk ai` engine; all `ai` flags apply.
|
|
392
|
+
- `--app <id>` clears the app's data before each test (iOS: force-stop only).
|
|
393
|
+
Without it, each test must self-isolate (e.g. start with `launch <pkg> --clear`).
|
|
394
|
+
- A failing test doesn't stop the suite. stdout is the suite directory
|
|
395
|
+
(`./.verikun/suites/<id>/` with `index.json` + `index.html` linking every
|
|
396
|
+
test's report); **exit 1 if any test failed** — so it gates CI directly.
|
|
397
|
+
- **`--retries N`** (default `0`) re-runs a failed test up to N times. A later
|
|
398
|
+
pass recovers the suite (exit `0`) and surfaces a warning — failed-attempt
|
|
399
|
+
archives stay linked via `attempts` / suite `warnings`. An **environment break is
|
|
400
|
+
retried too** (a dropped `--server` connection, a device re-enumerating), with a
|
|
401
|
+
short backoff and a warning per blip; only a **budget abort** and a **usage error**
|
|
402
|
+
(exit `2`) are never retried, since a rerun cannot change either.
|
|
403
|
+
- **A broken *environment* does stop it: exit `3`.** If a test dies from an environment
|
|
404
|
+
error the toolchain is re-probed; only if it's still broken — and no retries remain —
|
|
405
|
+
does the suite abort (so a one-off flaky dump doesn't kill the run). The tests that
|
|
406
|
+
never ran are listed in `index.json`'s `aborted.notRun` and in the HTML banner — they
|
|
407
|
+
are **not** counted as failures. So `3` = fix the machine and rerun; `1` = a real
|
|
408
|
+
regression to investigate.
|
|
409
|
+
|
|
410
|
+
## Drive a remote device (--server)
|
|
411
|
+
|
|
412
|
+
If the device is attached to another machine running `vk server`, point
|
|
413
|
+
`vk ai` / `vk suite` / `vk install` at it — everything else works identically
|
|
414
|
+
(same reports, same exit codes; the server's device/platform apply):
|
|
415
|
+
|
|
416
|
+
```sh
|
|
417
|
+
export VERIKUN_SERVER=http://100.64.0.7:8391
|
|
418
|
+
export VERIKUN_SERVER_AUTH_KEY=<key printed/configured by the server>
|
|
419
|
+
vk install ./app-debug.apk --server "$VERIKUN_SERVER" # server needs --allow-install
|
|
420
|
+
vk suite tests/ --app com.example.app --server "$VERIKUN_SERVER"
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
A wrong URL/key fails fast with exit 3; `409` means another run holds the
|
|
424
|
+
device. To expose a device from THIS machine: `vk server --allow-install`
|
|
425
|
+
(add `--bind <addr>` to leave loopback; auth key auto-generates if unset).
|
|
426
|
+
|
|
427
|
+
## Test runs & reports
|
|
428
|
+
|
|
429
|
+
Every action is **recorded into a test run** — one auto-starts on your first
|
|
430
|
+
action, no setup needed. Each command becomes a step with its timing, the
|
|
431
|
+
selector + identifier it resolved through, and pass/fail. When a step fails, `vk`
|
|
432
|
+
automatically captures a screenshot **and** the UI hierarchy of that page.
|
|
433
|
+
`vk run archive` also captures a session-scoped device-log dump into
|
|
434
|
+
`artifacts/logcat.txt` by default, plus an app-scoped dump into
|
|
435
|
+
`artifacts/logcat-app.txt` (shown in the HTML accordion) when the run launched
|
|
436
|
+
an app. You can still run `vk log <package>` mid-run to attach a snapshot to a
|
|
437
|
+
step. Opt out on green runs with `--no-logs` / `VERIKUN_NO_LOGS` (failures still
|
|
438
|
+
capture).
|
|
439
|
+
|
|
440
|
+
- `vk run status` — the current run's steps and outcomes
|
|
441
|
+
- `vk run archive [name] [--no-logs]` — finish the run: writes a **JUnit XML** + a
|
|
442
|
+
self-contained **HTML report** (screenshots, captured hierarchies, device log,
|
|
443
|
+
and the identifiers used) to `./.verikun/runs/<id>/`, and exits non-zero if any
|
|
444
|
+
step failed — so it gates CI
|
|
445
|
+
- `vk run clear` — discard the run, no report
|
|
446
|
+
- `vk run start [name]` — begin a fresh named run explicitly (optional)
|
|
447
|
+
|
|
448
|
+
An implicit run **rolls over automatically** when the context changes — a
|
|
449
|
+
different device, a different `VERIKUN_SESSION`, or 30 min idle
|
|
450
|
+
(`VERIKUN_RUN_IDLE_MIN`, 0 disables): the stale run is archived and a fresh one
|
|
451
|
+
starts, so unrelated sessions never merge into one report. A run you named with
|
|
452
|
+
`vk run start` is sticky to idle (only a device/session change rolls it over).
|
|
453
|
+
|
|
454
|
+
When the task is "run/verify flow X and give me a report", just drive the flow
|
|
455
|
+
and end with `vk run archive` — the report *is* the deliverable. **Drop a
|
|
456
|
+
`vk screenshot` around each significant transition** (before/after a navigation tap,
|
|
457
|
+
a submit, a screen change) so the report carries a visual trail for post-run review —
|
|
458
|
+
these are write-only evidence, so don't read the PNGs back (see [Be frugal](#be-frugal-text-over-images-and-remember-identifiers)).
|
|
459
|
+
A failing step already auto-captures its own screen + hierarchy on top of that. The
|
|
460
|
+
archived `run.json` records which selector resolved each step, so it doubles as the
|
|
461
|
+
identifier memory described above. Set `VERIKUN_NO_RUN=1` to disable recording.
|
|
462
|
+
|
|
463
|
+
## Improve verikun (report friction upstream)
|
|
464
|
+
|
|
465
|
+
When **verikun itself** — not the app, not your selector — is the friction (a model heal on
|
|
466
|
+
a *cached* replay: `[ai] plan cache hit` + a repair, or `"cached": true` with
|
|
467
|
+
`modelRepairs > 0` in `--json`; a `drifted, not repaired` give-up; or a recurring gotcha in
|
|
468
|
+
vk's own operation), use the **`suggest-verikun-improvement`** skill. It drafts a light,
|
|
469
|
+
TL;DR-first suggestion — **redacted** of every app-under-test specific (package, on-screen
|
|
470
|
+
text, selector values, test prose, logs) — for you to **review before it's submitted** to
|
|
471
|
+
`ddikman/verikun`. Don't hand-roll the issue: that skill owns the redaction and the
|
|
472
|
+
draft-first flow.
|
|
473
|
+
|
|
474
|
+
## Exit codes — rely on these for control flow
|
|
475
|
+
|
|
476
|
+
- `0` success / found / assertion passed
|
|
477
|
+
- `1` not found / assertion failed / wait timeout
|
|
478
|
+
- `2` usage error **or ambiguous selector** (refine it or add `--index N`)
|
|
479
|
+
- `3` environment error (no device, adb/idb missing, hierarchy dump failed) — for
|
|
480
|
+
`ai`/`suite`/`install`/`server` the toolchain is verified up front, so this arrives
|
|
481
|
+
immediately with an install hint rather than mid-flow
|
|
482
|
+
|
|
483
|
+
## Gotchas
|
|
484
|
+
|
|
485
|
+
- **Disable animations once** for reliable dumps: `vk doctor --fix`. Live
|
|
486
|
+
animations can make `vk ui` flaky (it already retries 3×).
|
|
487
|
+
- **Ambiguous selector → exit 2**, never a random tap. `vk` prints the candidate
|
|
488
|
+
matches; add `--index N` or use a more specific selector.
|
|
489
|
+
- **Indexes are per-snapshot.** `vk tap 3` taps `[3]` from the *latest* dump;
|
|
490
|
+
prefer `@id` / `text:` selectors for stability across screens.
|
|
491
|
+
- **Text starting with `-`:** put `--` first → `vk type -- "-50% off"`.
|
|
492
|
+
- **`vk device set` from a plain shell stays applied.** Inside `batch`/`ai`/`suite` it is
|
|
493
|
+
restored automatically even if the flow dies, but a one-off `vk device set airplane=on`
|
|
494
|
+
is yours to `vk device reset` — don't strand someone's phone offline.
|
|
495
|
+
- **One device auto-resolves.** Multiple → pass `-d <serial>` or set `VERIKUN_DEVICE`.
|
|
496
|
+
- **`vk text` opens the keyboard.** Use `--enter` to submit, or `vk back` to
|
|
497
|
+
dismiss it before re-inspecting (it can cover elements).
|
|
498
|
+
- **Unicode/emoji** may not type via `adb input text` (an Android limitation);
|
|
499
|
+
ASCII is reliable.
|
|
500
|
+
- **`vk log` is a snapshot, not a stream** — it dumps recent lines and exits.
|
|
501
|
+
Scoping with a `package` filters to that app's live process; once the app has
|
|
502
|
+
**crashed** its process is gone, so `vk log <pkg>` falls back to system-wide
|
|
503
|
+
logs (where the crash trace still is). The logs are **raw device output** and
|
|
504
|
+
can contain anything the app logged — including secrets — so treat archived
|
|
505
|
+
reports accordingly (`VERIKUN_NO_RUN=1` disables recording).
|
|
506
|
+
- **Special characters type fine.** Emails and symbols (`@ . + _ - / = : , ; ! # % & …`)
|
|
507
|
+
go in verbatim — `vk` backslash-escapes every device-shell metacharacter before
|
|
508
|
+
`adb input text`, so `vk text @email "bob+tag@mail.com"` lands the whole address,
|
|
509
|
+
not just `bob`. **Quote the value** when you build the command in a shell (or feed
|
|
510
|
+
it via `vk batch`/stdin, which uses no host shell) so your *own* shell can't split
|
|
511
|
+
or drop the `@`/`#`/`&` before `vk` sees it.
|
|
512
|
+
- **iOS** (`--ios`): full parity with Android — `ui`/`find`, `tap`, `text`/`type`,
|
|
513
|
+
`swipe`, `key`, `assert`, `screenshot`, `launch`/`stop` — on simulators and
|
|
514
|
+
physical devices. Interaction + hierarchy come from `idb` (`brew install
|
|
515
|
+
idb-companion` + `pip install fb-idb`); simulator screenshots/launch/stop/logs
|
|
516
|
+
use `xcrun simctl`. Run `vk doctor --ios` to check the toolchain. Caveats: `clear`
|
|
517
|
+
is unsupported (no per-app reset), `current` is `(unknown)`, device logs are
|
|
518
|
+
simulator-only, and `device set` is partial — `dark`/`font-scale` work on a simulator
|
|
519
|
+
while `airplane`/`rotation` do not exist there at all (`vk device caps --ios`). iOS
|
|
520
|
+
accessibility ids are often unset, so prefer `text:`/`desc:` selectors there.
|
|
521
|
+
|
|
522
|
+
## Worked example — verify a login flow
|
|
523
|
+
|
|
524
|
+
```sh
|
|
525
|
+
vk doctor --fix # deterministic UI
|
|
526
|
+
vk launch com.example.app
|
|
527
|
+
vk text @email_input "user@example.com" # field lookup auto-waits up to 5s
|
|
528
|
+
vk text @password_input "hunter2" --enter
|
|
529
|
+
vk assert text:"Welcome back" --wait 8s # poll up to 8s, then assert → exit 0 = logged in
|
|
530
|
+
vk assert @error_banner --gone # exit 0 → no error banner shown
|
|
531
|
+
vk run archive login-smoke # -> ./.verikun/runs/<id>/report.html (+ report.xml)
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
Note there's no explicit `wait @email_input` — `text` auto-waits for the field.
|
|
535
|
+
Check `$?` after `assert`/`wait`/`find` to branch on success vs failure.
|