verikun 0.18.0 → 0.18.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/README.md +7 -1
- package/dist/cli.js +73 -10
- package/dist/report.js +68 -3
- package/dist/run.js +67 -6
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -144,11 +144,17 @@ skips the archive dump on green runs (failures still capture).
|
|
|
144
144
|
- **`report.html`** — a self-contained report: every step, the identifiers used,
|
|
145
145
|
any screenshots taken, the screenshot + hierarchy of any failed page, a link to
|
|
146
146
|
the full device log, an app-scoped log accordion when the run launched an app,
|
|
147
|
-
and any per-step logs from `vk log`.
|
|
147
|
+
and any per-step logs from `vk log`. A run that did not pass says so in a banner
|
|
148
|
+
at the top.
|
|
148
149
|
- **`artifacts/logcat.txt`** — full device log for the run window (default).
|
|
149
150
|
- **`artifacts/logcat-app.txt`** — app-scoped log (when a package/bundle was launched).
|
|
150
151
|
- **`run.json`** — the raw recording.
|
|
151
152
|
|
|
153
|
+
A `vk ai` run can also fail where no single command did — a `repeat` that never
|
|
154
|
+
sees its target, a cost/timeout abort. That verdict is recorded too (as
|
|
155
|
+
`run.json`'s `failure`, plus a failed step carrying the reason and a screenshot),
|
|
156
|
+
so a failed test is never reported as `failures="0"` in the JUnit that CI reads.
|
|
157
|
+
|
|
152
158
|
`vk run archive` exits non-zero when the run contained failures, so the same
|
|
153
159
|
command both produces the report and gates CI.
|
|
154
160
|
|
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");
|
|
@@ -1322,6 +1323,24 @@ async function resolveBackend(platform, device, flags) {
|
|
|
1322
1323
|
driver.clearApp(appId);
|
|
1323
1324
|
},
|
|
1324
1325
|
preflight: () => driver.preflight(),
|
|
1326
|
+
captureFailure: async () => {
|
|
1327
|
+
// Two independent tries: a screencap can succeed where a dump doesn't (and
|
|
1328
|
+
// vice versa), and neither is allowed to derail recording the failure.
|
|
1329
|
+
const out = {};
|
|
1330
|
+
try {
|
|
1331
|
+
out.png = driver.screenshot();
|
|
1332
|
+
}
|
|
1333
|
+
catch {
|
|
1334
|
+
/* device may be gone — that may be why we failed */
|
|
1335
|
+
}
|
|
1336
|
+
try {
|
|
1337
|
+
out.hierarchy = driver.getElements({ all: false });
|
|
1338
|
+
}
|
|
1339
|
+
catch {
|
|
1340
|
+
/* ditto */
|
|
1341
|
+
}
|
|
1342
|
+
return out;
|
|
1343
|
+
},
|
|
1325
1344
|
},
|
|
1326
1345
|
platform,
|
|
1327
1346
|
device,
|
|
@@ -1352,12 +1371,52 @@ async function resolveBackend(platform, device, flags) {
|
|
|
1352
1371
|
await (0, remote_1.pingServer)(opts);
|
|
1353
1372
|
await remote.getElements();
|
|
1354
1373
|
},
|
|
1374
|
+
// Hierarchy only: the server exposes no screenshot route, so a remote run's
|
|
1375
|
+
// engine failure archives without a picture. Honest degrade over a protocol
|
|
1376
|
+
// change here — tracked in #48.
|
|
1377
|
+
captureFailure: async () => {
|
|
1378
|
+
try {
|
|
1379
|
+
return { hierarchy: await remote.getElements() };
|
|
1380
|
+
}
|
|
1381
|
+
catch {
|
|
1382
|
+
return {};
|
|
1383
|
+
}
|
|
1384
|
+
},
|
|
1355
1385
|
},
|
|
1356
1386
|
platform: health.platform,
|
|
1357
1387
|
device: health.serial,
|
|
1358
1388
|
remote: { url: server, version: health.version },
|
|
1359
1389
|
};
|
|
1360
1390
|
}
|
|
1391
|
+
/**
|
|
1392
|
+
* The one terminal-failure record for a non-ok engine result — `null` when the run
|
|
1393
|
+
* passed. Exported for the unit suite.
|
|
1394
|
+
*
|
|
1395
|
+
* Budget and timeout aborts come back from the engine as a bare flag with NO `failure`
|
|
1396
|
+
* object, so their reason is composed here; `where` is `run` because the abort is not
|
|
1397
|
+
* attributable to one node. Both the recorded failure and the `[ai] …` status line are
|
|
1398
|
+
* built from this, so the archive and the console cannot disagree about why a run died.
|
|
1399
|
+
*/
|
|
1400
|
+
function terminalFailure(r, opts) {
|
|
1401
|
+
if (r.ok)
|
|
1402
|
+
return null;
|
|
1403
|
+
if (r.abortedForEnv) {
|
|
1404
|
+
return { where: r.failure?.where ?? 'run', reason: r.failure?.reason ?? 'device unavailable', kind: 'env' };
|
|
1405
|
+
}
|
|
1406
|
+
if (r.abortedForBudget) {
|
|
1407
|
+
return { where: r.failure?.where ?? 'run', reason: `cost ceiling $${opts.maxCostUsd} reached`, kind: 'budget' };
|
|
1408
|
+
}
|
|
1409
|
+
if (r.abortedForTimeout) {
|
|
1410
|
+
return { where: 'run', reason: `run timeout (${Math.round(opts.timeoutMs / 1000)}s) reached`, kind: 'timeout' };
|
|
1411
|
+
}
|
|
1412
|
+
return { where: r.failure?.where ?? 'run', reason: r.failure?.reason ?? 'failed', kind: 'fail' };
|
|
1413
|
+
}
|
|
1414
|
+
/** The `[ai] …` console verdict for a terminal failure, phrased as it always was. */
|
|
1415
|
+
function terminalStatusLine(t) {
|
|
1416
|
+
if (t.kind === 'fail')
|
|
1417
|
+
return `FAIL at ${t.where}: ${t.reason}`;
|
|
1418
|
+
return `ABORTED — ${t.kind === 'env' ? `environment: ${t.reason}` : t.reason}`;
|
|
1419
|
+
}
|
|
1361
1420
|
/**
|
|
1362
1421
|
* Best-effort archive-time device-log capture via an ExecBackend (local driver or
|
|
1363
1422
|
* remote `/v1/logs`). Writes `artifacts/logcat.txt` onto the active run so a later
|
|
@@ -1470,6 +1529,13 @@ async function runAiTest(file, opts, backend, platform, device) {
|
|
|
1470
1529
|
// seal the run so it is not left dangling in .verikun/run/ for the next command
|
|
1471
1530
|
// to roll over. Then let the error map to an exit code as usual.
|
|
1472
1531
|
run_1.Recorder.annotateRun({ ai: { ok: false, cost: cost.summaryLine(), modelRepairs: 0, improvements: [] } });
|
|
1532
|
+
// No evidence capture here: a throw at this level usually IS the device dying, so
|
|
1533
|
+
// the capture would fail the same way and only add noise to the error path.
|
|
1534
|
+
run_1.Recorder.recordTerminalFailure({
|
|
1535
|
+
where: 'run',
|
|
1536
|
+
reason: e.message,
|
|
1537
|
+
kind: (0, errors_1.isEnvError)(e) ? 'env' : 'fail',
|
|
1538
|
+
});
|
|
1473
1539
|
try {
|
|
1474
1540
|
await prefetchArchiveLogs(backend);
|
|
1475
1541
|
run_1.Recorder.archive();
|
|
@@ -1496,21 +1562,18 @@ async function runAiTest(file, opts, backend, platform, device) {
|
|
|
1496
1562
|
(0, output_1.err)(`[ai] could not cache plan: ${e.message}`);
|
|
1497
1563
|
}
|
|
1498
1564
|
}
|
|
1565
|
+
// A failure the engine produced (a control node giving up, a budget/timeout abort)
|
|
1566
|
+
// never ran through a command, so nothing recorded it — without this the archive
|
|
1567
|
+
// declares the failed test green. Must come BEFORE the archive that renders it.
|
|
1568
|
+
const terminal = terminalFailure(result, opts);
|
|
1569
|
+
if (terminal)
|
|
1570
|
+
run_1.Recorder.recordTerminalFailure(terminal, await backend.captureFailure?.());
|
|
1499
1571
|
run_1.Recorder.annotateRun({
|
|
1500
1572
|
ai: { ok: result.ok, cost: costLine, modelRepairs: result.modelRepairs, improvements: result.improvements },
|
|
1501
1573
|
});
|
|
1502
1574
|
await prefetchArchiveLogs(backend);
|
|
1503
1575
|
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}`);
|
|
1576
|
+
(0, output_1.err)(`[ai] ${terminal ? terminalStatusLine(terminal) : 'PASS'} · ${costLine}`);
|
|
1514
1577
|
(0, output_1.err)(`[ai] report: ${htmlPath}`);
|
|
1515
1578
|
if (result.improvements.length) {
|
|
1516
1579
|
(0, output_1.err)(`[ai] ${result.improvements.length} suggested improvement(s) (also in the report):`);
|
package/dist/report.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// recorder (run.ts) / suite runner (suite.ts) own all the I/O. The RunState data
|
|
6
6
|
// model lives in run.ts; we import the types only.
|
|
7
7
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.runFailure = runFailure;
|
|
8
9
|
exports.toJUnitXml = toJUnitXml;
|
|
9
10
|
exports.suiteTotals = suiteTotals;
|
|
10
11
|
exports.toSuiteIndexJson = toSuiteIndexJson;
|
|
@@ -41,12 +42,40 @@ function resolvedLabel(s) {
|
|
|
41
42
|
function fmtDuration(ms) {
|
|
42
43
|
return ms < 1000 ? `${ms} ms` : `${(ms / 1000).toFixed(2)} s`;
|
|
43
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* The run's verdict, taken from the ENGINE rather than inferred from step statuses.
|
|
47
|
+
*
|
|
48
|
+
* A `vk ai` run can fail outside any command — a `repeat` that never sees its target,
|
|
49
|
+
* a budget/timeout abort — and those record no step, so a tally-only report declared
|
|
50
|
+
* the run green (issue #41). `Recorder.recordTerminalFailure` now writes `failure`, and
|
|
51
|
+
* `ai.ok` is the older, coarser signal we still fall back to; between them an
|
|
52
|
+
* unrecorded failure can no longer read as success.
|
|
53
|
+
*/
|
|
54
|
+
function runFailure(run) {
|
|
55
|
+
if (run.failure)
|
|
56
|
+
return run.failure;
|
|
57
|
+
if (run.ai && !run.ai.ok)
|
|
58
|
+
return { where: 'run', reason: 'the run did not pass (no step recorded the failure)' };
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
44
61
|
function counts(run) {
|
|
45
62
|
const passed = run.steps.filter((s) => s.status === 'passed').length;
|
|
46
63
|
const failures = run.steps.filter((s) => s.status === 'failed').length;
|
|
47
64
|
const errors = run.steps.filter((s) => s.status === 'error').length;
|
|
48
65
|
const timeMs = run.steps.reduce((a, s) => a + s.durationMs, 0);
|
|
49
|
-
|
|
66
|
+
// Belt and braces. Normally the terminal failure IS a step by the time we render, so
|
|
67
|
+
// this stays false; it fires only if the failure never reached the recorder, and then
|
|
68
|
+
// both renderers emit one extra entry — hence `tests` grows too, so the tally never
|
|
69
|
+
// disagrees with the testcase list it is supposed to describe.
|
|
70
|
+
const unrecorded = failures + errors === 0 && runFailure(run) !== null;
|
|
71
|
+
return {
|
|
72
|
+
tests: run.steps.length + (unrecorded ? 1 : 0),
|
|
73
|
+
passed,
|
|
74
|
+
failures: failures + (unrecorded ? 1 : 0),
|
|
75
|
+
errors,
|
|
76
|
+
timeMs,
|
|
77
|
+
unrecorded,
|
|
78
|
+
};
|
|
50
79
|
}
|
|
51
80
|
// --- JUnit ----------------------------------------------------------------
|
|
52
81
|
function toJUnitXml(run) {
|
|
@@ -93,6 +122,12 @@ function toJUnitXml(run) {
|
|
|
93
122
|
return ` <testcase ${attrs}>${body}\n </testcase>`;
|
|
94
123
|
})
|
|
95
124
|
.join('\n');
|
|
125
|
+
const f = runFailure(run);
|
|
126
|
+
const unrecordedCase = c.unrecorded && f
|
|
127
|
+
? ` <testcase name="${xmlAttr(`run did not pass (${f.where})`)}" classname="verikun.run" time="0.000">` +
|
|
128
|
+
`\n <failure message="${xmlAttr(f.reason)}" type="AssertionFailure">${xmlText(`${f.where}: ${f.reason}`)}</failure>\n </testcase>`
|
|
129
|
+
: '';
|
|
130
|
+
const allCases = [cases, unrecordedCase].filter(Boolean).join('\n');
|
|
96
131
|
const suiteAttrs = `name="${xmlAttr(run.name)}" tests="${c.tests}" failures="${c.failures}" ` +
|
|
97
132
|
`errors="${c.errors}" time="${suiteTime}" timestamp="${xmlAttr(run.startedAt)}"`;
|
|
98
133
|
const suiteExtras = [];
|
|
@@ -106,7 +141,7 @@ function toJUnitXml(run) {
|
|
|
106
141
|
return (`<?xml version="1.0" encoding="UTF-8"?>\n` +
|
|
107
142
|
`<testsuites name="verikun" tests="${c.tests}" failures="${c.failures}" errors="${c.errors}" time="${suiteTime}">\n` +
|
|
108
143
|
`<testsuite ${suiteAttrs}>\n` +
|
|
109
|
-
`${
|
|
144
|
+
`${allCases}\n` +
|
|
110
145
|
(suiteExtras.length
|
|
111
146
|
? ` <system-out>${xmlText(suiteExtras.join('\n'))}</system-out>\n`
|
|
112
147
|
: '') +
|
|
@@ -151,6 +186,9 @@ const STYLE = `
|
|
|
151
186
|
.aibox { background:#fff; border:1px solid var(--line); border-radius:8px; padding:12px 14px; margin-bottom:20px; font-size:13px; }
|
|
152
187
|
.aibox .cost { font-family:ui-monospace,SFMono-Regular,Menlo,monospace; color:var(--muted); margin-top:4px; }
|
|
153
188
|
.aibox ul { margin:8px 0 0; padding-left:18px; }
|
|
189
|
+
.failbox { background:#fff; border:1px solid var(--fail); border-left-width:4px; border-radius:8px; padding:12px 14px; margin-bottom:20px; font-size:13px; }
|
|
190
|
+
.failbox .where { font-family:ui-monospace,SFMono-Regular,Menlo,monospace; color:var(--muted); }
|
|
191
|
+
.failbox .why { color:var(--fail); margin-top:4px; }
|
|
154
192
|
`;
|
|
155
193
|
function aiPanelHtml(ai) {
|
|
156
194
|
const improvements = ai.improvements.length
|
|
@@ -164,6 +202,14 @@ function aiPanelHtml(ai) {
|
|
|
164
202
|
${improvements}
|
|
165
203
|
</div>`;
|
|
166
204
|
}
|
|
205
|
+
/** The run-level failure, stated once at the top. This page is where a human looks
|
|
206
|
+
* first, so the verdict has to be visible without reading 26 green rows. */
|
|
207
|
+
function failBoxHtml(f) {
|
|
208
|
+
return `<div class="failbox">
|
|
209
|
+
<div><strong>This run did not pass.</strong> <span class="where">${htmlEsc(f.where)}</span></div>
|
|
210
|
+
<div class="why">${htmlEsc(f.reason)}</div>
|
|
211
|
+
</div>`;
|
|
212
|
+
}
|
|
167
213
|
function stepHtml(s) {
|
|
168
214
|
const detail = [];
|
|
169
215
|
if (selectorLabel(s))
|
|
@@ -333,6 +379,7 @@ ${suite.tests.map((x) => suiteTestRow(x, linkBase)).join('\n')}
|
|
|
333
379
|
*/
|
|
334
380
|
function toHtml(run, opts = {}) {
|
|
335
381
|
const c = counts(run);
|
|
382
|
+
const failure = runFailure(run);
|
|
336
383
|
const chips = [
|
|
337
384
|
`<span class="chip pass">${c.passed} passed</span>`,
|
|
338
385
|
c.failures ? `<span class="chip fail">${c.failures} failed</span>` : '',
|
|
@@ -376,9 +423,27 @@ function toHtml(run, opts = {}) {
|
|
|
376
423
|
<div class="summary">
|
|
377
424
|
${chips}
|
|
378
425
|
</div>
|
|
426
|
+
${failure ? failBoxHtml(failure) : ''}
|
|
379
427
|
${run.ai ? aiPanelHtml(run.ai) : ''}
|
|
380
428
|
<ol class="steps">
|
|
381
|
-
${
|
|
429
|
+
${[
|
|
430
|
+
...run.steps.map(stepHtml),
|
|
431
|
+
// Only when the failure reached no step — otherwise it is already a red row.
|
|
432
|
+
...(c.unrecorded && failure
|
|
433
|
+
? [
|
|
434
|
+
stepHtml({
|
|
435
|
+
index: run.steps.length,
|
|
436
|
+
command: 'ai',
|
|
437
|
+
name: `run did not pass (${failure.where})`,
|
|
438
|
+
startedAt: run.startedAt,
|
|
439
|
+
durationMs: 0,
|
|
440
|
+
status: 'failed',
|
|
441
|
+
exitCode: 1,
|
|
442
|
+
message: failure.reason,
|
|
443
|
+
}),
|
|
444
|
+
]
|
|
445
|
+
: []),
|
|
446
|
+
].join('\n ')}
|
|
382
447
|
</ol>${appLogPanel}
|
|
383
448
|
</div>
|
|
384
449
|
</body>
|
package/dist/run.js
CHANGED
|
@@ -105,6 +105,19 @@ function loadState(dir) {
|
|
|
105
105
|
return null;
|
|
106
106
|
}
|
|
107
107
|
}
|
|
108
|
+
/** Relative path of a step's failure screenshot. One place, because a synthetic
|
|
109
|
+
* terminal-failure step has to land on the same convention as a recorded one. */
|
|
110
|
+
const failImagePath = (index) => `artifacts/step-${index}-fail.png`;
|
|
111
|
+
function writeArtifactTo(dir, rel, buf) {
|
|
112
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.join)(dir, 'artifacts'), { recursive: true });
|
|
113
|
+
(0, node_fs_1.writeFileSync)((0, node_path_1.join)(dir, rel), buf);
|
|
114
|
+
}
|
|
115
|
+
/** Compact hierarchy text, HEAD-capped — a failure dump is read top-down, so the
|
|
116
|
+
* first screenful is the useful part (unlike logs, which are kept tail-first). */
|
|
117
|
+
function capHierarchy(els) {
|
|
118
|
+
const text = (0, format_1.formatCompact)(els);
|
|
119
|
+
return text.length > HIERARCHY_CAP ? text.slice(0, HIERARCHY_CAP) + '\n…(truncated)' : text;
|
|
120
|
+
}
|
|
108
121
|
function saveState(dir, state) {
|
|
109
122
|
(0, node_fs_1.mkdirSync)(dir, { recursive: true });
|
|
110
123
|
(0, node_fs_1.writeFileSync)(statePath(dir), JSON.stringify(state, null, 2));
|
|
@@ -427,8 +440,8 @@ class Recorder {
|
|
|
427
440
|
if (!driver)
|
|
428
441
|
return;
|
|
429
442
|
try {
|
|
430
|
-
this.writeArtifact(
|
|
431
|
-
this.step.failImage =
|
|
443
|
+
this.writeArtifact(failImagePath(this.step.index), driver.screenshot());
|
|
444
|
+
this.step.failImage = failImagePath(this.step.index);
|
|
432
445
|
}
|
|
433
446
|
catch (e) {
|
|
434
447
|
// Best-effort evidence: the device may be gone (often why the step failed). Surface
|
|
@@ -437,8 +450,7 @@ class Recorder {
|
|
|
437
450
|
(0, output_1.err)(`[verikun] could not capture failure screenshot (${e.message})`);
|
|
438
451
|
}
|
|
439
452
|
try {
|
|
440
|
-
|
|
441
|
-
this.step.failHierarchy = text.length > HIERARCHY_CAP ? text.slice(0, HIERARCHY_CAP) + '\n…(truncated)' : text;
|
|
453
|
+
this.step.failHierarchy = capHierarchy(driver.getElements({ all: false }));
|
|
442
454
|
}
|
|
443
455
|
catch (e) {
|
|
444
456
|
if (!quiet)
|
|
@@ -450,8 +462,7 @@ class Recorder {
|
|
|
450
462
|
this.sink[rel] = buf;
|
|
451
463
|
return;
|
|
452
464
|
}
|
|
453
|
-
(
|
|
454
|
-
(0, node_fs_1.writeFileSync)((0, node_path_1.join)(this.dir, rel), buf);
|
|
465
|
+
writeArtifactTo(this.dir, rel, buf);
|
|
455
466
|
}
|
|
456
467
|
commit() {
|
|
457
468
|
this.step.durationMs = Date.now() - this.startMs;
|
|
@@ -647,6 +658,56 @@ class Recorder {
|
|
|
647
658
|
last.message = message;
|
|
648
659
|
saveState(dir, state);
|
|
649
660
|
}
|
|
661
|
+
/**
|
|
662
|
+
* Record a terminal failure the `vk ai` ENGINE produced rather than a command — a
|
|
663
|
+
* control node that gave up (`repeat` exhausted, `when` matched no branch), a
|
|
664
|
+
* budget/timeout abort, an engine-internal throw. None of those go through
|
|
665
|
+
* beginStep, so before this existed nothing marked the run red and the archived
|
|
666
|
+
* report declared a failed test fully green (issue #41).
|
|
667
|
+
*
|
|
668
|
+
* Always records the run-level verdict. Appends a synthetic failed step ONLY when
|
|
669
|
+
* no step is already red — a leaf failure carries its own step and evidence, and
|
|
670
|
+
* counting it twice would be its own kind of lie.
|
|
671
|
+
*/
|
|
672
|
+
static recordTerminalFailure(failure, evidence) {
|
|
673
|
+
if (process.env.VERIKUN_NO_RUN)
|
|
674
|
+
return;
|
|
675
|
+
const dir = activeDir();
|
|
676
|
+
const state = loadState(dir);
|
|
677
|
+
if (!state)
|
|
678
|
+
return;
|
|
679
|
+
state.failure = { where: failure.where, reason: failure.reason };
|
|
680
|
+
if (state.steps.every((s) => s.status === 'passed')) {
|
|
681
|
+
const index = state.steps.length;
|
|
682
|
+
// An environment abort is exit 3 (the box is broken, not the app), matching
|
|
683
|
+
// what `vk ai` itself returns; everything else is an exit-1 assertion failure.
|
|
684
|
+
const env = failure.kind === 'env';
|
|
685
|
+
const step = {
|
|
686
|
+
index,
|
|
687
|
+
command: 'ai',
|
|
688
|
+
name: `ai ${env ? 'aborted' : 'failed'} at ${failure.where}`,
|
|
689
|
+
startedAt: nowIso(),
|
|
690
|
+
durationMs: 0,
|
|
691
|
+
status: env ? 'error' : 'failed',
|
|
692
|
+
exitCode: env ? 3 : 1,
|
|
693
|
+
message: failure.reason,
|
|
694
|
+
};
|
|
695
|
+
if (evidence?.png) {
|
|
696
|
+
try {
|
|
697
|
+
writeArtifactTo(dir, failImagePath(index), evidence.png);
|
|
698
|
+
step.failImage = failImagePath(index);
|
|
699
|
+
}
|
|
700
|
+
catch (e) {
|
|
701
|
+
(0, output_1.err)(`[verikun] could not write the failure screenshot (${e.message})`);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
if (evidence?.hierarchy)
|
|
705
|
+
step.failHierarchy = capHierarchy(evidence.hierarchy);
|
|
706
|
+
state.steps.push(step);
|
|
707
|
+
}
|
|
708
|
+
state.updatedAt = nowIso();
|
|
709
|
+
saveState(dir, state);
|
|
710
|
+
}
|
|
650
711
|
static start(name, platform, device, force) {
|
|
651
712
|
const dir = activeDir();
|
|
652
713
|
const existing = loadState(dir);
|
package/dist/version.js
CHANGED
|
@@ -3,4 +3,4 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.VERSION = void 0;
|
|
4
4
|
// GENERATED by scripts/gen-version.mjs from package.json's "version" at build time
|
|
5
5
|
// (the `prebuild` script). Do NOT edit by hand; bump package.json instead.
|
|
6
|
-
exports.VERSION = '0.18.
|
|
6
|
+
exports.VERSION = '0.18.1';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "verikun",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.1",
|
|
4
4
|
"description": "Drive Android emulators/devices and iOS simulators for AI agents: tap, type, swipe, screenshot, and inspect the UI hierarchy by semantic identifiers — like Puppeteer for native apps.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"android",
|