visor-ai 0.2.7 → 0.2.9
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/dist/adapters.js +224 -22
- package/dist/appMap.js +2486 -0
- package/dist/cli.js +424 -45
- package/dist/daemon.js +87 -11
- package/dist/localRuntime.js +124 -0
- package/dist/runner.js +25 -2
- package/package.json +4 -1
package/dist/cli.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import { DEFAULT_SERVER_URL } from './adapters.js';
|
|
3
|
-
import { DaemonRequestTimeoutError, DaemonUnavailableError, runDaemonAction, runDaemonScenario, startVisorDaemon, statusVisorDaemon, stopVisorDaemon } from './daemon.js';
|
|
3
|
+
import { DaemonOperationError, DaemonRequestTimeoutError, DaemonUnavailableError, runDaemonAction, runDaemonDiscover, runDaemonScenario, startVisorDaemon, statusVisorDaemon, stopVisorDaemon } from './daemon.js';
|
|
4
4
|
import { DeviceSelectionError, resolveRunningDevice } from './devices.js';
|
|
5
5
|
import { makeError } from './errors.js';
|
|
6
|
+
import { LocalRuntimeAdapter } from './localRuntime.js';
|
|
6
7
|
import { writeReports } from './report.js';
|
|
7
|
-
import { determinismCheck } from './runner.js';
|
|
8
|
+
import { determinismCheck, runScenario } from './runner.js';
|
|
8
9
|
import { errorMessage, makeId, utcNowIso } from './utils.js';
|
|
9
10
|
import { parseAndValidate } from './validator.js';
|
|
10
11
|
function packageVersion() {
|
|
@@ -29,6 +30,7 @@ const ALL_COMMANDS = new Set([
|
|
|
29
30
|
...ACTION_COMMANDS,
|
|
30
31
|
'validate',
|
|
31
32
|
'run',
|
|
33
|
+
'discover',
|
|
32
34
|
'benchmark',
|
|
33
35
|
'report',
|
|
34
36
|
'start',
|
|
@@ -44,6 +46,8 @@ const GLOBAL_SPEC = {
|
|
|
44
46
|
'server-url': 'string',
|
|
45
47
|
'app-id': 'string',
|
|
46
48
|
attach: 'boolean',
|
|
49
|
+
'no-map': 'boolean',
|
|
50
|
+
repair: 'boolean',
|
|
47
51
|
verbose: 'boolean'
|
|
48
52
|
};
|
|
49
53
|
const ACTION_SPEC = {
|
|
@@ -51,12 +55,17 @@ const ACTION_SPEC = {
|
|
|
51
55
|
target: 'string',
|
|
52
56
|
x: 'number',
|
|
53
57
|
y: 'number',
|
|
58
|
+
'start-x': 'number',
|
|
59
|
+
'start-y': 'number',
|
|
60
|
+
'end-x': 'number',
|
|
61
|
+
'end-y': 'number',
|
|
54
62
|
direction: 'string',
|
|
55
63
|
percent: 'number',
|
|
56
64
|
normalized: 'boolean',
|
|
57
65
|
to: 'string',
|
|
58
66
|
name: 'string',
|
|
59
67
|
value: 'string',
|
|
68
|
+
'start-value': 'number',
|
|
60
69
|
label: 'string',
|
|
61
70
|
ms: 'number',
|
|
62
71
|
path: 'string'
|
|
@@ -68,9 +77,28 @@ const COMMAND_SPECS = {
|
|
|
68
77
|
timeout: 'number',
|
|
69
78
|
output: 'string',
|
|
70
79
|
format: 'string',
|
|
80
|
+
runtime: 'string',
|
|
71
81
|
'server-url': 'string',
|
|
72
82
|
'app-id': 'string',
|
|
73
|
-
attach: 'boolean'
|
|
83
|
+
attach: 'boolean',
|
|
84
|
+
'no-map': 'boolean',
|
|
85
|
+
repair: 'boolean',
|
|
86
|
+
crawl: 'boolean',
|
|
87
|
+
'crawl-depth': 'number',
|
|
88
|
+
'crawl-limit': 'number'
|
|
89
|
+
},
|
|
90
|
+
discover: {
|
|
91
|
+
device: 'string',
|
|
92
|
+
timeout: 'number',
|
|
93
|
+
format: 'string',
|
|
94
|
+
'server-url': 'string',
|
|
95
|
+
'app-id': 'string',
|
|
96
|
+
attach: 'boolean',
|
|
97
|
+
'no-map': 'boolean',
|
|
98
|
+
repair: 'boolean',
|
|
99
|
+
crawl: 'boolean',
|
|
100
|
+
'crawl-depth': 'number',
|
|
101
|
+
'crawl-limit': 'number'
|
|
74
102
|
},
|
|
75
103
|
benchmark: {
|
|
76
104
|
runs: 'number',
|
|
@@ -81,7 +109,10 @@ const COMMAND_SPECS = {
|
|
|
81
109
|
format: 'string',
|
|
82
110
|
'server-url': 'string',
|
|
83
111
|
'app-id': 'string',
|
|
84
|
-
attach: 'boolean'
|
|
112
|
+
attach: 'boolean',
|
|
113
|
+
'no-map': 'boolean',
|
|
114
|
+
repair: 'boolean',
|
|
115
|
+
'compare-map': 'boolean'
|
|
85
116
|
},
|
|
86
117
|
report: { format: 'string' },
|
|
87
118
|
start: {
|
|
@@ -106,6 +137,13 @@ const COMMAND_SPECS = {
|
|
|
106
137
|
wait: ACTION_SPEC,
|
|
107
138
|
source: ACTION_SPEC
|
|
108
139
|
};
|
|
140
|
+
const ACTION_ARG_OPTION_NAMES = {
|
|
141
|
+
'start-x': 'startX',
|
|
142
|
+
'start-y': 'startY',
|
|
143
|
+
'end-x': 'endX',
|
|
144
|
+
'end-y': 'endY',
|
|
145
|
+
'start-value': 'startValue'
|
|
146
|
+
};
|
|
109
147
|
function helpText() {
|
|
110
148
|
return [
|
|
111
149
|
'Visor TypeScript CLI',
|
|
@@ -117,8 +155,9 @@ function helpText() {
|
|
|
117
155
|
'',
|
|
118
156
|
'Commands:',
|
|
119
157
|
' validate <scenario>',
|
|
120
|
-
' run <scenario> [--output <dir>]',
|
|
158
|
+
' run <scenario> [--output <dir>] [--runtime appium|local]',
|
|
121
159
|
' benchmark <scenario> [--runs <n>] [--threshold <percent>]',
|
|
160
|
+
' discover [--app-id <id>]',
|
|
122
161
|
' report [path]',
|
|
123
162
|
' start [--server-url <url>] [--appium-cmd <cmd>]',
|
|
124
163
|
' status [--server-url <url>]',
|
|
@@ -127,12 +166,125 @@ function helpText() {
|
|
|
127
166
|
'',
|
|
128
167
|
'Examples:',
|
|
129
168
|
' visor validate scenarios/checkout-smoke.json',
|
|
169
|
+
' visor run path/to/scenario.json --runtime local --output artifacts-local',
|
|
130
170
|
' visor start --server-url http://127.0.0.1:4723',
|
|
131
171
|
' visor run scenarios/checkout-smoke.json --output artifacts-test',
|
|
172
|
+
' visor run scenarios/checkout-smoke.json --no-map',
|
|
173
|
+
' visor discover --app-id com.example.app',
|
|
132
174
|
' visor scroll --device emulator-5554 --direction down',
|
|
133
175
|
' visor status'
|
|
134
176
|
].join('\n');
|
|
135
177
|
}
|
|
178
|
+
function commandHelpText(command) {
|
|
179
|
+
if (command === 'tap') {
|
|
180
|
+
const examples = [
|
|
181
|
+
'visor tap --target accessibility=Continue',
|
|
182
|
+
'visor tap --target text=Continue',
|
|
183
|
+
'visor tap --target text~=Settings',
|
|
184
|
+
'visor tap --target "first-in-section=Featured products"',
|
|
185
|
+
'visor tap --x 120 --y 640',
|
|
186
|
+
'visor tap --x 0.5 --y 0.92 --normalized'
|
|
187
|
+
];
|
|
188
|
+
return {
|
|
189
|
+
usageText: [
|
|
190
|
+
'Visor tap',
|
|
191
|
+
'',
|
|
192
|
+
'Usage:',
|
|
193
|
+
' visor tap --target <selector> [runtime options]',
|
|
194
|
+
' visor tap --x <points> --y <points> [--normalized] [runtime options]',
|
|
195
|
+
'',
|
|
196
|
+
'Options:',
|
|
197
|
+
' --target <selector> Selector to tap',
|
|
198
|
+
' --x <points> X coordinate in screen points, or fraction with --normalized',
|
|
199
|
+
' --y <points> Y coordinate in screen points, or fraction with --normalized',
|
|
200
|
+
' --normalized Treat x/y as fractions of current screen size',
|
|
201
|
+
' --no-map Disable app-map reads and writes',
|
|
202
|
+
' --repair Allow opt-in exploratory app-map repair'
|
|
203
|
+
].join('\n'),
|
|
204
|
+
examples
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
if (command === 'discover') {
|
|
208
|
+
const examples = [
|
|
209
|
+
'visor discover --app-id com.example.app',
|
|
210
|
+
'visor discover --app-id com.example.app --crawl --crawl-depth 2 --crawl-limit 24'
|
|
211
|
+
];
|
|
212
|
+
return {
|
|
213
|
+
usageText: [
|
|
214
|
+
'Visor discover',
|
|
215
|
+
'',
|
|
216
|
+
'Usage:',
|
|
217
|
+
' visor discover [--app-id <id>] [--crawl] [runtime options]',
|
|
218
|
+
'',
|
|
219
|
+
'Options:',
|
|
220
|
+
' --crawl Explore safe controls and record app-map edges',
|
|
221
|
+
' --crawl-depth <n> Maximum crawl depth, default 2',
|
|
222
|
+
' --crawl-limit <n> Maximum crawl actions, default 24',
|
|
223
|
+
' --no-map Disable app-map reads and writes'
|
|
224
|
+
].join('\n'),
|
|
225
|
+
examples
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
if (command === 'act') {
|
|
229
|
+
const examples = [
|
|
230
|
+
'visor act --name back',
|
|
231
|
+
'visor act --name reset --app-id com.example.app',
|
|
232
|
+
'visor act --name drag --start-x 120 --start-y 640 --end-x 320 --end-y 640',
|
|
233
|
+
'visor act --name slider --target accessibility=Amount --value 0.5'
|
|
234
|
+
];
|
|
235
|
+
return {
|
|
236
|
+
usageText: [
|
|
237
|
+
'Visor act',
|
|
238
|
+
'',
|
|
239
|
+
'Usage:',
|
|
240
|
+
' visor act --name <type|back|home|reset|drag|slider> [options]',
|
|
241
|
+
'',
|
|
242
|
+
'Options:',
|
|
243
|
+
' --name <operation> Helper action name',
|
|
244
|
+
' --target <selector> Target selector for type or slider',
|
|
245
|
+
' --value <value> Text value for type, or 0..1 slider value',
|
|
246
|
+
' --start-x <points> Drag start x coordinate',
|
|
247
|
+
' --start-y <points> Drag start y coordinate',
|
|
248
|
+
' --end-x <points> Drag end x coordinate',
|
|
249
|
+
' --end-y <points> Drag end y coordinate',
|
|
250
|
+
' --start-value <0..1> Slider starting value, default 0.5',
|
|
251
|
+
' --normalized Treat drag coordinates as viewport fractions'
|
|
252
|
+
].join('\n'),
|
|
253
|
+
examples
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
if (command === 'benchmark') {
|
|
257
|
+
const examples = [
|
|
258
|
+
'visor benchmark scenarios/checkout-smoke.json --runs 20',
|
|
259
|
+
'visor benchmark scenarios/checkout-smoke.json --runs 5 --compare-map'
|
|
260
|
+
];
|
|
261
|
+
return {
|
|
262
|
+
usageText: [
|
|
263
|
+
'Visor benchmark',
|
|
264
|
+
'',
|
|
265
|
+
'Usage:',
|
|
266
|
+
' visor benchmark <scenario> [--runs <n>] [--threshold <percent>] [--compare-map]',
|
|
267
|
+
'',
|
|
268
|
+
'Options:',
|
|
269
|
+
' --runs <n> Number of runs, default 20',
|
|
270
|
+
' --threshold <percent> Required determinism score, default 95',
|
|
271
|
+
' --compare-map Run no-map and app-map variants for A/B comparison'
|
|
272
|
+
].join('\n'),
|
|
273
|
+
examples
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
return {
|
|
277
|
+
usageText: [
|
|
278
|
+
`Visor ${command}`,
|
|
279
|
+
'',
|
|
280
|
+
'Usage:',
|
|
281
|
+
` visor ${command} [options]`,
|
|
282
|
+
'',
|
|
283
|
+
'Run visor --help for the full command list.'
|
|
284
|
+
].join('\n'),
|
|
285
|
+
examples: []
|
|
286
|
+
};
|
|
287
|
+
}
|
|
136
288
|
function envelopeOk(commandId, startedAt, artifacts = [], nextAction = '') {
|
|
137
289
|
return {
|
|
138
290
|
status: 'ok',
|
|
@@ -216,7 +368,14 @@ async function resolvedRuntime(options) {
|
|
|
216
368
|
output_dir: String(options.output ?? 'artifacts'),
|
|
217
369
|
server_url: String(options['server-url'] ?? DEFAULT_SERVER_URL),
|
|
218
370
|
app_id: typeof options['app-id'] === 'string' ? options['app-id'] : undefined,
|
|
219
|
-
attach_to_running: Boolean(options.attach)
|
|
371
|
+
attach_to_running: Boolean(options.attach),
|
|
372
|
+
map: {
|
|
373
|
+
enabled: !Boolean(options['no-map']),
|
|
374
|
+
...(options.repair === true ? { repair: true } : {}),
|
|
375
|
+
...(options.crawl === true ? { crawl: true } : {}),
|
|
376
|
+
...(typeof options['crawl-depth'] === 'number' ? { crawlDepth: options['crawl-depth'] } : {}),
|
|
377
|
+
...(typeof options['crawl-limit'] === 'number' ? { crawlLimit: options['crawl-limit'] } : {})
|
|
378
|
+
}
|
|
220
379
|
};
|
|
221
380
|
}
|
|
222
381
|
function actionArgs(command, options) {
|
|
@@ -228,12 +387,15 @@ function actionArgs(command, options) {
|
|
|
228
387
|
'verbose',
|
|
229
388
|
'server-url',
|
|
230
389
|
'seed',
|
|
390
|
+
'runtime',
|
|
231
391
|
'app-id',
|
|
232
|
-
'attach'
|
|
392
|
+
'attach',
|
|
393
|
+
'no-map',
|
|
394
|
+
'repair'
|
|
233
395
|
]);
|
|
234
396
|
const args = Object.entries(options).reduce((acc, [key, value]) => {
|
|
235
397
|
if (!commonIgnored.has(key) && value !== undefined) {
|
|
236
|
-
acc[key] = value;
|
|
398
|
+
acc[ACTION_ARG_OPTION_NAMES[key] ?? key] = value;
|
|
237
399
|
}
|
|
238
400
|
return acc;
|
|
239
401
|
}, {});
|
|
@@ -242,6 +404,10 @@ function actionArgs(command, options) {
|
|
|
242
404
|
}
|
|
243
405
|
return args;
|
|
244
406
|
}
|
|
407
|
+
function unsupportedRuntimeResult(commandId, startedAt, runtime) {
|
|
408
|
+
const response = envelopeFail(commandId, startedAt, 'INPUT_ERROR', 'Unsupported runtime', `Unsupported runtime '${String(runtime)}'`, 'Use --runtime appium or --runtime local');
|
|
409
|
+
return { code: 1, response };
|
|
410
|
+
}
|
|
245
411
|
function isTargetInitializationError(error) {
|
|
246
412
|
const message = errorMessage(error);
|
|
247
413
|
return (message.includes('Failed to create WebdriverIO Appium session') ||
|
|
@@ -261,15 +427,80 @@ function targetInitializationNextStep(error) {
|
|
|
261
427
|
}
|
|
262
428
|
return 'Verify Appium driver setup, target device state, and app id, then retry.';
|
|
263
429
|
}
|
|
264
|
-
function
|
|
430
|
+
function isRecord(value) {
|
|
431
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
432
|
+
}
|
|
433
|
+
function optionalString(value) {
|
|
434
|
+
return typeof value === 'string' ? value : undefined;
|
|
435
|
+
}
|
|
436
|
+
function mapArray(value) {
|
|
437
|
+
return Array.isArray(value) ? value : [];
|
|
438
|
+
}
|
|
439
|
+
function mapActionCount(variants) {
|
|
440
|
+
let total = 0;
|
|
441
|
+
for (const variant of variants) {
|
|
442
|
+
if (!isRecord(variant)) {
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
total += mapArray(variant.actions).length;
|
|
446
|
+
}
|
|
447
|
+
return total;
|
|
448
|
+
}
|
|
449
|
+
function appMapSummaryFromPath(mapPath) {
|
|
450
|
+
try {
|
|
451
|
+
const parsed = JSON.parse(fs.readFileSync(mapPath, 'utf8'));
|
|
452
|
+
if (!isRecord(parsed)) {
|
|
453
|
+
return undefined;
|
|
454
|
+
}
|
|
455
|
+
const variants = mapArray(parsed.variants);
|
|
456
|
+
return {
|
|
457
|
+
schema_version: typeof parsed.schema_version === 'number' ? parsed.schema_version : undefined,
|
|
458
|
+
identity: optionalString(parsed.identity),
|
|
459
|
+
app_id: optionalString(parsed.app_id),
|
|
460
|
+
platform: optionalString(parsed.platform),
|
|
461
|
+
screens: mapArray(parsed.screens).length,
|
|
462
|
+
variants: variants.length,
|
|
463
|
+
edges: mapArray(parsed.edges).length,
|
|
464
|
+
actions: mapActionCount(variants),
|
|
465
|
+
auth_required_variants: variants.filter((variant) => isRecord(variant) && variant.auth_required === true).length,
|
|
466
|
+
updated_at: optionalString(parsed.updated_at)
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
catch {
|
|
470
|
+
return undefined;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
function addDiscoverMapSummary(result) {
|
|
474
|
+
if (!isRecord(result.map)) {
|
|
475
|
+
return result;
|
|
476
|
+
}
|
|
477
|
+
const map = result.map;
|
|
478
|
+
if (map.summary !== undefined || typeof map.path !== 'string') {
|
|
479
|
+
return result;
|
|
480
|
+
}
|
|
481
|
+
const summary = appMapSummaryFromPath(map.path);
|
|
482
|
+
if (!summary) {
|
|
483
|
+
return result;
|
|
484
|
+
}
|
|
485
|
+
return {
|
|
486
|
+
...result,
|
|
487
|
+
map: {
|
|
488
|
+
...map,
|
|
489
|
+
summary
|
|
490
|
+
}
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
function cmdHelp(command) {
|
|
265
494
|
const commandId = makeId('cmd');
|
|
266
495
|
const startedAt = utcNowIso();
|
|
267
496
|
const response = envelopeOk(commandId, startedAt, [], 'validate');
|
|
497
|
+
const commandHelp = command ? commandHelpText(command) : undefined;
|
|
268
498
|
response.data = {
|
|
269
|
-
usageText: helpText(),
|
|
270
|
-
commands: Array.from(ALL_COMMANDS),
|
|
271
|
-
examples: [
|
|
499
|
+
usageText: commandHelp?.usageText ?? helpText(),
|
|
500
|
+
commands: command ? [command] : Array.from(ALL_COMMANDS),
|
|
501
|
+
examples: commandHelp?.examples ?? [
|
|
272
502
|
'visor validate scenarios/checkout-smoke.json',
|
|
503
|
+
'visor run path/to/scenario.json --runtime local --output artifacts-local',
|
|
273
504
|
'visor start --server-url http://127.0.0.1:4723',
|
|
274
505
|
'visor run scenarios/checkout-smoke.json --output artifacts-test',
|
|
275
506
|
'visor scroll --device emulator-5554 --direction down',
|
|
@@ -313,6 +544,19 @@ export async function cmdValidate(parsed) {
|
|
|
313
544
|
return { code: 1, response };
|
|
314
545
|
}
|
|
315
546
|
}
|
|
547
|
+
async function cmdRunLocal(commandId, startedAt, scenario, warnings, runtime) {
|
|
548
|
+
const result = await runScenario(scenario, new LocalRuntimeAdapter(), runtime.device, runtime.timeout, runtime.output_dir);
|
|
549
|
+
const outputs = writeReports(result, runtime.output_dir);
|
|
550
|
+
const response = result.status === 'ok'
|
|
551
|
+
? envelopeOk(commandId, startedAt, Object.values(outputs), 'report')
|
|
552
|
+
: envelopeFail(commandId, startedAt, result.error?.code ?? 'ACTION_ERROR', result.error?.message ?? 'Local runtime scenario failed', result.error?.likely_cause ?? 'The deterministic local runtime did not satisfy the scenario', result.error?.next_step ?? 'Inspect local runtime scenario artifacts and retry');
|
|
553
|
+
response.artifacts = Object.values(outputs);
|
|
554
|
+
response.data = {
|
|
555
|
+
run: result,
|
|
556
|
+
warnings
|
|
557
|
+
};
|
|
558
|
+
return { code: result.status === 'ok' ? 0 : 2, response };
|
|
559
|
+
}
|
|
316
560
|
export async function cmdRun(parsed) {
|
|
317
561
|
const commandId = makeId('cmd');
|
|
318
562
|
const startedAt = utcNowIso();
|
|
@@ -323,6 +567,24 @@ export async function cmdRun(parsed) {
|
|
|
323
567
|
response.data = { issues };
|
|
324
568
|
return { code: 1, response };
|
|
325
569
|
}
|
|
570
|
+
if (parsed.options.runtime !== undefined &&
|
|
571
|
+
parsed.options.runtime !== 'appium' &&
|
|
572
|
+
parsed.options.runtime !== 'local') {
|
|
573
|
+
return unsupportedRuntimeResult(commandId, startedAt, parsed.options.runtime);
|
|
574
|
+
}
|
|
575
|
+
if (parsed.options.runtime === 'local') {
|
|
576
|
+
const runtime = {
|
|
577
|
+
device: 'local',
|
|
578
|
+
timeout: typeof parsed.options.timeout === 'number'
|
|
579
|
+
? parsed.options.timeout
|
|
580
|
+
: typeof scenario.config.timeoutMs === 'number'
|
|
581
|
+
? scenario.config.timeoutMs
|
|
582
|
+
: undefined,
|
|
583
|
+
output_dir: String(parsed.options.output ??
|
|
584
|
+
(typeof scenario.config.artifactsDir === 'string' ? scenario.config.artifactsDir : 'artifacts'))
|
|
585
|
+
};
|
|
586
|
+
return cmdRunLocal(commandId, startedAt, scenario, warningIssues(issues), runtime);
|
|
587
|
+
}
|
|
326
588
|
let runtime;
|
|
327
589
|
try {
|
|
328
590
|
runtime = await resolvedRuntime({
|
|
@@ -351,7 +613,10 @@ export async function cmdRun(parsed) {
|
|
|
351
613
|
device: runtime.device,
|
|
352
614
|
app_id: runtime.app_id,
|
|
353
615
|
attach_to_running: runtime.attach_to_running
|
|
354
|
-
}, scenario, runtime.device, runtime.timeout, runtime.output_dir
|
|
616
|
+
}, scenario, runtime.device, runtime.timeout, runtime.output_dir, {
|
|
617
|
+
...runtime.map,
|
|
618
|
+
appId: runtime.app_id
|
|
619
|
+
});
|
|
355
620
|
const outputs = writeReports(result, runtime.output_dir);
|
|
356
621
|
if (result.status === 'fail' && result.error) {
|
|
357
622
|
const response = envelopeFail(commandId, startedAt, result.error.code, result.error.message, result.error.likely_cause, result.error.next_step);
|
|
@@ -384,6 +649,51 @@ export async function cmdRun(parsed) {
|
|
|
384
649
|
return { code: 1, response };
|
|
385
650
|
}
|
|
386
651
|
}
|
|
652
|
+
export async function cmdDiscover(parsed) {
|
|
653
|
+
const commandId = makeId('cmd');
|
|
654
|
+
const startedAt = utcNowIso();
|
|
655
|
+
let runtime;
|
|
656
|
+
try {
|
|
657
|
+
runtime = await resolvedRuntime(parsed.options);
|
|
658
|
+
}
|
|
659
|
+
catch (error) {
|
|
660
|
+
if (!(error instanceof DeviceSelectionError)) {
|
|
661
|
+
throw error;
|
|
662
|
+
}
|
|
663
|
+
const response = envelopeFail(commandId, startedAt, 'TARGET_ERROR', 'Device selection failed', error.message, 'Start one target device or rerun with --device <device-id>');
|
|
664
|
+
response.data = { devices: error.devices };
|
|
665
|
+
return { code: 1, response };
|
|
666
|
+
}
|
|
667
|
+
try {
|
|
668
|
+
const result = await runDaemonDiscover({
|
|
669
|
+
platform: runtime.platform,
|
|
670
|
+
server_url: runtime.server_url,
|
|
671
|
+
device: runtime.device,
|
|
672
|
+
app_id: runtime.app_id,
|
|
673
|
+
attach_to_running: runtime.attach_to_running
|
|
674
|
+
}, {
|
|
675
|
+
...runtime.map,
|
|
676
|
+
appId: runtime.app_id
|
|
677
|
+
});
|
|
678
|
+
const response = envelopeOk(commandId, startedAt, [], 'run');
|
|
679
|
+
response.data = addDiscoverMapSummary(result);
|
|
680
|
+
return { code: 0, response };
|
|
681
|
+
}
|
|
682
|
+
catch (error) {
|
|
683
|
+
const isDaemonUnavailable = error instanceof DaemonUnavailableError;
|
|
684
|
+
const isDaemonTimeout = error instanceof DaemonRequestTimeoutError;
|
|
685
|
+
const response = envelopeFail(commandId, startedAt, isDaemonUnavailable || isDaemonTimeout ? 'TARGET_ERROR' : 'ACTION_ERROR', isDaemonUnavailable
|
|
686
|
+
? 'discover requires visor start'
|
|
687
|
+
: isDaemonTimeout
|
|
688
|
+
? 'discover timed out waiting for Visor daemon'
|
|
689
|
+
: 'discover failed', errorMessage(error), isDaemonUnavailable
|
|
690
|
+
? 'Run `visor start`, verify the target emulator/simulator is booted, and retry.'
|
|
691
|
+
: isDaemonTimeout
|
|
692
|
+
? 'Inspect .visor/daemon/daemon.log and .visor/appium/*.log, then retry or run `visor stop --force` before restarting.'
|
|
693
|
+
: 'Check target app state and retry');
|
|
694
|
+
return { code: 1, response };
|
|
695
|
+
}
|
|
696
|
+
}
|
|
387
697
|
export async function cmdBenchmark(parsed) {
|
|
388
698
|
const commandId = makeId('cmd');
|
|
389
699
|
const startedAt = utcNowIso();
|
|
@@ -417,39 +727,97 @@ export async function cmdBenchmark(parsed) {
|
|
|
417
727
|
}
|
|
418
728
|
const runs = typeof parsed.options.runs === 'number' ? parsed.options.runs : 20;
|
|
419
729
|
const threshold = typeof parsed.options.threshold === 'number' ? parsed.options.threshold : 95;
|
|
420
|
-
const
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
730
|
+
const runtimeRequest = {
|
|
731
|
+
platform: runtime.platform,
|
|
732
|
+
server_url: runtime.server_url,
|
|
733
|
+
device: runtime.device,
|
|
734
|
+
app_id: runtime.app_id,
|
|
735
|
+
attach_to_running: runtime.attach_to_running
|
|
736
|
+
};
|
|
737
|
+
const runVariant = async (name, mapEnabled, mapOptions) => {
|
|
738
|
+
const signatures = [];
|
|
739
|
+
const runIds = [];
|
|
740
|
+
let failures = 0;
|
|
741
|
+
for (let index = 0; index < runs; index += 1) {
|
|
742
|
+
try {
|
|
743
|
+
const result = await runDaemonScenario(runtimeRequest, scenario, runtime.device, runtime.timeout, runtime.output_dir, mapOptions);
|
|
744
|
+
writeReports(result, runtime.output_dir);
|
|
745
|
+
signatures.push(result.determinism_signature);
|
|
746
|
+
runIds.push(result.run_id);
|
|
747
|
+
if (result.status !== 'ok') {
|
|
748
|
+
failures += 1;
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
catch (error) {
|
|
752
|
+
if (error instanceof DaemonUnavailableError) {
|
|
753
|
+
const response = envelopeFail(commandId, startedAt, 'TARGET_ERROR', 'Benchmark requires visor start', errorMessage(error), 'Run `visor start`, verify the target emulator/simulator is booted, and retry.');
|
|
754
|
+
return { error: { code: 1, response } };
|
|
755
|
+
}
|
|
756
|
+
if (error instanceof DaemonRequestTimeoutError) {
|
|
757
|
+
const response = envelopeFail(commandId, startedAt, 'TARGET_ERROR', 'Timed out waiting for Visor daemon', errorMessage(error), 'Inspect .visor/daemon/daemon.log and .visor/appium/*.log, then retry or run `visor stop --force` before restarting.');
|
|
758
|
+
return { error: { code: 1, response } };
|
|
759
|
+
}
|
|
436
760
|
failures += 1;
|
|
437
761
|
}
|
|
438
762
|
}
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
763
|
+
const score = determinismCheck(signatures);
|
|
764
|
+
return {
|
|
765
|
+
variant: {
|
|
766
|
+
name,
|
|
767
|
+
mapEnabled,
|
|
768
|
+
runs,
|
|
769
|
+
threshold,
|
|
770
|
+
determinismScore: score,
|
|
771
|
+
pass: score >= threshold && failures === 0,
|
|
772
|
+
failures,
|
|
773
|
+
runIds
|
|
443
774
|
}
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
775
|
+
};
|
|
776
|
+
};
|
|
777
|
+
if (parsed.options['compare-map'] === true) {
|
|
778
|
+
const variants = [];
|
|
779
|
+
for (const variantConfig of [
|
|
780
|
+
{ name: 'no-map', mapEnabled: false },
|
|
781
|
+
{ name: 'map', mapEnabled: true }
|
|
782
|
+
]) {
|
|
783
|
+
const outcome = await runVariant(variantConfig.name, variantConfig.mapEnabled, {
|
|
784
|
+
...runtime.map,
|
|
785
|
+
enabled: variantConfig.mapEnabled,
|
|
786
|
+
appId: runtime.app_id
|
|
787
|
+
});
|
|
788
|
+
if ('error' in outcome) {
|
|
789
|
+
return outcome.error;
|
|
447
790
|
}
|
|
448
|
-
|
|
791
|
+
variants.push(outcome.variant);
|
|
449
792
|
}
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
793
|
+
const failures = variants.reduce((total, variant) => total + Number(variant.failures ?? 0), 0);
|
|
794
|
+
const score = Math.min(...variants.map((variant) => Number(variant.determinismScore ?? 0)));
|
|
795
|
+
const passGate = variants.every((variant) => variant.pass === true);
|
|
796
|
+
const runIds = variants.flatMap((variant) => Array.isArray(variant.runIds) ? variant.runIds : []);
|
|
797
|
+
const response = envelopeOk(commandId, startedAt, [], 'report');
|
|
798
|
+
response.data = {
|
|
799
|
+
runs,
|
|
800
|
+
threshold,
|
|
801
|
+
determinismScore: score,
|
|
802
|
+
pass: passGate,
|
|
803
|
+
failures,
|
|
804
|
+
runIds,
|
|
805
|
+
variants,
|
|
806
|
+
warnings: warningIssues(issues)
|
|
807
|
+
};
|
|
808
|
+
return { code: passGate ? 0 : 3, response };
|
|
809
|
+
}
|
|
810
|
+
const outcome = await runVariant('benchmark', runtime.map.enabled !== false, {
|
|
811
|
+
...runtime.map,
|
|
812
|
+
appId: runtime.app_id
|
|
813
|
+
});
|
|
814
|
+
if ('error' in outcome) {
|
|
815
|
+
return outcome.error;
|
|
816
|
+
}
|
|
817
|
+
const score = Number(outcome.variant.determinismScore ?? 0);
|
|
818
|
+
const failures = Number(outcome.variant.failures ?? 0);
|
|
819
|
+
const passGate = outcome.variant.pass === true;
|
|
820
|
+
const runIds = Array.isArray(outcome.variant.runIds) ? outcome.variant.runIds : [];
|
|
453
821
|
const response = envelopeOk(commandId, startedAt, [], 'report');
|
|
454
822
|
response.data = {
|
|
455
823
|
runs,
|
|
@@ -499,7 +867,10 @@ export async function cmdAction(command, parsed) {
|
|
|
499
867
|
device: options.device,
|
|
500
868
|
app_id: options.app_id,
|
|
501
869
|
attach_to_running: options.attach_to_running
|
|
502
|
-
}, command, actionArgs(command, parsed.options)
|
|
870
|
+
}, command, actionArgs(command, parsed.options), {
|
|
871
|
+
...options.map,
|
|
872
|
+
appId: options.app_id
|
|
873
|
+
});
|
|
503
874
|
const actionPayload = payload.args;
|
|
504
875
|
if (actionPayload && typeof actionPayload === 'object' && !Array.isArray(actionPayload)) {
|
|
505
876
|
const maybePath = actionPayload.path;
|
|
@@ -509,6 +880,9 @@ export async function cmdAction(command, parsed) {
|
|
|
509
880
|
}
|
|
510
881
|
}
|
|
511
882
|
catch (error) {
|
|
883
|
+
if (error instanceof DaemonOperationError && error.data) {
|
|
884
|
+
payload = error.data;
|
|
885
|
+
}
|
|
512
886
|
actionError = error;
|
|
513
887
|
}
|
|
514
888
|
if (actionError) {
|
|
@@ -573,10 +947,12 @@ export async function cmdStop(parsed) {
|
|
|
573
947
|
}
|
|
574
948
|
}
|
|
575
949
|
export async function executeCommand(argv) {
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
argv.
|
|
579
|
-
|
|
950
|
+
const requestedHelp = argv[0] === 'help' || argv.includes('--help') || argv.includes('-h');
|
|
951
|
+
if (requestedHelp) {
|
|
952
|
+
const helpCommand = argv.find((token) => ALL_COMMANDS.has(token));
|
|
953
|
+
return cmdHelp(helpCommand);
|
|
954
|
+
}
|
|
955
|
+
if (argv.length === 0) {
|
|
580
956
|
return cmdHelp();
|
|
581
957
|
}
|
|
582
958
|
if (argv.includes('--version') || argv.includes('-v')) {
|
|
@@ -589,6 +965,9 @@ export async function executeCommand(argv) {
|
|
|
589
965
|
if (parsed.command === 'run') {
|
|
590
966
|
return cmdRun(parsed);
|
|
591
967
|
}
|
|
968
|
+
if (parsed.command === 'discover') {
|
|
969
|
+
return cmdDiscover(parsed);
|
|
970
|
+
}
|
|
592
971
|
if (parsed.command === 'benchmark') {
|
|
593
972
|
return cmdBenchmark(parsed);
|
|
594
973
|
}
|