verikun 0.24.0 → 0.25.1
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 +19 -1
- package/CHANGELOG.md +31 -0
- package/README.md +2 -2
- package/dist/agent/remote.js +16 -3
- package/dist/args.js +6 -4
- package/dist/cli.js +112 -49
- package/dist/device/claims.js +0 -6
- package/dist/device/failover.js +230 -0
- package/dist/device/prep.js +49 -11
- package/dist/drivers/adb.js +56 -32
- package/dist/server.js +341 -15
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -141,7 +141,10 @@ banner, the retry path, dark theme, a layout that breaks at accessibility text s
|
|
|
141
141
|
you reset it, so don't leave someone's phone in airplane mode.
|
|
142
142
|
|
|
143
143
|
- `vk device prep [--dry-run] [--revert]` — set a **test** device up once, stickily:
|
|
144
|
-
`animations=off stay-awake=
|
|
144
|
+
`animations=off stay-awake=off screen-timeout=1m dnd=on doze=off`. The display then sleeps by
|
|
145
|
+
itself a minute after the last command and is woken by the next one; `--no-sleep-when-idle`
|
|
146
|
+
keeps it lit for good (`stay-awake=on screen-timeout=max`), which is what a device with a
|
|
147
|
+
PIN/pattern lock needs, since verikun can only clear a *swipe* lock.
|
|
145
148
|
|
|
146
149
|
Unlike `device set`, prep **survives the run** and is undone only by `--revert`. A physical
|
|
147
150
|
device must be named (`--device <serial>`) — that requirement is deliberate, so prep can
|
|
@@ -452,6 +455,19 @@ device; `503` means the server has no device attached — boot one (below). To
|
|
|
452
455
|
expose a device from THIS machine: `vk server --allow-install`
|
|
453
456
|
(add `--bind <addr>` to leave loopback; auth key auto-generates if unset).
|
|
454
457
|
|
|
458
|
+
**If you see `[verikun] server moved device: A → B` on stderr**, the server left a
|
|
459
|
+
device that failed and is now on another one. What that means depends on the line:
|
|
460
|
+
|
|
461
|
+
- `— retried there` (installs only): the build DID land, on **B**. Anything you go on
|
|
462
|
+
to do with an explicit serial must name B, not A.
|
|
463
|
+
- `— this step failed on the old device; the next runs on the new one`: your step
|
|
464
|
+
failed on **A**. Do NOT re-run it expecting a different answer for the same reason —
|
|
465
|
+
the failure was real on A, and B has none of the state your flow built up. Start the
|
|
466
|
+
flow again from the top if you want it on B.
|
|
467
|
+
|
|
468
|
+
The server rules the bad device out until it is power-cycled;
|
|
469
|
+
`vk devices --server <url>` shows why in its `NOTE` column.
|
|
470
|
+
|
|
455
471
|
## The device is missing or wedged
|
|
456
472
|
|
|
457
473
|
```sh
|
|
@@ -536,6 +552,8 @@ owns the redaction and the review-first flow.
|
|
|
536
552
|
verikun detects this, wakes the device and clears a *swipe* lock automatically; on a
|
|
537
553
|
PIN/pattern/password it exits **3** naming the lock rather than returning that dump.
|
|
538
554
|
Tell the user to remove the lock in Settings > Security — verikun never asks for a PIN.
|
|
555
|
+
Taps, swipes, typing and screenshots wake it the same way: a prepped display sleeps after a
|
|
556
|
+
minute idle, and an injected tap on a sleeping screen would otherwise do nothing and exit `0`.
|
|
539
557
|
- **Ambiguous selector → exit 2**, never a random tap. `vk` prints the candidate
|
|
540
558
|
matches; add `--index N` or use a more specific selector.
|
|
541
559
|
- **Indexes are per-snapshot.** `vk tap 3` taps `[3]` from the *latest* dump;
|
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,37 @@ All notable changes to this project are documented here. The format is based on
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.25.1] - 2026-08-21
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
- **`vk device prep`**: gives the display a 1-minute timeout instead of keeping it lit forever; `--no-sleep-when-idle` keeps the old behaviour. ([#101])
|
|
13
|
+
- **`vk batch|ai|suite`**: no longer switch a prepped device's display off at teardown — it now sleeps by itself.
|
|
14
|
+
- **`vk tap|type|key|swipe|screenshot`**: wake a sleeping display first; an injected tap on a dozing screen did nothing and exited `0`.
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
- **`vk device prep`**: warns when a device that will now sleep is behind a PIN/pattern lock.
|
|
18
|
+
|
|
19
|
+
[#101]: https://github.com/ddikman/verikun/issues/101
|
|
20
|
+
|
|
21
|
+
## [0.25.0] - 2026-08-21
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
- **`vk server`**: moves to another attached healthy device when the bound one fails; only a pinned `--device` disables it. ([#99])
|
|
25
|
+
- **`vk server --allow-failover[=serials]`**: re-enable failover on a pinned server, and bound where it may go.
|
|
26
|
+
- **`vk server --no-failover`** / **`VERIKUN_NO_FAILOVER`**: disable failover outright.
|
|
27
|
+
- **`/v1/health`**: adds `failoverEnabled` and `quarantined`, so a client can see which devices the server ruled out.
|
|
28
|
+
- **`vk install --server`**: reports the device the build landed on when it differs from the one bound.
|
|
29
|
+
|
|
30
|
+
### Changed
|
|
31
|
+
- **`vk install --server`**: a device-attributable install failure retries on another healthy device.
|
|
32
|
+
- **`/v1/exec`**, **`/v1/elements`**: report `deviceChanged` when the server moved device; the failing step is never replayed.
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
- **`/v1/logs`**: served the startup device's logs after a rebind instead of the bound device's.
|
|
36
|
+
- **`/v1/health`**: reported the startup device's read path after a rebind.
|
|
37
|
+
|
|
38
|
+
[#99]: https://github.com/ddikman/verikun/issues/99
|
|
39
|
+
|
|
9
40
|
## [0.24.0] - 2026-08-20
|
|
10
41
|
|
|
11
42
|
### Added
|
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
- **Puppeteer for native mobile** — a thin wrapper over native Android and iOS automation runners with zero runtime dependencies.
|
|
9
9
|
- **Natural-language tests** — `vk ai <file>`: runs plain-English tests, compiled once and replayed model-free (~$0), calling a model only to self-heal a drifted step. [What that costs](https://ddikman.github.io/verikun/reference/cost/), and how the `--max-cost-usd` ceiling bounds it.
|
|
10
10
|
- **Self-improving** — the agent runner will provide prescriptive improvements to existing scripts to help stabilise flakiness for future runs.
|
|
11
|
-
- **CI-ready** — `vk suite` runs a folder of tests as one gated pass/fail run; `vk server` exposes a real device over an authenticated tunnel so a disposable CI runner (no phone attached) can still drive it.
|
|
11
|
+
- **CI-ready** — `vk suite` runs a folder of tests as one gated pass/fail run; `vk server` exposes a real device over an authenticated tunnel so a disposable CI runner (no phone attached) can still drive it, and moves to another attached device if that one goes bad.
|
|
12
12
|
|
|
13
13
|
```
|
|
14
14
|
$ vk ui
|
|
@@ -38,7 +38,7 @@ The package also carries the agent [`SKILL.md`](.claude/skills/verikun/SKILL.md)
|
|
|
38
38
|
|
|
39
39
|
```sh
|
|
40
40
|
vk doctor # check adb/device (read-only — never changes anything)
|
|
41
|
-
vk device prep --device <id> # set a TEST device up once: animations off,
|
|
41
|
+
vk device prep --device <id> # set a TEST device up once: animations off, sane display timeout
|
|
42
42
|
vk devices # list attached devices
|
|
43
43
|
vk ui # semantic snapshot of the current screen
|
|
44
44
|
vk tap @login_button # tap by resource-id
|
package/dist/agent/remote.js
CHANGED
|
@@ -89,8 +89,14 @@ class RemoteTransport {
|
|
|
89
89
|
finally {
|
|
90
90
|
clearTimeout(timer);
|
|
91
91
|
}
|
|
92
|
-
if (!res.ok)
|
|
93
|
-
|
|
92
|
+
if (!res.ok) {
|
|
93
|
+
const body = await readBody(res);
|
|
94
|
+
// Before throwing: a failing request may still have moved the device, and that is
|
|
95
|
+
// exactly the case a caller must not miss (an exhausted install, a dead-device read).
|
|
96
|
+
if (body?.deviceChanged)
|
|
97
|
+
this.opts.onDeviceChange?.(body.deviceChanged);
|
|
98
|
+
throw describeStatus(res.status, body, url);
|
|
99
|
+
}
|
|
94
100
|
const parsed = await readBody(res);
|
|
95
101
|
if (parsed === null)
|
|
96
102
|
throw new errors_1.CliError(`verikun server at ${url} returned a non-JSON response`, 3);
|
|
@@ -157,6 +163,9 @@ function createRemoteBackend(opts, health) {
|
|
|
157
163
|
const res = await t.postJson('/v1/exec', req, EXEC_TIMEOUT_MS);
|
|
158
164
|
if (record && res.step)
|
|
159
165
|
opts.onStep?.(res.step, decodeArtifacts(res.artifacts), res.logStart);
|
|
166
|
+
// A failing step is a 200, so this is the ordinary path for a mid-run device death.
|
|
167
|
+
if (res.deviceChanged)
|
|
168
|
+
opts.onDeviceChange?.(res.deviceChanged);
|
|
160
169
|
return { code: res.code, error: res.error ? (0, rpc_1.rebuildError)(res.error) : undefined };
|
|
161
170
|
};
|
|
162
171
|
return {
|
|
@@ -184,11 +193,15 @@ function createRemoteBackend(opts, health) {
|
|
|
184
193
|
throw new errors_1.CliError(`install: cannot read '${appPath}' (${e.message})`, 2);
|
|
185
194
|
}
|
|
186
195
|
const sha256 = (0, node_crypto_1.createHash)('sha256').update(buf).digest('hex');
|
|
187
|
-
await t.request('POST', '/v1/install', buf, INSTALL_TIMEOUT_MS, {
|
|
196
|
+
const res = await t.request('POST', '/v1/install', buf, INSTALL_TIMEOUT_MS, {
|
|
188
197
|
'content-type': 'application/octet-stream',
|
|
189
198
|
'x-verikun-ext': ext,
|
|
190
199
|
'x-verikun-sha256': sha256,
|
|
191
200
|
});
|
|
201
|
+
// Install is the one operation the server replays elsewhere, so a move here means
|
|
202
|
+
// the build DID land — on a different device than the one we started with.
|
|
203
|
+
if (res.deviceChanged)
|
|
204
|
+
opts.onDeviceChange?.(res.deviceChanged);
|
|
192
205
|
},
|
|
193
206
|
async reset(appId) {
|
|
194
207
|
// Between-test housekeeping (vk suite): the step is deliberately NOT spliced
|
package/dist/args.js
CHANGED
|
@@ -49,10 +49,12 @@ const BOOLEAN = new Set([
|
|
|
49
49
|
'no-sleep-when-idle',
|
|
50
50
|
'allow-install',
|
|
51
51
|
'allow-unsafe-anonymous',
|
|
52
|
-
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
//
|
|
52
|
+
'no-failover',
|
|
53
|
+
// NOT here on purpose: 'allow-device-control', 'allow-failover' and 'ensure-device'
|
|
54
|
+
// are tri-state (absent / on / on-with-a-value), and the inline `--flag=value` branch
|
|
55
|
+
// is checked BEFORE this set — listing them would make `--allow-device-control=Pixel_6`
|
|
56
|
+
// parse fine but `--ensure-device Pixel_6` silently become a boolean plus a positional.
|
|
57
|
+
// Its opposite, 'no-failover', never takes a value, so it DOES belong above.
|
|
56
58
|
// Selector state modifiers (STATE_ATTRS in ui/selector.ts) and their negations.
|
|
57
59
|
// `enabled` was missing here until 0.15.0, and the omission was not cosmetic: a
|
|
58
60
|
// non-BOOLEAN flag swallows the next token, so `vk tap --enabled @submit` bound
|
package/dist/cli.js
CHANGED
|
@@ -50,6 +50,7 @@ exports.chooseLogOpts = chooseLogOpts;
|
|
|
50
50
|
exports.evalAssert = evalAssert;
|
|
51
51
|
exports.tokenizeLine = tokenizeLine;
|
|
52
52
|
exports.withBatchGlobals = withBatchGlobals;
|
|
53
|
+
exports.retryAfterDeviceMove = retryAfterDeviceMove;
|
|
53
54
|
exports.serverFromFlags = serverFromFlags;
|
|
54
55
|
exports.ensureDeviceTarget = ensureDeviceTarget;
|
|
55
56
|
exports.terminalFailure = terminalFailure;
|
|
@@ -1393,10 +1394,12 @@ function devicePrep(ctx) {
|
|
|
1393
1394
|
const serial = ctx.driver.resolvedSerial();
|
|
1394
1395
|
if ((0, args_1.flagBool)(ctx.flags, 'revert'))
|
|
1395
1396
|
return devicePrepRevert(ctx, serial, dryRun, asJson);
|
|
1397
|
+
// Which display policy: sleeps by itself after PREP_SCREEN_TIMEOUT, or never turns off.
|
|
1398
|
+
const sleepWhenIdle = !(0, args_1.flagBool)(ctx.flags, 'no-sleep-when-idle');
|
|
1396
1399
|
// Partition by what this platform can actually do, so the gate below counts real writes.
|
|
1397
1400
|
const skipped = [];
|
|
1398
1401
|
const applicable = [];
|
|
1399
|
-
for (const knob of prep_1.
|
|
1402
|
+
for (const knob of (0, prep_1.prepKnobs)(sleepWhenIdle)) {
|
|
1400
1403
|
const spec = settings_1.SETTINGS[knob.key];
|
|
1401
1404
|
const support = spec.support[ctx.platform];
|
|
1402
1405
|
if (support === 'unsupported') {
|
|
@@ -1450,7 +1453,6 @@ function devicePrep(ctx) {
|
|
|
1450
1453
|
}
|
|
1451
1454
|
// Earliest wins across re-preps, or `--revert` would restore the device to prepped.
|
|
1452
1455
|
const prior = (0, prep_1.readPrep)(serial);
|
|
1453
|
-
const sleepWhenIdle = !(0, args_1.flagBool)(ctx.flags, 'no-sleep-when-idle');
|
|
1454
1456
|
(0, prep_1.writePrep)((0, prep_1.newPrepRecord)(serial, ctx.platform, (0, prep_1.mergeOriginals)(prior?.original ?? {}, original), sleepWhenIdle));
|
|
1455
1457
|
const applied = Object.fromEntries(applicable.map((k) => [k.key, k.target]));
|
|
1456
1458
|
ctx.record?.note({ message: `device prep ${serial} (${changes.length} changed)` });
|
|
@@ -1462,11 +1464,32 @@ function devicePrep(ctx) {
|
|
|
1462
1464
|
for (const s of skipped)
|
|
1463
1465
|
(0, output_1.err)(`note: ${s.key} skipped — ${s.reason.replace(/\n/g, ' ')}`);
|
|
1464
1466
|
if (sleepWhenIdle)
|
|
1465
|
-
(0, output_1.err)(
|
|
1467
|
+
(0, output_1.err)(`note: the display sleeps by itself after ${prep_1.PREP_SCREEN_TIMEOUT} idle, and is woken on the next read`);
|
|
1468
|
+
warnSecureLock(ctx.platform, serial, sleepWhenIdle);
|
|
1466
1469
|
(0, output_1.err)(`undo with: verikun device prep --revert --device ${serial}`);
|
|
1467
1470
|
}
|
|
1468
1471
|
return 0;
|
|
1469
1472
|
}
|
|
1473
|
+
/**
|
|
1474
|
+
* Say so, once, when a device that will now sleep is behind a SECURE lock.
|
|
1475
|
+
*
|
|
1476
|
+
* The wake on the read path can only clear a *swipe* keyguard; a PIN/pattern/password one makes
|
|
1477
|
+
* `getElements` exit 3 rather than hand back the lock screen. That is honest, but it is a
|
|
1478
|
+
* failure, and prep — an explicit setup command that already resolved the serial — is where you
|
|
1479
|
+
* want to hear about it, not twenty minutes into a suite. It only warns: refusing would make a
|
|
1480
|
+
* perfectly usable device un-preppable, and `--no-sleep-when-idle` is the way out.
|
|
1481
|
+
*/
|
|
1482
|
+
function warnSecureLock(platform, serial, sleepWhenIdle) {
|
|
1483
|
+
if (!sleepWhenIdle || platform !== 'android')
|
|
1484
|
+
return;
|
|
1485
|
+
const lock = (0, adb_1.lockKindOf)(serial);
|
|
1486
|
+
if (lock === 'none' || lock === 'unknown')
|
|
1487
|
+
return;
|
|
1488
|
+
(0, output_1.err)(`warning: this device has a screen lock (${lock}), so a read after the display sleeps can land ` +
|
|
1489
|
+
'on the keyguard (verikun can only clear a swipe lock, and never asks for a PIN).\n' +
|
|
1490
|
+
'Remove it in Settings > Security, or keep the display lit with `verikun device prep ' +
|
|
1491
|
+
`--no-sleep-when-idle --device ${serial}\`.`);
|
|
1492
|
+
}
|
|
1470
1493
|
/** Put a prepared device back the way it was found, and forget it. */
|
|
1471
1494
|
function devicePrepRevert(ctx, serial, dryRun, asJson) {
|
|
1472
1495
|
const rec = (0, prep_1.readPrep)(serial);
|
|
@@ -1556,35 +1579,6 @@ function requireSettingKey(v) {
|
|
|
1556
1579
|
* same code. (Remote is a known gap: the overrides live in the *server's* run file, so
|
|
1557
1580
|
* a locally-empty snapshot means this correctly skips — see the issue's Out of scope.)
|
|
1558
1581
|
*/
|
|
1559
|
-
/**
|
|
1560
|
-
* Park prepared devices when the flow that used them ends — #97's "in sleep mode when they're
|
|
1561
|
-
* not [in use]".
|
|
1562
|
-
*
|
|
1563
|
-
* Three properties worth keeping:
|
|
1564
|
-
*
|
|
1565
|
-
* * It can only ever touch a device you explicitly PREPPED, and only if that prep did not
|
|
1566
|
-
* pass `--no-sleep-when-idle`. A borrowed phone that was never prepped is never slept.
|
|
1567
|
-
* * It goes through the driver rather than the command dispatcher, so it does not appear as
|
|
1568
|
-
* a `key sleep` testcase in the report. Parking is host hygiene, not a test step.
|
|
1569
|
-
* * `ownClaimedSerials()` is empty in `--server` mode (the local process never resolves a
|
|
1570
|
-
* device), so the remote path degrades to doing nothing on its own, with no branch here.
|
|
1571
|
-
* Managing a remote device's power is `--allow-device-control`'s job.
|
|
1572
|
-
*
|
|
1573
|
-
* Must run BEFORE `releaseOwnClaims()`, which is what it reads its serials from.
|
|
1574
|
-
*/
|
|
1575
|
-
function parkPreparedDevices(platform) {
|
|
1576
|
-
for (const serial of (0, claims_1.ownClaimedSerials)()) {
|
|
1577
|
-
if (!(0, prep_1.readPrep)(serial)?.sleepWhenIdle)
|
|
1578
|
-
continue;
|
|
1579
|
-
try {
|
|
1580
|
-
(0, output_1.err)(`[verikun] parking prepared device ${serial} (sleep)`);
|
|
1581
|
-
(0, drivers_1.getDriver)(platform, serial).pressKey('sleep');
|
|
1582
|
-
}
|
|
1583
|
-
catch {
|
|
1584
|
-
/* teardown must never throw — the device may be exactly why we are unwinding */
|
|
1585
|
-
}
|
|
1586
|
-
}
|
|
1587
|
-
}
|
|
1588
1582
|
async function restoreDeviceOverrides(backend) {
|
|
1589
1583
|
if (!run_1.Recorder.hasDeviceOverrides())
|
|
1590
1584
|
return;
|
|
@@ -1865,7 +1859,6 @@ async function cmdBatch(positionals, batchFlags) {
|
|
|
1865
1859
|
/* the device may be exactly why we are unwinding — never mask the real error */
|
|
1866
1860
|
}
|
|
1867
1861
|
}
|
|
1868
|
-
parkPreparedDevices(platformFromFlags(batchFlags));
|
|
1869
1862
|
(0, claims_1.releaseOwnClaims)();
|
|
1870
1863
|
}
|
|
1871
1864
|
}
|
|
@@ -1999,6 +1992,38 @@ async function obtainPlan(key, file, opts, cost, provider) {
|
|
|
1999
1992
|
}
|
|
2000
1993
|
return { plan: compiled.plan, cached: false };
|
|
2001
1994
|
}
|
|
1995
|
+
// --- execution backend (local driver vs remote `vk server`) -----------------
|
|
1996
|
+
//
|
|
1997
|
+
// `vk ai`, `vk suite`, and `vk install` run their device work through an
|
|
1998
|
+
// ExecBackend. Local wraps one shared Driver; remote speaks HTTP to a `vk server`
|
|
1999
|
+
// beside the device (--server / VERIKUN_SERVER), where each validated leaf is ONE
|
|
2000
|
+
// round-trip (the auto-wait loop stays server-side). In remote mode the server
|
|
2001
|
+
// owns the device: its /v1/health platform+serial supersede the client's
|
|
2002
|
+
// --platform/--device, and no local driver is ever built.
|
|
2003
|
+
/**
|
|
2004
|
+
* Run a remote read; if it failed AND the server reported it moved device, ask once more.
|
|
2005
|
+
*
|
|
2006
|
+
* This is the ONE place a failed remote read is re-asked, and the narrowness is the point.
|
|
2007
|
+
* It is only ever wired to `preflight` — the connect probe at `vk ai`/`vk suite` startup and
|
|
2008
|
+
* the suite's between-tests health check — where nothing has run yet on either device. A
|
|
2009
|
+
* mid-flow read is never retried: the new device has none of the state the flow built up, so
|
|
2010
|
+
* its screen would answer a different question than the one being asked.
|
|
2011
|
+
*
|
|
2012
|
+
* Gating on the move (rather than retrying every failure) also keeps the connect probe's
|
|
2013
|
+
* fail-fast property: a device that is simply broken still fails on the first try.
|
|
2014
|
+
*
|
|
2015
|
+
* Exported solely so the unit suite can reach it.
|
|
2016
|
+
*/
|
|
2017
|
+
async function retryAfterDeviceMove(read, moved) {
|
|
2018
|
+
try {
|
|
2019
|
+
return await read();
|
|
2020
|
+
}
|
|
2021
|
+
catch (e) {
|
|
2022
|
+
if (!moved())
|
|
2023
|
+
throw e;
|
|
2024
|
+
return read();
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2002
2027
|
/** The `--server` URL, or VERIKUN_SERVER. Exported-shape helper so `resolveBackend`
|
|
2003
2028
|
* and `vk devices --server` can never disagree about what "remote" means. */
|
|
2004
2029
|
function serverFromFlags(flags) {
|
|
@@ -2171,9 +2196,14 @@ async function resolveBackend(platform, device, flags) {
|
|
|
2171
2196
|
},
|
|
2172
2197
|
platform,
|
|
2173
2198
|
device,
|
|
2199
|
+
moves: [],
|
|
2174
2200
|
};
|
|
2175
2201
|
}
|
|
2176
2202
|
let runCtx = { platform, device };
|
|
2203
|
+
const moves = [];
|
|
2204
|
+
/** Set by the last move; the preflight below reads it to decide whether re-asking is
|
|
2205
|
+
* warranted, then clears it. */
|
|
2206
|
+
let movedDuringCall;
|
|
2177
2207
|
const opts = {
|
|
2178
2208
|
url: server,
|
|
2179
2209
|
authKey: (0, args_1.flagStr)(flags, 'auth-key') || process.env.VERIKUN_SERVER_AUTH_KEY || undefined,
|
|
@@ -2181,6 +2211,19 @@ async function resolveBackend(platform, device, flags) {
|
|
|
2181
2211
|
// is identical to a local run's. logStart travels from the server's device clock
|
|
2182
2212
|
// so archive-time / vk log scoping works without a local driver.
|
|
2183
2213
|
onStep: (step, artifacts, logStart) => run_1.Recorder.appendForeignStep(step, artifacts, { ...runCtx, logStart }),
|
|
2214
|
+
onDeviceChange: (c) => {
|
|
2215
|
+
moves.push(c);
|
|
2216
|
+
movedDuringCall = c;
|
|
2217
|
+
// Re-point the run context, so steps after the move are attributed to the device
|
|
2218
|
+
// that actually ran them. This makes `rolloverReason` seal the device-A run and
|
|
2219
|
+
// open a fresh one for B — intended: since a step is never replayed, no single run
|
|
2220
|
+
// can contain steps from two devices, and a report that claimed otherwise would lie.
|
|
2221
|
+
runCtx = { ...runCtx, device: c.to };
|
|
2222
|
+
(0, output_1.err)(`[verikun] server moved device: ${c.from} → ${c.to} (${c.reason})` +
|
|
2223
|
+
(c.retried
|
|
2224
|
+
? ' — retried there'
|
|
2225
|
+
: ' — this step failed on the old device; the next runs on the new one'));
|
|
2226
|
+
},
|
|
2184
2227
|
};
|
|
2185
2228
|
let health = await (0, remote_1.pingServer)(opts); // fails fast (exit 3) on a bad URL or key
|
|
2186
2229
|
// `--ensure-device` boots BEFORE runCtx is fixed: resolveBackend bakes the serial
|
|
@@ -2188,7 +2231,15 @@ async function resolveBackend(platform, device, flags) {
|
|
|
2188
2231
|
// attribute every spliced step to a device that didn't exist yet.
|
|
2189
2232
|
health = await ensureRemoteDevice(health, opts, server, flags);
|
|
2190
2233
|
runCtx = { platform: health.platform, device: health.serial ?? undefined };
|
|
2191
|
-
(0, output_1.err)(`[verikun] server ${server}: ${health.platform} · device ${health.serial ?? '(none)'} · verikun ${health.version}`
|
|
2234
|
+
(0, output_1.err)(`[verikun] server ${server}: ${health.platform} · device ${health.serial ?? '(none)'} · verikun ${health.version}` +
|
|
2235
|
+
(health.failoverEnabled ? ' · failover: on' : ''));
|
|
2236
|
+
// A pool the server has already ruled out explains a lot of otherwise-baffling
|
|
2237
|
+
// behaviour ("why is it on THAT phone?"), so say it once, up front.
|
|
2238
|
+
if (health.quarantined?.length) {
|
|
2239
|
+
(0, output_1.err)(`[verikun] server has ruled out ${health.quarantined.length} device(s):`);
|
|
2240
|
+
for (const q of health.quarantined)
|
|
2241
|
+
(0, output_1.err)(`[verikun] ${q.serial} ${q.reason}`);
|
|
2242
|
+
}
|
|
2192
2243
|
// Say the read path once, here. Reads execute server-side, so this is the only end of the
|
|
2193
2244
|
// connection that knows it — and without it a companion that had silently stood down was
|
|
2194
2245
|
// indistinguishable from one that never engaged, for a whole suite (issue #77). An older
|
|
@@ -2206,7 +2257,8 @@ async function resolveBackend(platform, device, flags) {
|
|
|
2206
2257
|
// healthy and keep grinding. One dump is the cheap call that actually proves it.
|
|
2207
2258
|
preflight: async () => {
|
|
2208
2259
|
await (0, remote_1.pingServer)(opts);
|
|
2209
|
-
|
|
2260
|
+
movedDuringCall = undefined;
|
|
2261
|
+
await retryAfterDeviceMove(() => remote.getElements(), () => movedDuringCall !== undefined);
|
|
2210
2262
|
},
|
|
2211
2263
|
// Hierarchy only: the server exposes no screenshot route, so a remote run's
|
|
2212
2264
|
// engine failure archives without a picture. Honest degrade over a protocol
|
|
@@ -2223,6 +2275,7 @@ async function resolveBackend(platform, device, flags) {
|
|
|
2223
2275
|
platform: health.platform,
|
|
2224
2276
|
device: health.serial ?? undefined,
|
|
2225
2277
|
remote: { url: server, version: health.version, reads: health.reads },
|
|
2278
|
+
moves,
|
|
2226
2279
|
};
|
|
2227
2280
|
}
|
|
2228
2281
|
/**
|
|
@@ -2462,8 +2515,7 @@ async function cmdAi(positionals, flags) {
|
|
|
2462
2515
|
// otherwise an unattended run leaves the phone offline or in dark mode.
|
|
2463
2516
|
await restoreDeviceOverrides(backend);
|
|
2464
2517
|
await backend.close?.(); // frees a remote server's device lock for the next command
|
|
2465
|
-
|
|
2466
|
-
(0, claims_1.releaseOwnClaims)(); // and the host-level claim, so the next job can have the device
|
|
2518
|
+
(0, claims_1.releaseOwnClaims)(); // the host-level claim, so the next job can have the device
|
|
2467
2519
|
}
|
|
2468
2520
|
if ((0, args_1.flagBool)(flags, 'json')) {
|
|
2469
2521
|
(0, output_1.json)({
|
|
@@ -2501,7 +2553,7 @@ async function cmdInstall(positionals, flags) {
|
|
|
2501
2553
|
if (!(0, node_fs_1.existsSync)(path))
|
|
2502
2554
|
throw new errors_1.CliError(`install: '${appPath}' does not exist`, 2);
|
|
2503
2555
|
const platform = platformFromFlags(flags);
|
|
2504
|
-
const { backend, remote } = await resolveBackend(platform, deviceFromFlags(flags, platform), flags);
|
|
2556
|
+
const { backend, remote, moves } = await resolveBackend(platform, deviceFromFlags(flags, platform), flags);
|
|
2505
2557
|
(0, output_1.err)(`[verikun] installing ${appPath}${remote ? ` via ${remote.url}` : ''}…`);
|
|
2506
2558
|
try {
|
|
2507
2559
|
await backend.install(path);
|
|
@@ -2509,10 +2561,16 @@ async function cmdInstall(positionals, flags) {
|
|
|
2509
2561
|
finally {
|
|
2510
2562
|
await backend.close?.();
|
|
2511
2563
|
}
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2564
|
+
// Where it LANDED, not just that it landed: after a failover that is a different
|
|
2565
|
+
// device than the one the run started against, and a caller acting on the old serial
|
|
2566
|
+
// (`adb -s … shell am start`) would be driving a phone without the build.
|
|
2567
|
+
const moved = moves.length ? moves[moves.length - 1] : undefined;
|
|
2568
|
+
if ((0, args_1.flagBool)(flags, 'json')) {
|
|
2569
|
+
(0, output_1.json)({ installed: appPath, ...(remote ? { server: remote.url } : {}), ...(moved ? { deviceChanged: moved } : {}) });
|
|
2570
|
+
}
|
|
2571
|
+
else {
|
|
2572
|
+
(0, output_1.out)(`installed ${appPath}${moved ? ` on ${moved.to}` : ''}`);
|
|
2573
|
+
}
|
|
2516
2574
|
return 0;
|
|
2517
2575
|
}
|
|
2518
2576
|
// ---------------------------------------------------------------------------
|
|
@@ -2551,7 +2609,6 @@ async function cmdSuiteEntry(positionals, flags) {
|
|
|
2551
2609
|
finally {
|
|
2552
2610
|
await restoreDeviceOverrides(backend);
|
|
2553
2611
|
await backend.close?.();
|
|
2554
|
-
parkPreparedDevices(platform);
|
|
2555
2612
|
(0, claims_1.releaseOwnClaims)();
|
|
2556
2613
|
}
|
|
2557
2614
|
}
|
|
@@ -2900,11 +2957,11 @@ DEVICE STATE (change the device the app runs on, then put it back)
|
|
|
2900
2957
|
do this automatically when the flow ends OR fails,
|
|
2901
2958
|
so a dead test can't leave the phone offline.
|
|
2902
2959
|
device prep [--dry-run] [--json] Prepare a TEST device once, stickily: animations off,
|
|
2903
|
-
display
|
|
2904
|
-
Survives the run (unlike \`device set\`),
|
|
2905
|
-
only by \`--revert\`. A PHYSICAL device must
|
|
2906
|
-
--device — prep must never land on a personal
|
|
2907
|
-
--no-sleep-when-idle keeps the
|
|
2960
|
+
display timeout ${prep_1.PREP_SCREEN_TIMEOUT}, Do Not Disturb on,
|
|
2961
|
+
battery idle off. Survives the run (unlike \`device set\`),
|
|
2962
|
+
so it is undone only by \`--revert\`. A PHYSICAL device must
|
|
2963
|
+
be named with --device — prep must never land on a personal
|
|
2964
|
+
phone. --no-sleep-when-idle keeps the display on for good.
|
|
2908
2965
|
device prep --revert [--dry-run] Put a prepared device back the way it was found
|
|
2909
2966
|
device caps [--json] What this platform supports, and the manual
|
|
2910
2967
|
equivalent where it doesn't
|
|
@@ -2961,7 +3018,7 @@ SUITE (run a directory of natural-language tests as one gated suite)
|
|
|
2961
3018
|
|
|
2962
3019
|
SERVER (expose a locally-connected device to remote verikun clients)
|
|
2963
3020
|
server [--bind addr] [--port n] [--auth-key k] [--allow-install]
|
|
2964
|
-
[--allow-device-control[=names]]
|
|
3021
|
+
[--allow-device-control[=names]] [--allow-failover[=serials]|--no-failover]
|
|
2965
3022
|
[--allow-unsafe-anonymous] Serve THIS machine's device over HTTP+JSON for
|
|
2966
3023
|
\`vk ai/suite/install --server <url>\`. Only
|
|
2967
3024
|
verikun's validated action grammar is runnable
|
|
@@ -2979,6 +3036,12 @@ SERVER (expose a locally-connected device to remote verikun clients)
|
|
|
2979
3036
|
the flag the server also starts even when no device is attached, so a client can
|
|
2980
3037
|
boot one: \`vk devices start|stop|restart [name] --server <url>\`, or add
|
|
2981
3038
|
--ensure-device[=name] to ai/suite/install to boot once before the first step.
|
|
3039
|
+
Failover is ON by default: if the bound device cannot serve a request, the server
|
|
3040
|
+
moves to another attached, healthy, unclaimed one and rules the bad one out until it
|
|
3041
|
+
is power-cycled. An install is retried there; a mid-run step is NOT — it fails on the
|
|
3042
|
+
device it ran on, and the next request lands on the healthy one. Passing --device
|
|
3043
|
+
pins the binding and turns this off; --allow-failover[=serials] turns it back on (and
|
|
3044
|
+
bounds where it may go), --no-failover / VERIKUN_NO_FAILOVER disables it outright.
|
|
2982
3045
|
|
|
2983
3046
|
ENVIRONMENT
|
|
2984
3047
|
devices [--all] [--json] List attached devices/simulators, and which job is
|
package/dist/device/claims.js
CHANGED
|
@@ -31,7 +31,6 @@
|
|
|
31
31
|
// Platform-agnostic by design, like `device/settings.ts` and `ui/`: it never touches
|
|
32
32
|
// adb/xcrun. The drivers know which devices exist; this knows which are taken.
|
|
33
33
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
34
|
-
exports.ownClaimedSerials = ownClaimedSerials;
|
|
35
34
|
exports.releaseOwnClaims = releaseOwnClaims;
|
|
36
35
|
exports.setProcessScoped = setProcessScoped;
|
|
37
36
|
exports.claimsEnabled = claimsEnabled;
|
|
@@ -73,11 +72,6 @@ let processScoped = false;
|
|
|
73
72
|
* store what it took is the only way it can release anything.
|
|
74
73
|
*/
|
|
75
74
|
const acquired = new Set();
|
|
76
|
-
/** Which devices this process took. Read by the prep teardown, which has to act on them
|
|
77
|
-
* BEFORE `releaseOwnClaims` empties this — see `parkPreparedDevices` in cli.ts. */
|
|
78
|
-
function ownClaimedSerials() {
|
|
79
|
-
return [...acquired];
|
|
80
|
-
}
|
|
81
75
|
/** Give back every device this process claimed. Best-effort; teardown must never throw. */
|
|
82
76
|
function releaseOwnClaims(o = {}) {
|
|
83
77
|
const released = [];
|