verikun 0.18.0 → 0.19.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/README.md +73 -1
- package/dist/agent/grammar.js +14 -0
- package/dist/agent/ir.js +52 -3
- package/dist/args.js +1 -0
- package/dist/cli.js +325 -33
- package/dist/device/settings.js +252 -0
- package/dist/drivers/adb.js +177 -0
- package/dist/drivers/ios.js +87 -0
- package/dist/exec.js +12 -0
- package/dist/report.js +68 -3
- package/dist/run.js +141 -7
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -95,6 +95,16 @@ vk screenshot # -> ./.verikun/screen.png
|
|
|
95
95
|
| `clear <app>` | Wipe the app's locally stored data — login/session, preferences, caches — resetting it to a just-installed state (Android `pm clear`, which also force-stops the app). iOS unsupported: there is no per-app data reset. |
|
|
96
96
|
| `install <app.apk\|.ipa> [--server url]` | Install a build on the device (`adb install -r` / `idb install`). With `--server`, the file is uploaded to a remote [`vk server`](#remote-devices--vk-server) started with `--allow-install` (single-file `.apk`/`.ipa`, sha256-verified). |
|
|
97
97
|
|
|
98
|
+
### Device state
|
|
99
|
+
| Command | Description |
|
|
100
|
+
|---|---|
|
|
101
|
+
| `device set <key>=<value> …` | Change the *device* the app runs on, snapshotting each original first. Keys: `airplane`, `dark`, `font-scale`, `rotation`, `stay-awake`. Every change is **verified by reading it back** — `svc`/`cmd`/`settings put` are fire-and-forget and silently no-op on some OEM skins, so trusting the exit code would report success for a change that never happened. Refuses `airplane=on` over a wireless adb link (it would cut the connection carrying the next command); `--allow-wireless` overrides. |
|
|
102
|
+
| `device get [key] [--json]` | Current values; `n/a` where the platform can't answer. |
|
|
103
|
+
| `device reset [key …]` | Restore what this run changed. `batch`, `ai` and `suite` also do this automatically when the flow ends **or fails**, so a dead test can't leave your phone offline or rotated. |
|
|
104
|
+
| `device caps [--json]` | What the active platform supports, and the manual equivalent where it doesn't. |
|
|
105
|
+
|
|
106
|
+
See [Device state](#device-state-1) for the per-platform matrix.
|
|
107
|
+
|
|
98
108
|
### Batch
|
|
99
109
|
| Command | Description |
|
|
100
110
|
|---|---|
|
|
@@ -144,11 +154,17 @@ skips the archive dump on green runs (failures still capture).
|
|
|
144
154
|
- **`report.html`** — a self-contained report: every step, the identifiers used,
|
|
145
155
|
any screenshots taken, the screenshot + hierarchy of any failed page, a link to
|
|
146
156
|
the full device log, an app-scoped log accordion when the run launched an app,
|
|
147
|
-
and any per-step logs from `vk log`.
|
|
157
|
+
and any per-step logs from `vk log`. A run that did not pass says so in a banner
|
|
158
|
+
at the top.
|
|
148
159
|
- **`artifacts/logcat.txt`** — full device log for the run window (default).
|
|
149
160
|
- **`artifacts/logcat-app.txt`** — app-scoped log (when a package/bundle was launched).
|
|
150
161
|
- **`run.json`** — the raw recording.
|
|
151
162
|
|
|
163
|
+
A `vk ai` run can also fail where no single command did — a `repeat` that never
|
|
164
|
+
sees its target, a cost/timeout abort. That verdict is recorded too (as
|
|
165
|
+
`run.json`'s `failure`, plus a failed step carrying the reason and a screenshot),
|
|
166
|
+
so a failed test is never reported as `failures="0"` in the JUnit that CI reads.
|
|
167
|
+
|
|
152
168
|
`vk run archive` exits non-zero when the run contained failures, so the same
|
|
153
169
|
command both produces the report and gates CI.
|
|
154
170
|
|
|
@@ -602,6 +618,58 @@ at runtime, so driving a flow to a report should capture liberally around transi
|
|
|
602
618
|
`vk ai` does this automatically (see [AI](#ai--natural-language-tests)); when driving by
|
|
603
619
|
hand, `vk screenshot` around each screen change and leave the PNG in the report.
|
|
604
620
|
|
|
621
|
+
## Device state
|
|
622
|
+
|
|
623
|
+
Some behaviour only appears when the *device* changes underneath the app: the offline
|
|
624
|
+
banner, the retry path, dark theme, a layout that breaks at accessibility text sizes,
|
|
625
|
+
landscape. `vk device set` changes those, verifies each one landed, and — this is the part
|
|
626
|
+
that makes it safe to point at your own phone — **puts them back**.
|
|
627
|
+
|
|
628
|
+
```sh
|
|
629
|
+
vk device set airplane=on # go offline
|
|
630
|
+
vk tap @retry
|
|
631
|
+
vk assert text:"No connection"
|
|
632
|
+
vk device reset # back online
|
|
633
|
+
|
|
634
|
+
vk device set dark=on font-scale=1.3 rotation=landscape # several at once
|
|
635
|
+
vk device get --json
|
|
636
|
+
vk device caps # what this platform supports
|
|
637
|
+
```
|
|
638
|
+
|
|
639
|
+
| key | values | Android | iOS simulator | iOS device |
|
|
640
|
+
|---|---|---|---|---|
|
|
641
|
+
| `airplane` | `on\|off` | ✅ | ❌ no radio to switch off | ❌ |
|
|
642
|
+
| `dark` | `on\|off` | ✅ | ✅ | ❌ |
|
|
643
|
+
| `font-scale` | `0.5`–`3.0`, or `default` | ✅ | ✅ mapped to the nearest Dynamic Type category | ❌ |
|
|
644
|
+
| `rotation` | `portrait\|landscape\|portrait-reverse\|landscape-reverse\|auto` | ✅ | ❌ neither simctl nor idb rotates | ❌ |
|
|
645
|
+
| `stay-awake` | `on\|off` | ✅ | ⊘ no-op — simulators don't sleep | ❌ |
|
|
646
|
+
|
|
647
|
+
An unsupported key exits **3 before any device I/O**, naming the manual equivalent, so a
|
|
648
|
+
test asking for something the platform can't do fails on the first step rather than
|
|
649
|
+
half-way through a half-modified device. `vk device caps --json` is the same matrix for
|
|
650
|
+
whatever platform you're pointed at.
|
|
651
|
+
|
|
652
|
+
**Restore is the point.** `device set` records what each setting held *before* it changed
|
|
653
|
+
it, in the run file rather than in memory — so `vk device reset` works from a later process
|
|
654
|
+
too. `batch`, `ai` and `suite` restore automatically in a `finally`, which is what stops a
|
|
655
|
+
test that dies between `airplane=on` and `airplane=off` from leaving your phone offline.
|
|
656
|
+
A bare `vk device set` from your shell stays applied until you reset it, deliberately.
|
|
657
|
+
|
|
658
|
+
Three things worth knowing:
|
|
659
|
+
|
|
660
|
+
- **`airplane=on` is verified by effect, not by the flag.** Android remembers a user who
|
|
661
|
+
re-enabled wifi during a previous flight, so `airplane-mode enable` can leave wifi *up*.
|
|
662
|
+
`vk` reads the device's own `airplane_mode_toggleable_radios` — the radios that can
|
|
663
|
+
survive the flag — confirms each actually went down, and forces any survivor. Reporting
|
|
664
|
+
"offline" while the app is still online would make an offline test pass for the wrong
|
|
665
|
+
reason. (Cellular is not in that list, and `mobile_data` is a stored preference rather
|
|
666
|
+
than live radio state, so it is deliberately not probed.)
|
|
667
|
+
- **`airplane=off` re-enables the radio, not the internet.** Follow it with
|
|
668
|
+
`vk assert <selector> --wait 10s` rather than tapping straight away.
|
|
669
|
+
- **Wireless adb is refused for `airplane=on`.** It would cut the very link carrying the
|
|
670
|
+
next command, and nothing could turn it back on remotely. Exit 2; `--allow-wireless` if
|
|
671
|
+
you mean it.
|
|
672
|
+
|
|
605
673
|
## How it works
|
|
606
674
|
|
|
607
675
|
```
|
|
@@ -652,6 +720,10 @@ your setup with `vk doctor --ios`. `vk --ios tap`, `vk --ios ui`, etc. then work
|
|
|
652
720
|
- `log` capture is simulator-only (via `log show`); for a physical device use
|
|
653
721
|
Console.app or `idb log` directly.
|
|
654
722
|
- `--tree` renders flat — idb's accessibility list has no nesting depth.
|
|
723
|
+
- `device set` is partial: `dark` and `font-scale` work on a **simulator** (`simctl ui`),
|
|
724
|
+
`stay-awake` is a no-op (simulators don't sleep), and `airplane`/`rotation` are
|
|
725
|
+
unsupported — neither `simctl ui` nor `idb ui` exposes a radio or an orientation.
|
|
726
|
+
A physical device supports none of them. Run `vk device caps --ios` for the live matrix.
|
|
655
727
|
|
|
656
728
|
## Using it from an AI agent
|
|
657
729
|
|
package/dist/agent/grammar.js
CHANGED
|
@@ -28,6 +28,20 @@ Each step is one of three node types:
|
|
|
28
28
|
assert <selector> [--text <s>] [--gone] — assert presence/text/absence (FAILS the test if false)
|
|
29
29
|
wait <selector> [--gone] [--timeout <ms>] — block until present/absent
|
|
30
30
|
screenshot — capture the screen into the report
|
|
31
|
+
device <set|get|reset|caps> — change the DEVICE (not the app) to test how the app
|
|
32
|
+
copes. The command name is EXACTLY "device"; the
|
|
33
|
+
subcommand is the FIRST POSITIONAL, never part of the
|
|
34
|
+
command name. Each assignment is one positional:
|
|
35
|
+
{"command":"device","positionals":["set","dark=on","font-scale=1.3"]}
|
|
36
|
+
{"command":"device","positionals":["reset"]}
|
|
37
|
+
Keys: airplane=on|off (go offline — for retry/error
|
|
38
|
+
handling), dark=on|off, font-scale=<0.5-3.0>,
|
|
39
|
+
rotation=portrait|landscape|portrait-reverse|
|
|
40
|
+
landscape-reverse|auto, stay-awake=on|off.
|
|
41
|
+
ALWAYS finish the scenario with a "reset" step. Do NOT
|
|
42
|
+
tap immediately after airplane=off — the radio is back
|
|
43
|
+
but the network is not; follow it with a wait/assert.
|
|
44
|
+
Android only for airplane + rotation.
|
|
31
45
|
|
|
32
46
|
2. IF-PRESENT — { "type":"if-present", "selector":<sel>, "body":[<command leaves>] }
|
|
33
47
|
Run body ONLY if the selector is on screen now. Use for OPTIONAL interstitials:
|
package/dist/agent/ir.js
CHANGED
|
@@ -23,6 +23,8 @@ exports.bodiesOf = bodiesOf;
|
|
|
23
23
|
exports.validateNode = validateNode;
|
|
24
24
|
exports.parsePlan = parsePlan;
|
|
25
25
|
exports.leafToFlags = leafToFlags;
|
|
26
|
+
const errors_1 = require("../errors");
|
|
27
|
+
const settings_1 = require("../device/settings");
|
|
26
28
|
/** Max control-node nesting. 1 = a control node at top level whose body is leaves
|
|
27
29
|
* (the v1 rule). 2 = a control node inside that body, whose own body is leaves —
|
|
28
30
|
* i.e. `repeat { when { … } }`, the loop-that-branches shape a real dynamic flow
|
|
@@ -50,6 +52,7 @@ exports.KNOWN_COMMANDS = new Set([
|
|
|
50
52
|
'screenshot', 'shot',
|
|
51
53
|
'wait', 'assert',
|
|
52
54
|
'launch', 'open', 'stop', 'clear',
|
|
55
|
+
'device',
|
|
53
56
|
// Inspection/diagnostic commands (`current`, `ui`, `find`, `log`, `logs`) are
|
|
54
57
|
// deliberately NOT here: they are not test actions (the grammar never offers them), so
|
|
55
58
|
// a plan or repair must never emit them — and `log`'s flags reach a device shell
|
|
@@ -253,6 +256,47 @@ const READ_FIELDS = new Set(['text', 'desc', 'id', 'idShort']);
|
|
|
253
256
|
/** ctx keys are interpolated into selectors as {{ctx.NAME}}; keep them boring so a
|
|
254
257
|
* name can never smuggle regex/template metacharacters into a selector. */
|
|
255
258
|
const CTX_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
259
|
+
/**
|
|
260
|
+
* Fold `{command: "device set"}` back into `{command: "device", positionals: ["set", …]}`.
|
|
261
|
+
*
|
|
262
|
+
* `device` is the only verb in the grammar with a subcommand, and every other one is a
|
|
263
|
+
* single word — so the model reliably writes the pair as one command name. That is an
|
|
264
|
+
* unambiguous spelling of a REAL command, not a hallucination, so normalizing it is the
|
|
265
|
+
* same kind of leniency as the `click`/`tap` aliases rather than a hole in the
|
|
266
|
+
* allowlist: an unknown subcommand still falls through and is rejected below.
|
|
267
|
+
*/
|
|
268
|
+
function normalizeLeaf(command, positionals) {
|
|
269
|
+
const m = /^device[\s:_-]+(set|get|reset|caps)$/i.exec(command.trim());
|
|
270
|
+
return m ? { command: 'device', positionals: [m[1].toLowerCase(), ...positionals] } : { command, positionals };
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Check a `device` leaf's subcommand and assignments at PLAN-VALIDATION time.
|
|
274
|
+
*
|
|
275
|
+
* Every other command is validated only by name, because a bad selector is a runtime
|
|
276
|
+
* fact the engine can heal. A device setting is different: whether it exists, and
|
|
277
|
+
* whether the value is legal, are known statically — so catching it here means a
|
|
278
|
+
* suite that asks for an unknown key fails before the first tap instead of twenty
|
|
279
|
+
* steps in, on a device it has already half-modified.
|
|
280
|
+
*/
|
|
281
|
+
function validateDeviceStep(positionals, where) {
|
|
282
|
+
const sub = (positionals[0] ?? '').toLowerCase();
|
|
283
|
+
if (!['set', 'get', 'reset', 'caps'].includes(sub)) {
|
|
284
|
+
throw new InvalidPlanError(`${where}: device subcommand must be set|get|reset|caps, got ${JSON.stringify(sub)}`);
|
|
285
|
+
}
|
|
286
|
+
const rest = positionals.slice(1);
|
|
287
|
+
try {
|
|
288
|
+
if (sub === 'set')
|
|
289
|
+
(0, settings_1.parseDeviceAssignments)(rest);
|
|
290
|
+
else
|
|
291
|
+
for (const k of rest) {
|
|
292
|
+
if (!(0, settings_1.isSettingKey)(k.trim().toLowerCase()))
|
|
293
|
+
throw new errors_1.CliError(`unknown device setting '${k}'`, 2);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
catch (e) {
|
|
297
|
+
throw new InvalidPlanError(`${where}: ${e instanceof Error ? e.message.split('\n')[0] : String(e)}`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
256
300
|
/** Validate a single node (used for both compile output and a spliced repair).
|
|
257
301
|
*
|
|
258
302
|
* `depth` is how many control nodes enclose this one. It is the ONLY thing bounding
|
|
@@ -294,15 +338,20 @@ function validateNode(node, where, depth = 0) {
|
|
|
294
338
|
const optional = (v, pick) => v === undefined || v === null ? undefined : pick(v);
|
|
295
339
|
switch (n.type) {
|
|
296
340
|
case 'command': {
|
|
297
|
-
if (typeof n.command !== 'string'
|
|
341
|
+
if (typeof n.command !== 'string')
|
|
298
342
|
throw new InvalidPlanError(`${where}: unknown command ${JSON.stringify(n.command)}`);
|
|
299
|
-
}
|
|
300
343
|
if (!Array.isArray(n.positionals) || !n.positionals.every((p) => typeof p === 'string')) {
|
|
301
344
|
throw new InvalidPlanError(`${where}: positionals must be a string[]`);
|
|
302
345
|
}
|
|
346
|
+
const { command, positionals } = normalizeLeaf(n.command, n.positionals);
|
|
347
|
+
if (!exports.KNOWN_COMMANDS.has(command)) {
|
|
348
|
+
throw new InvalidPlanError(`${where}: unknown command ${JSON.stringify(n.command)}`);
|
|
349
|
+
}
|
|
303
350
|
if (!isFlagSpecArray(n.flags))
|
|
304
351
|
throw new InvalidPlanError(`${where}: flags must be {name,value}[]`);
|
|
305
|
-
|
|
352
|
+
if (command === 'device')
|
|
353
|
+
validateDeviceStep(positionals, where);
|
|
354
|
+
return { type: 'command', command, positionals, flags: n.flags };
|
|
306
355
|
}
|
|
307
356
|
case 'read': {
|
|
308
357
|
const selector = selectorOf();
|