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/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.terminalFailure = terminalFailure;
|
|
53
54
|
exports.executeForServer = executeForServer;
|
|
54
55
|
exports.run = run;
|
|
55
56
|
const node_fs_1 = require("node:fs");
|
|
@@ -58,6 +59,8 @@ const args_1 = require("./args");
|
|
|
58
59
|
const errors_1 = require("./errors");
|
|
59
60
|
const exec_1 = require("./exec");
|
|
60
61
|
const drivers_1 = require("./drivers");
|
|
62
|
+
const adb_1 = require("./drivers/adb");
|
|
63
|
+
const settings_1 = require("./device/settings");
|
|
61
64
|
const selector_1 = require("./ui/selector");
|
|
62
65
|
const state_support_1 = require("./ui/state-support");
|
|
63
66
|
const format_1 = require("./ui/format");
|
|
@@ -923,6 +926,194 @@ function cmdClear(ctx) {
|
|
|
923
926
|
(0, output_1.out)(`cleared ${appId}`);
|
|
924
927
|
return 0;
|
|
925
928
|
}
|
|
929
|
+
// ---------------------------------------------------------------------------
|
|
930
|
+
// device — control the device the app runs on (see device/settings.ts)
|
|
931
|
+
// ---------------------------------------------------------------------------
|
|
932
|
+
//
|
|
933
|
+
// The capability table is the single source of truth: it validates values, decides
|
|
934
|
+
// what each platform supports, and prints itself via `device caps`. Two properties
|
|
935
|
+
// are load-bearing:
|
|
936
|
+
//
|
|
937
|
+
// - FAIL EARLY. Every assignment is parsed and capability-checked before ANY device
|
|
938
|
+
// I/O, so `device set dark=on rotation=landscape` on iOS refuses up front instead
|
|
939
|
+
// of applying dark mode and then dying — a half-applied device is worse than none.
|
|
940
|
+
// - SNAPSHOT FIRST. The pre-change value is persisted before the change is made, so
|
|
941
|
+
// `device reset` can undo it even from a later process.
|
|
942
|
+
const DEVICE_USAGE = 'Usage: verikun device set <key>=<value> [<key>=<value> ...] | device get [key] | device reset [key ...] | device caps\n' +
|
|
943
|
+
`Keys: ${settings_1.SETTING_KEYS.join(', ')}`;
|
|
944
|
+
function cmdDevice(ctx) {
|
|
945
|
+
const sub = (ctx.positionals[0] ?? '').toLowerCase();
|
|
946
|
+
const rest = ctx.positionals.slice(1);
|
|
947
|
+
switch (sub) {
|
|
948
|
+
case 'set':
|
|
949
|
+
return deviceSet(ctx, rest);
|
|
950
|
+
case 'get':
|
|
951
|
+
case 'status':
|
|
952
|
+
return deviceGet(ctx, rest);
|
|
953
|
+
case 'reset':
|
|
954
|
+
return deviceReset(ctx, rest);
|
|
955
|
+
case 'caps':
|
|
956
|
+
return deviceCaps(ctx);
|
|
957
|
+
default:
|
|
958
|
+
throw new errors_1.CliError((sub ? `Unknown 'device' subcommand '${sub}'.\n` : '') + DEVICE_USAGE, 2);
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
/** Render a setting value for humans/JSON; a platform that cannot answer says so. */
|
|
962
|
+
const showValue = (v) => v ?? 'n/a';
|
|
963
|
+
function deviceSet(ctx, args) {
|
|
964
|
+
const assignments = (0, settings_1.parseDeviceAssignments)(args);
|
|
965
|
+
// Gate 1: capability, for EVERY key, before touching the device.
|
|
966
|
+
const support = new Map();
|
|
967
|
+
for (const { key } of assignments)
|
|
968
|
+
support.set(key, (0, settings_1.checkSupport)(key, ctx.platform));
|
|
969
|
+
// Gate 2: would this cut the wire we are talking over? Only wireless adb is at
|
|
970
|
+
// risk, and only from the radios. A stderr warning would be useless here — the
|
|
971
|
+
// command that reads it is the one that just lost its transport — so refuse, and
|
|
972
|
+
// require an explicit opt-in to accept losing the link.
|
|
973
|
+
if (ctx.platform === 'android') {
|
|
974
|
+
const transport = (0, adb_1.adbTransport)(ctx.driver.resolvedSerial());
|
|
975
|
+
for (const { key, value } of assignments) {
|
|
976
|
+
if ((0, adb_1.severanceRisk)(transport, key, value) && !(0, args_1.flagBool)(ctx.flags, 'allow-wireless')) {
|
|
977
|
+
throw new errors_1.CliError(`Refusing to set ${key}=${value} over a wireless adb connection (${ctx.driver.resolvedSerial()}): ` +
|
|
978
|
+
'it would cut the link carrying this session, and nothing could turn it back on remotely.\n' +
|
|
979
|
+
'Connect over USB, or pass --allow-wireless to accept losing the connection.', 2);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
const applied = {};
|
|
984
|
+
for (const { key, value } of assignments) {
|
|
985
|
+
// A no-op key has nothing to put back, so it is never snapshotted.
|
|
986
|
+
if (support.get(key) !== 'noop')
|
|
987
|
+
snapshotSetting(ctx, key);
|
|
988
|
+
ctx.driver.setDeviceSetting(key, value);
|
|
989
|
+
applied[key] = value;
|
|
990
|
+
}
|
|
991
|
+
const summary = assignments.map((a) => `${a.key}=${a.value}`).join(' ');
|
|
992
|
+
ctx.record?.note({ message: `device set ${summary}` });
|
|
993
|
+
if ((0, args_1.flagBool)(ctx.flags, 'json'))
|
|
994
|
+
(0, output_1.json)({ set: applied });
|
|
995
|
+
else
|
|
996
|
+
(0, output_1.out)(`device set ${summary}`);
|
|
997
|
+
return 0;
|
|
998
|
+
}
|
|
999
|
+
/** Persist what a setting held before we change it. Warns rather than fails when the
|
|
1000
|
+
* value can't be captured — the change still happens, it just can't be auto-undone. */
|
|
1001
|
+
function snapshotSetting(ctx, key) {
|
|
1002
|
+
if (!ctx.record) {
|
|
1003
|
+
// Recording disabled (VERIKUN_NO_RUN=1): there is nowhere to persist the original,
|
|
1004
|
+
// so this change is one-way. Say so rather than implying reset will handle it.
|
|
1005
|
+
(0, output_1.err)(`warning: run recording is off, so '${key}' was not snapshotted — \`device reset\` cannot restore it`);
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
const original = ctx.driver.getDeviceSetting(key);
|
|
1009
|
+
if (original === null) {
|
|
1010
|
+
(0, output_1.err)(`warning: could not read the current '${key}' — \`device reset\` will not restore it`);
|
|
1011
|
+
return;
|
|
1012
|
+
}
|
|
1013
|
+
ctx.record.rememberDeviceOverride(key, original);
|
|
1014
|
+
}
|
|
1015
|
+
function deviceGet(ctx, args) {
|
|
1016
|
+
const keys = args.length ? args.map(requireSettingKey) : settings_1.SETTING_KEYS;
|
|
1017
|
+
const values = {};
|
|
1018
|
+
for (const key of keys) {
|
|
1019
|
+
values[key] = settings_1.SETTINGS[key].support[ctx.platform] === 'unsupported'
|
|
1020
|
+
? 'n/a'
|
|
1021
|
+
: showValue(ctx.driver.getDeviceSetting(key));
|
|
1022
|
+
}
|
|
1023
|
+
if ((0, args_1.flagBool)(ctx.flags, 'json'))
|
|
1024
|
+
(0, output_1.json)(values);
|
|
1025
|
+
else
|
|
1026
|
+
for (const key of keys)
|
|
1027
|
+
(0, output_1.out)(`${key.padEnd(12)} ${values[key]}`);
|
|
1028
|
+
return 0;
|
|
1029
|
+
}
|
|
1030
|
+
function deviceReset(ctx, args) {
|
|
1031
|
+
// Read from the recorder's in-flight state, not from disk: this step's own commit()
|
|
1032
|
+
// will write that state back, so a disk-level delete here would be undone.
|
|
1033
|
+
const overrides = ctx.record?.deviceOverrides() ?? {};
|
|
1034
|
+
const wanted = args.length ? args.map(requireSettingKey) : Object.keys(overrides);
|
|
1035
|
+
const restored = {};
|
|
1036
|
+
const failed = [];
|
|
1037
|
+
for (const key of wanted) {
|
|
1038
|
+
const original = overrides[key];
|
|
1039
|
+
if (original === undefined)
|
|
1040
|
+
continue; // never changed by this run — leave it alone
|
|
1041
|
+
try {
|
|
1042
|
+
ctx.driver.setDeviceSetting(key, original);
|
|
1043
|
+
restored[key] = original;
|
|
1044
|
+
}
|
|
1045
|
+
catch (e) {
|
|
1046
|
+
// Best-effort by design: reset is usually reached while cleaning up after a
|
|
1047
|
+
// failure, and one stubborn setting must not stop the rest being put back.
|
|
1048
|
+
failed.push(`${key} (${e instanceof Error ? e.message.split('\n')[0] : String(e)})`);
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
ctx.record?.forgetDeviceOverrides(Object.keys(restored));
|
|
1052
|
+
const summary = Object.entries(restored).map(([k, v]) => `${k}=${v}`).join(' ');
|
|
1053
|
+
ctx.record?.note({ message: summary ? `device reset ${summary}` : 'device reset (nothing to restore)' });
|
|
1054
|
+
if (failed.length)
|
|
1055
|
+
(0, output_1.err)(`warning: could not restore ${failed.join(', ')}`);
|
|
1056
|
+
if ((0, args_1.flagBool)(ctx.flags, 'json'))
|
|
1057
|
+
(0, output_1.json)({ restored, ...(failed.length ? { failed } : {}) });
|
|
1058
|
+
else
|
|
1059
|
+
(0, output_1.out)(summary ? `device reset ${summary}` : 'device reset: nothing to restore');
|
|
1060
|
+
return 0;
|
|
1061
|
+
}
|
|
1062
|
+
function deviceCaps(ctx) {
|
|
1063
|
+
const rows = settings_1.SETTING_KEYS.map((key) => {
|
|
1064
|
+
const spec = settings_1.SETTINGS[key];
|
|
1065
|
+
const support = spec.support[ctx.platform];
|
|
1066
|
+
return {
|
|
1067
|
+
key,
|
|
1068
|
+
values: spec.values,
|
|
1069
|
+
support,
|
|
1070
|
+
describe: spec.describe,
|
|
1071
|
+
...(support === 'unsupported' && spec.manual[ctx.platform] ? { manual: spec.manual[ctx.platform] } : {}),
|
|
1072
|
+
...(spec.note[ctx.platform] ? { note: spec.note[ctx.platform] } : {}),
|
|
1073
|
+
};
|
|
1074
|
+
});
|
|
1075
|
+
if ((0, args_1.flagBool)(ctx.flags, 'json')) {
|
|
1076
|
+
(0, output_1.json)({ platform: ctx.platform, settings: rows });
|
|
1077
|
+
return 0;
|
|
1078
|
+
}
|
|
1079
|
+
(0, output_1.out)(`device settings on ${ctx.platform}:`);
|
|
1080
|
+
for (const r of rows) {
|
|
1081
|
+
(0, output_1.out)(` ${r.key.padEnd(12)} ${r.support.padEnd(12)} ${r.values}`);
|
|
1082
|
+
(0, output_1.out)(` ${' '.repeat(12)} ${r.describe}`);
|
|
1083
|
+
if (r.manual)
|
|
1084
|
+
(0, output_1.out)(` ${' '.repeat(12)} unsupported: ${r.manual.replace(/\n/g, ' ')}`);
|
|
1085
|
+
if (r.note)
|
|
1086
|
+
(0, output_1.out)(` ${' '.repeat(12)} note: ${r.note}`);
|
|
1087
|
+
}
|
|
1088
|
+
return 0;
|
|
1089
|
+
}
|
|
1090
|
+
function requireSettingKey(v) {
|
|
1091
|
+
const k = v.trim().toLowerCase();
|
|
1092
|
+
if (!(0, settings_1.isSettingKey)(k)) {
|
|
1093
|
+
throw new errors_1.CliError(`Unknown device setting '${v}'. Known: ${settings_1.SETTING_KEYS.join(', ')}.`, 2);
|
|
1094
|
+
}
|
|
1095
|
+
return k;
|
|
1096
|
+
}
|
|
1097
|
+
/**
|
|
1098
|
+
* Put back every device setting this run changed. Best-effort and silent about
|
|
1099
|
+
* "nothing to do", because it runs from a `finally` — the flow may well be unwinding
|
|
1100
|
+
* from the very failure that left the device in a modified state.
|
|
1101
|
+
*
|
|
1102
|
+
* Runs through the backend rather than a Driver so the local and remote paths are the
|
|
1103
|
+
* same code. (Remote is a known gap: the overrides live in the *server's* run file, so
|
|
1104
|
+
* a locally-empty snapshot means this correctly skips — see the issue's Out of scope.)
|
|
1105
|
+
*/
|
|
1106
|
+
async function restoreDeviceOverrides(backend) {
|
|
1107
|
+
if (!run_1.Recorder.hasDeviceOverrides())
|
|
1108
|
+
return;
|
|
1109
|
+
try {
|
|
1110
|
+
(0, output_1.err)('[verikun] restoring device settings changed by this run…');
|
|
1111
|
+
await backend.exec('device', ['reset'], {});
|
|
1112
|
+
}
|
|
1113
|
+
catch {
|
|
1114
|
+
/* the device may be exactly why we are unwinding — never mask the real error */
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
926
1117
|
function cmdCurrent(ctx) {
|
|
927
1118
|
(0, output_1.out)(ctx.driver.currentApp());
|
|
928
1119
|
return 0;
|
|
@@ -1143,31 +1334,46 @@ async function cmdBatch(positionals, batchFlags) {
|
|
|
1143
1334
|
(0, output_1.err)('[verikun] batch: no commands to run');
|
|
1144
1335
|
return 0;
|
|
1145
1336
|
}
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1337
|
+
// The finally is what makes `device set` safe to use in a batch: a line that fails
|
|
1338
|
+
// (or a ^C) still puts the device back, instead of leaving it offline or rotated.
|
|
1339
|
+
try {
|
|
1340
|
+
for (const { n, text } of commands) {
|
|
1341
|
+
let code;
|
|
1342
|
+
try {
|
|
1343
|
+
const { command, positionals: pos, flags } = (0, args_1.parseArgs)(tokenizeLine(text));
|
|
1344
|
+
if (!command)
|
|
1345
|
+
continue; // tokens were all flags — nothing to run
|
|
1346
|
+
if (command === 'batch') {
|
|
1347
|
+
throw new errors_1.CliError(`batch: a batch line may not itself be 'batch' (line ${n})`, 2);
|
|
1348
|
+
}
|
|
1349
|
+
if (!quiet)
|
|
1350
|
+
(0, output_1.err)(`[verikun] batch ${n}: ${text}`);
|
|
1351
|
+
code = await executeParsed(command, pos, withBatchGlobals(flags, batchFlags));
|
|
1352
|
+
}
|
|
1353
|
+
catch (e) {
|
|
1354
|
+
// A malformed line (bad quoting, nested batch) is itself an error to halt on.
|
|
1355
|
+
code = mapError(e, batchFlags);
|
|
1356
|
+
}
|
|
1357
|
+
if (code !== 0) {
|
|
1358
|
+
(0, output_1.err)(`[verikun] batch stopped at line ${n} (\`${text}\`) — exit ${code}`);
|
|
1359
|
+
return code;
|
|
1154
1360
|
}
|
|
1155
|
-
if (!quiet)
|
|
1156
|
-
(0, output_1.err)(`[verikun] batch ${n}: ${text}`);
|
|
1157
|
-
code = await executeParsed(command, pos, withBatchGlobals(flags, batchFlags));
|
|
1158
|
-
}
|
|
1159
|
-
catch (e) {
|
|
1160
|
-
// A malformed line (bad quoting, nested batch) is itself an error to halt on.
|
|
1161
|
-
code = mapError(e, batchFlags);
|
|
1162
1361
|
}
|
|
1163
|
-
if (
|
|
1164
|
-
(0, output_1.err)(`[verikun] batch
|
|
1165
|
-
|
|
1362
|
+
if (!quiet)
|
|
1363
|
+
(0, output_1.err)(`[verikun] batch: ${commands.length} command(s) ok`);
|
|
1364
|
+
return 0;
|
|
1365
|
+
}
|
|
1366
|
+
finally {
|
|
1367
|
+
if (run_1.Recorder.hasDeviceOverrides()) {
|
|
1368
|
+
(0, output_1.err)('[verikun] restoring device settings changed by this batch…');
|
|
1369
|
+
try {
|
|
1370
|
+
await executeParsed('device', ['reset'], withBatchGlobals({}, batchFlags));
|
|
1371
|
+
}
|
|
1372
|
+
catch {
|
|
1373
|
+
/* the device may be exactly why we are unwinding — never mask the real error */
|
|
1374
|
+
}
|
|
1166
1375
|
}
|
|
1167
1376
|
}
|
|
1168
|
-
if (!quiet)
|
|
1169
|
-
(0, output_1.err)(`[verikun] batch: ${commands.length} command(s) ok`);
|
|
1170
|
-
return 0;
|
|
1171
1377
|
}
|
|
1172
1378
|
function parseAiOptions(flags) {
|
|
1173
1379
|
const model = (0, cost_1.resolveModel)((0, args_1.flagStr)(flags, 'model'));
|
|
@@ -1322,6 +1528,24 @@ async function resolveBackend(platform, device, flags) {
|
|
|
1322
1528
|
driver.clearApp(appId);
|
|
1323
1529
|
},
|
|
1324
1530
|
preflight: () => driver.preflight(),
|
|
1531
|
+
captureFailure: async () => {
|
|
1532
|
+
// Two independent tries: a screencap can succeed where a dump doesn't (and
|
|
1533
|
+
// vice versa), and neither is allowed to derail recording the failure.
|
|
1534
|
+
const out = {};
|
|
1535
|
+
try {
|
|
1536
|
+
out.png = driver.screenshot();
|
|
1537
|
+
}
|
|
1538
|
+
catch {
|
|
1539
|
+
/* device may be gone — that may be why we failed */
|
|
1540
|
+
}
|
|
1541
|
+
try {
|
|
1542
|
+
out.hierarchy = driver.getElements({ all: false });
|
|
1543
|
+
}
|
|
1544
|
+
catch {
|
|
1545
|
+
/* ditto */
|
|
1546
|
+
}
|
|
1547
|
+
return out;
|
|
1548
|
+
},
|
|
1325
1549
|
},
|
|
1326
1550
|
platform,
|
|
1327
1551
|
device,
|
|
@@ -1352,12 +1576,52 @@ async function resolveBackend(platform, device, flags) {
|
|
|
1352
1576
|
await (0, remote_1.pingServer)(opts);
|
|
1353
1577
|
await remote.getElements();
|
|
1354
1578
|
},
|
|
1579
|
+
// Hierarchy only: the server exposes no screenshot route, so a remote run's
|
|
1580
|
+
// engine failure archives without a picture. Honest degrade over a protocol
|
|
1581
|
+
// change here — tracked in #48.
|
|
1582
|
+
captureFailure: async () => {
|
|
1583
|
+
try {
|
|
1584
|
+
return { hierarchy: await remote.getElements() };
|
|
1585
|
+
}
|
|
1586
|
+
catch {
|
|
1587
|
+
return {};
|
|
1588
|
+
}
|
|
1589
|
+
},
|
|
1355
1590
|
},
|
|
1356
1591
|
platform: health.platform,
|
|
1357
1592
|
device: health.serial,
|
|
1358
1593
|
remote: { url: server, version: health.version },
|
|
1359
1594
|
};
|
|
1360
1595
|
}
|
|
1596
|
+
/**
|
|
1597
|
+
* The one terminal-failure record for a non-ok engine result — `null` when the run
|
|
1598
|
+
* passed. Exported for the unit suite.
|
|
1599
|
+
*
|
|
1600
|
+
* Budget and timeout aborts come back from the engine as a bare flag with NO `failure`
|
|
1601
|
+
* object, so their reason is composed here; `where` is `run` because the abort is not
|
|
1602
|
+
* attributable to one node. Both the recorded failure and the `[ai] …` status line are
|
|
1603
|
+
* built from this, so the archive and the console cannot disagree about why a run died.
|
|
1604
|
+
*/
|
|
1605
|
+
function terminalFailure(r, opts) {
|
|
1606
|
+
if (r.ok)
|
|
1607
|
+
return null;
|
|
1608
|
+
if (r.abortedForEnv) {
|
|
1609
|
+
return { where: r.failure?.where ?? 'run', reason: r.failure?.reason ?? 'device unavailable', kind: 'env' };
|
|
1610
|
+
}
|
|
1611
|
+
if (r.abortedForBudget) {
|
|
1612
|
+
return { where: r.failure?.where ?? 'run', reason: `cost ceiling $${opts.maxCostUsd} reached`, kind: 'budget' };
|
|
1613
|
+
}
|
|
1614
|
+
if (r.abortedForTimeout) {
|
|
1615
|
+
return { where: 'run', reason: `run timeout (${Math.round(opts.timeoutMs / 1000)}s) reached`, kind: 'timeout' };
|
|
1616
|
+
}
|
|
1617
|
+
return { where: r.failure?.where ?? 'run', reason: r.failure?.reason ?? 'failed', kind: 'fail' };
|
|
1618
|
+
}
|
|
1619
|
+
/** The `[ai] …` console verdict for a terminal failure, phrased as it always was. */
|
|
1620
|
+
function terminalStatusLine(t) {
|
|
1621
|
+
if (t.kind === 'fail')
|
|
1622
|
+
return `FAIL at ${t.where}: ${t.reason}`;
|
|
1623
|
+
return `ABORTED — ${t.kind === 'env' ? `environment: ${t.reason}` : t.reason}`;
|
|
1624
|
+
}
|
|
1361
1625
|
/**
|
|
1362
1626
|
* Best-effort archive-time device-log capture via an ExecBackend (local driver or
|
|
1363
1627
|
* remote `/v1/logs`). Writes `artifacts/logcat.txt` onto the active run so a later
|
|
@@ -1470,6 +1734,13 @@ async function runAiTest(file, opts, backend, platform, device) {
|
|
|
1470
1734
|
// seal the run so it is not left dangling in .verikun/run/ for the next command
|
|
1471
1735
|
// to roll over. Then let the error map to an exit code as usual.
|
|
1472
1736
|
run_1.Recorder.annotateRun({ ai: { ok: false, cost: cost.summaryLine(), modelRepairs: 0, improvements: [] } });
|
|
1737
|
+
// No evidence capture here: a throw at this level usually IS the device dying, so
|
|
1738
|
+
// the capture would fail the same way and only add noise to the error path.
|
|
1739
|
+
run_1.Recorder.recordTerminalFailure({
|
|
1740
|
+
where: 'run',
|
|
1741
|
+
reason: e.message,
|
|
1742
|
+
kind: (0, errors_1.isEnvError)(e) ? 'env' : 'fail',
|
|
1743
|
+
});
|
|
1473
1744
|
try {
|
|
1474
1745
|
await prefetchArchiveLogs(backend);
|
|
1475
1746
|
run_1.Recorder.archive();
|
|
@@ -1496,21 +1767,18 @@ async function runAiTest(file, opts, backend, platform, device) {
|
|
|
1496
1767
|
(0, output_1.err)(`[ai] could not cache plan: ${e.message}`);
|
|
1497
1768
|
}
|
|
1498
1769
|
}
|
|
1770
|
+
// A failure the engine produced (a control node giving up, a budget/timeout abort)
|
|
1771
|
+
// never ran through a command, so nothing recorded it — without this the archive
|
|
1772
|
+
// declares the failed test green. Must come BEFORE the archive that renders it.
|
|
1773
|
+
const terminal = terminalFailure(result, opts);
|
|
1774
|
+
if (terminal)
|
|
1775
|
+
run_1.Recorder.recordTerminalFailure(terminal, await backend.captureFailure?.());
|
|
1499
1776
|
run_1.Recorder.annotateRun({
|
|
1500
1777
|
ai: { ok: result.ok, cost: costLine, modelRepairs: result.modelRepairs, improvements: result.improvements },
|
|
1501
1778
|
});
|
|
1502
1779
|
await prefetchArchiveLogs(backend);
|
|
1503
1780
|
const { dir, xmlPath, htmlPath, state } = run_1.Recorder.archive();
|
|
1504
|
-
|
|
1505
|
-
? 'PASS'
|
|
1506
|
-
: result.abortedForBudget
|
|
1507
|
-
? `ABORTED — cost ceiling $${opts.maxCostUsd} reached`
|
|
1508
|
-
: result.abortedForTimeout
|
|
1509
|
-
? `ABORTED — run timeout (${Math.round(opts.timeoutMs / 1000)}s) reached`
|
|
1510
|
-
: result.abortedForEnv
|
|
1511
|
-
? `ABORTED — environment: ${result.failure?.reason}`
|
|
1512
|
-
: `FAIL at ${result.failure?.where}: ${result.failure?.reason}`;
|
|
1513
|
-
(0, output_1.err)(`[ai] ${status} · ${costLine}`);
|
|
1781
|
+
(0, output_1.err)(`[ai] ${terminal ? terminalStatusLine(terminal) : 'PASS'} · ${costLine}`);
|
|
1514
1782
|
(0, output_1.err)(`[ai] report: ${htmlPath}`);
|
|
1515
1783
|
if (result.improvements.length) {
|
|
1516
1784
|
(0, output_1.err)(`[ai] ${result.improvements.length} suggested improvement(s) (also in the report):`);
|
|
@@ -1557,6 +1825,9 @@ async function cmdAi(positionals, flags) {
|
|
|
1557
1825
|
result = await runAiTest(file, opts, backend, platform, device);
|
|
1558
1826
|
}
|
|
1559
1827
|
finally {
|
|
1828
|
+
// Undo any device setting the test changed, INCLUDING when it failed part-way —
|
|
1829
|
+
// otherwise an unattended run leaves the phone offline or in dark mode.
|
|
1830
|
+
await restoreDeviceOverrides(backend);
|
|
1560
1831
|
await backend.close?.(); // frees a remote server's device lock for the next command
|
|
1561
1832
|
}
|
|
1562
1833
|
if ((0, args_1.flagBool)(flags, 'json')) {
|
|
@@ -1639,6 +1910,7 @@ async function cmdSuiteEntry(positionals, flags) {
|
|
|
1639
1910
|
});
|
|
1640
1911
|
}
|
|
1641
1912
|
finally {
|
|
1913
|
+
await restoreDeviceOverrides(backend);
|
|
1642
1914
|
await backend.close?.();
|
|
1643
1915
|
}
|
|
1644
1916
|
}
|
|
@@ -1688,6 +1960,8 @@ async function executeCommand(command, ctx) {
|
|
|
1688
1960
|
return cmdStop(ctx);
|
|
1689
1961
|
case 'clear':
|
|
1690
1962
|
return cmdClear(ctx);
|
|
1963
|
+
case 'device':
|
|
1964
|
+
return cmdDevice(ctx);
|
|
1691
1965
|
case 'current':
|
|
1692
1966
|
return cmdCurrent(ctx);
|
|
1693
1967
|
case 'log':
|
|
@@ -1730,7 +2004,7 @@ async function executeOutcome(command, positionals, flags, sharedDriver) {
|
|
|
1730
2004
|
// Recordable commands open a step (auto-starting an implicit run if needed);
|
|
1731
2005
|
// the step is finalized with the outcome — and, on failure, screenshot + UI
|
|
1732
2006
|
// hierarchy of the page are captured — whether the command returns or throws.
|
|
1733
|
-
const recordable = (0, run_1.isRecordable)(command);
|
|
2007
|
+
const recordable = (0, run_1.isRecordable)(command, positionals);
|
|
1734
2008
|
let driver = sharedDriver;
|
|
1735
2009
|
let recorder = null;
|
|
1736
2010
|
try {
|
|
@@ -1908,6 +2182,22 @@ ACT
|
|
|
1908
2182
|
idb install). With --server, uploads the file to a
|
|
1909
2183
|
remote vk server (which must run --allow-install)
|
|
1910
2184
|
|
|
2185
|
+
DEVICE STATE (change the device the app runs on, then put it back)
|
|
2186
|
+
device set <key>=<value> ... Apply settings, snapshotting each original first.
|
|
2187
|
+
Keys: ${settings_1.SETTING_KEYS.join(', ')}
|
|
2188
|
+
e.g. \`device set airplane=on\` to test offline handling,
|
|
2189
|
+
\`device set dark=on font-scale=1.3 rotation=landscape\`.
|
|
2190
|
+
Each change is verified by reading it back — the
|
|
2191
|
+
underlying device commands silently no-op on some skins.
|
|
2192
|
+
device get [key] [--json] Show current values ('n/a' where unsupported)
|
|
2193
|
+
device reset [key ...] Restore what this run changed. batch/ai/suite also
|
|
2194
|
+
do this automatically when the flow ends OR fails,
|
|
2195
|
+
so a dead test can't leave the phone offline.
|
|
2196
|
+
device caps [--json] What this platform supports, and the manual
|
|
2197
|
+
equivalent where it doesn't
|
|
2198
|
+
Refuses \`airplane=on\` over wireless adb (it would cut
|
|
2199
|
+
this very connection); --allow-wireless overrides.
|
|
2200
|
+
|
|
1911
2201
|
BATCH (script many commands in one process)
|
|
1912
2202
|
batch [--file path] [--quiet] Run newline-separated commands — from --file,
|
|
1913
2203
|
else piped stdin — each exactly as its own
|
|
@@ -2031,5 +2321,7 @@ EXIT CODES
|
|
|
2031
2321
|
|
|
2032
2322
|
iOS (--ios): full parity via idb — ui/tap/text/swipe/key + screenshot/launch/stop.
|
|
2033
2323
|
Needs idb (\`brew install idb-companion\` + \`pip install fb-idb\`); see \`vk doctor --ios\`.
|
|
2034
|
-
Caveats: no \`clear\` (no per-app reset), \`current\` is (unknown), device logs are simulator-only
|
|
2324
|
+
Caveats: no \`clear\` (no per-app reset), \`current\` is (unknown), device logs are simulator-only.
|
|
2325
|
+
\`device set\`: dark + font-scale work on a SIMULATOR (font-scale maps to the nearest
|
|
2326
|
+
Dynamic Type category); airplane and rotation are unsupported — run \`vk device caps --ios\`.`;
|
|
2035
2327
|
}
|