@e2e-dev/mobile 0.8.0-canary-20260928184528 → 0.8.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/dist/surface.d.ts CHANGED
@@ -12,7 +12,7 @@
12
12
  * scrolls the whole screen.
13
13
  */
14
14
  import { type EngineAttemptContext, type EngineCleanupContext, type EngineInitInfo, type EngineObserveOptions, type EngineSnapshot, type LocatorAction, type LocatorExpression, type NodeRef, type OperationContext, type PointerAction, type VideoSegment, type SemanticNode, type ViewportPoint } from 'e2e/engine';
15
- import type { AgentDeviceClient, ClientFactory, DevicePermission, LaunchPermissions, MobileOptions, PermissionState } from './options.ts';
15
+ import { type AgentDeviceClient, type ClientFactory, type DevicePermission, type LaunchPermissions, type MobileOptions, type PermissionState } from './options.ts';
16
16
  import { DevicePool } from './pool.ts';
17
17
  /** How `installApp` puts a build on the device. */
18
18
  export interface InstallAppOptions {
@@ -36,14 +36,14 @@ export interface OpenAppOptions {
36
36
  readonly relaunch?: boolean;
37
37
  /**
38
38
  * Arguments this launch hands the app: its process arguments on iOS,
39
- * `am start` arguments on Android. Replaces the engine's `launchArguments`
40
- * for a relaunch of the pinned app.
39
+ * `am start` arguments on Android. Replaces the target's
40
+ * `app.launchArguments` for a relaunch of the pinned app.
41
41
  */
42
42
  readonly launchArguments?: readonly string[];
43
43
  /**
44
44
  * Permissions the app holds before this launch, each granted, denied, or
45
- * reset. Replaces the engine's `permissions` for a relaunch of the pinned
46
- * app.
45
+ * reset. Replaces the target's `app.permissions` for a relaunch of the
46
+ * pinned app.
47
47
  */
48
48
  readonly permissions?: LaunchPermissions;
49
49
  }
@@ -52,6 +52,15 @@ type SettleOptions = {
52
52
  readonly settle: true;
53
53
  readonly settleQuietMs: number;
54
54
  } | Record<never, never>;
55
+ /**
56
+ * Refuses a permissions map before any device command: anything but a plain
57
+ * object, a name agent-device
58
+ * does not set, naming the nearest, or a state other than grant, deny, or
59
+ * reset. `INVALID_CONFIG` for a target's `app.permissions`, `INVALID_ARGUMENT`
60
+ * for a test's `device.openApp`. An `undefined` state is skipped, as the preset
61
+ * skips it.
62
+ */
63
+ export declare function assertPermissions(label: string, permissions: unknown, code: 'INVALID_CONFIG' | 'INVALID_ARGUMENT'): void;
55
64
  export declare class AgentDeviceSurface {
56
65
  readonly options: MobileOptions;
57
66
  private readonly createClient;
@@ -73,10 +82,14 @@ export declare class AgentDeviceSurface {
73
82
  private sessionApp;
74
83
  /** The app the build `appPath` installed, once `init` has, itself or through a lease. */
75
84
  private installedApp;
85
+ /** The target's app, as `init` received it. */
86
+ private app;
76
87
  /** Where relative build paths resolve; the run's project root once init has told us. */
77
88
  private projectRoot;
78
89
  /** The session and device this worker drives, for the error messages whose recovery is per device; set in init. */
79
90
  private where;
91
+ /** How this worker's device records through its provider: set in init when the pool's provider records and the slot rides one of its leases. */
92
+ private leaseRecorder;
80
93
  /**
81
94
  * Commands still running on the device. agent-device takes no abort
82
95
  * signal, so a cancelled or timed-out call is only abandoned by its
@@ -104,10 +117,12 @@ export declare class AgentDeviceSurface {
104
117
  */
105
118
  private knownViewport;
106
119
  constructor(options: MobileOptions, createClient: ClientFactory);
107
- /** Whether the manifest declares app restart and state clearing. */
108
- get managesApp(): boolean;
109
- /** The app `app.open()` launches: the `app` option, else the build `appPath` installed. */
120
+ /** Whether the target pins an app for `app.open()`, `app.restart()`, and `app.clearState()` to launch. */
121
+ private get managesApp();
122
+ /** The app `app.open()` launches: the target's `bundleId`, else the app its build `appPath` installed. */
110
123
  get pinnedApp(): string | undefined;
124
+ /** The build the target's `app.appPath` names, as declared. */
125
+ get appPath(): string | undefined;
111
126
  /** Whether an attempt is running on this surface right now. */
112
127
  get attemptRunning(): boolean;
113
128
  /** The live client; INVALID_STATE before init or after dispose. */
@@ -152,11 +167,29 @@ export declare class AgentDeviceSurface {
152
167
  /**
153
168
  * Asks the device to record its screen into the attempt directory. Taps stay
154
169
  * visible in the recording (agent-device's touch indicator), which is the
155
- * closest a phone comes to a cursor.
170
+ * closest a phone comes to a cursor, unless `videoTouches` is `false`. A
171
+ * device provider that records its devices records the attempt instead,
172
+ * when the slot rides one of its leases.
156
173
  */
157
174
  startVideo(operation: OperationContext): Promise<void>;
158
175
  /** Stops the recording and returns its one segment, or none when the device wrote nothing. */
159
176
  stopVideo(operation: OperationContext): Promise<readonly VideoSegment[]>;
177
+ /**
178
+ * Stops the device's screen recording. A device-scope recording started
179
+ * with no session made one of its own, and stopping it ends that session,
180
+ * whatever the test opened in it since, so the session is then as
181
+ * `closeApp` leaves it.
182
+ */
183
+ private stopScreenRecording;
184
+ /**
185
+ * Starts the device provider's recording of the leased device. Marked
186
+ * the moment `record` is called, before it settles, as the screen
187
+ * recording is before its start command: a start that outlives its budget
188
+ * may still come up, and `endAttempt` then stops it.
189
+ */
190
+ private startLeaseRecording;
191
+ /** Ends the provider's recording as the attempt's one segment, a file in its `video` directory or a link. */
192
+ private stopLeaseRecording;
160
193
  dispose(context: EngineCleanupContext): Promise<void>;
161
194
  /**
162
195
  * Opens an app in the session, remembering its identity for the path
@@ -1 +1 @@
1
- {"version":3,"file":"surface.d.ts","sourceRoot":"","sources":["../src/surface.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"surface.d.ts","sourceRoot":"","sources":["../src/surface.ts"],"names":[],"mappings":"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"}
package/dist/surface.js CHANGED
@@ -14,18 +14,20 @@
14
14
  import { existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
15
15
  import { tmpdir } from 'node:os';
16
16
  import path from 'node:path';
17
- import { EngineError, KEY_NAMES, parseKey, raceAbort, withinCleanupBudget, ConfigurationError, TestError, } from 'e2e/engine';
17
+ import { EngineError, KEY_NAMES, parseKey, raceAbort, rejectUnknownKeys, stopProviderRecording, withinCleanupBudget, ConfigurationError, TestError, } from 'e2e/engine';
18
18
  import { isNoSessionApp, isSnapshotPresentationFailure, runCommand, staleOr } from './errors.js';
19
19
  import { pointerInteraction, DEFAULT_LONG_PRESS_MS } from './actions.js';
20
20
  import { resolveExpression } from './locate.js';
21
21
  import { isWithin, projectSnapshot, ROOT_ID, screenRoot, screenTitle, } from './nodes.js';
22
+ import { DEVICE_PERMISSIONS } from './options.js';
22
23
  import { maskPng } from './png.js';
23
24
  import { deviceLabel, pinnedApp } from './bindings.js';
24
- import { assertAppId, assertConfiguredApp } from './links.js';
25
+ import { assertAppId } from './links.js';
25
26
  import { DevicePool, deviceSelection } from './pool.js';
27
+ import { recordLease, travelledLease } from './provider.js';
26
28
  import { invalidState, notActionable, logicalScreenSize, readPngSize, sanitizeFilename, screenLocation, swipeWithin, unsupported, } from './support.js';
27
29
  /** Why nothing is pinned with `appPath` alone: the install is the suite's. */
28
- const UNINSTALLED_BUILD = 'the build `appPath` names installed first with `device.installApp()`';
30
+ const UNINSTALLED_BUILD = "the build the target's `app.appPath` names installed first with `device.installApp()`";
29
31
  /** Platform element types that are the soft keyboard or one of its keys, as agent-device names them. */
30
32
  const KEYBOARD_TYPES = new Set(['keyboard', 'key']);
31
33
  /**
@@ -109,6 +111,42 @@ function sleep(ms, signal) {
109
111
  });
110
112
  }
111
113
  const DEFAULT_SETTLE_QUIET_MS = 150;
114
+ /** Every option `mobile()` takes, kept equal to `MobileOptions` by the compiler. */
115
+ const MOBILE_OPTION_KEYS = Object.keys({
116
+ platform: true,
117
+ device: true,
118
+ session: true,
119
+ snapshot: true,
120
+ settle: true,
121
+ transition: true,
122
+ videoTouches: true,
123
+ });
124
+ /** Every state a permission takes, kept equal to `PermissionState` by the compiler. */
125
+ const PERMISSION_STATES = Object.keys({ grant: true, deny: true, reset: true });
126
+ /**
127
+ * Refuses a permissions map before any device command: anything but a plain
128
+ * object, a name agent-device
129
+ * does not set, naming the nearest, or a state other than grant, deny, or
130
+ * reset. `INVALID_CONFIG` for a target's `app.permissions`, `INVALID_ARGUMENT`
131
+ * for a test's `device.openApp`. An `undefined` state is skipped, as the preset
132
+ * skips it.
133
+ */
134
+ export function assertPermissions(label, permissions, code) {
135
+ const fail = (message) => {
136
+ throw code === 'INVALID_CONFIG' ? new ConfigurationError(code, message) : new TestError(code, message);
137
+ };
138
+ // A Map or class instance carries no own keys to check, and the preset would apply none of it.
139
+ const prototype = typeof permissions === 'object' && permissions !== null ? Object.getPrototypeOf(permissions) : undefined;
140
+ if (prototype !== Object.prototype && prototype !== null) {
141
+ fail(`${label} must be a plain object of permission names to ${PERMISSION_STATES.join(', ')}`);
142
+ }
143
+ rejectUnknownKeys(label, permissions, DEVICE_PERMISSIONS, code);
144
+ for (const [name, state] of Object.entries(permissions)) {
145
+ if (state !== undefined && !PERMISSION_STATES.includes(state)) {
146
+ fail(`${label}.${name} must be one of ${PERMISSION_STATES.join(', ')}, got ${typeof state === 'string' ? `"${state}"` : String(state)}`);
147
+ }
148
+ }
149
+ }
112
150
  /** Resolves the `transition` option: the default budget or a custom one. */
113
151
  function transitionMs(transition) {
114
152
  const budget = transition ?? DEFAULT_TRANSITION_MS;
@@ -148,10 +186,14 @@ export class AgentDeviceSurface {
148
186
  sessionApp;
149
187
  /** The app the build `appPath` installed, once `init` has, itself or through a lease. */
150
188
  installedApp;
189
+ /** The target's app, as `init` received it. */
190
+ app = {};
151
191
  /** Where relative build paths resolve; the run's project root once init has told us. */
152
192
  projectRoot = process.cwd();
153
193
  /** The session and device this worker drives, for the error messages whose recovery is per device; set in init. */
154
194
  where;
195
+ /** How this worker's device records through its provider: set in init when the pool's provider records and the slot rides one of its leases. */
196
+ leaseRecorder;
155
197
  /**
156
198
  * Commands still running on the device. agent-device takes no abort
157
199
  * signal, so a cancelled or timed-out call is only abandoned by its
@@ -181,18 +223,22 @@ export class AgentDeviceSurface {
181
223
  constructor(options, createClient) {
182
224
  this.options = options;
183
225
  this.createClient = createClient;
184
- assertConfiguredApp(options.app);
226
+ rejectUnknownKeys('mobile()', options, MOBILE_OPTION_KEYS);
185
227
  this.pool = new DevicePool(options, createClient);
186
228
  this.settleOptions = settleOptions(options.settle);
187
229
  this.transitionMs = transitionMs(options.transition);
188
230
  }
189
- /** Whether the manifest declares app restart and state clearing. */
231
+ /** Whether the target pins an app for `app.open()`, `app.restart()`, and `app.clearState()` to launch. */
190
232
  get managesApp() {
191
- return this.options.app !== undefined || this.options.appPath !== undefined;
233
+ return this.app.bundleId !== undefined || this.app.appPath !== undefined;
192
234
  }
193
- /** The app `app.open()` launches: the `app` option, else the build `appPath` installed. */
235
+ /** The app `app.open()` launches: the target's `bundleId`, else the app its build `appPath` installed. */
194
236
  get pinnedApp() {
195
- return pinnedApp(this.options, this.installedApp);
237
+ return pinnedApp(this.app, this.installedApp);
238
+ }
239
+ /** The build the target's `app.appPath` names, as declared. */
240
+ get appPath() {
241
+ return this.app.appPath;
196
242
  }
197
243
  /** Whether an attempt is running on this surface right now. */
198
244
  get attemptRunning() {
@@ -249,12 +295,19 @@ export class AgentDeviceSurface {
249
295
  }
250
296
  async init(info) {
251
297
  this.projectRoot = info.projectRoot;
298
+ this.app = info.app;
252
299
  const binding = this.pool.binding(info.targetName, info.workerSlot, info.env);
253
300
  this.device = binding;
254
301
  const session = this.pool.session(info.targetName, info.workerSlot);
255
302
  const label = deviceLabel(binding);
256
303
  this.where = `session ${session}${label === undefined ? '' : ` on ${label}`}`;
257
304
  this.client ??= this.createClient(session, binding);
305
+ const provider = this.pool.recorder;
306
+ const lease = travelledLease(binding);
307
+ this.leaseRecorder =
308
+ provider === undefined || lease === undefined
309
+ ? undefined
310
+ : { provider, lease, runId: info.runId, targetName: info.targetName, env: info.env };
258
311
  await this.command('boot', (client) => client.devices.boot(this.selection()), info.signal);
259
312
  this.sessionApp = binding?.sessionApp === undefined ? undefined : await this.resumedSessionApp(session, binding.sessionApp, info.signal);
260
313
  // Nothing is installed here: a device provider that installed the build
@@ -290,12 +343,13 @@ export class AgentDeviceSurface {
290
343
  // worker's `prepare`, left it, and a test that wants it fresh calls
291
344
  // `app.open()`, which is `restart` below. The bindings are forgotten, the
292
345
  // screen is not, so a control still in place is acted on at once.
293
- this.attempt = { artifactsDir: context.artifactsDir, screenshots: 0, video: undefined };
346
+ this.attempt = { attemptId: context.attemptId, artifactsDir: context.artifactsDir, screenshots: 0, video: undefined };
294
347
  this.generation = new Map();
295
348
  this.located.clear();
296
349
  }
297
350
  async endAttempt(context) {
298
- const dangling = this.attempt?.video;
351
+ const attempt = this.attempt;
352
+ const dangling = attempt?.video;
299
353
  this.attempt = undefined;
300
354
  this.generation = new Map();
301
355
  this.located.clear();
@@ -304,16 +358,21 @@ export class AgentDeviceSurface {
304
358
  // and the device must not keep recording into the next one. A start that
305
359
  // outlived its budget may still be landing: it settles first, so the stop
306
360
  // cannot overtake it.
307
- if (dangling !== undefined) {
361
+ if (attempt !== undefined && dangling?.kind === 'lease') {
362
+ await this.stopLeaseRecording(dangling, attempt.artifactsDir, context.signal).catch(() => undefined);
363
+ }
364
+ else if (dangling !== undefined) {
308
365
  await this.settleInflight(context.signal).catch(() => undefined);
309
- await this.command('stop video recording', (client) => client.recording.record({ action: 'stop' }), context.signal).catch(() => undefined);
366
+ await this.stopScreenRecording(context.signal).catch(() => undefined);
310
367
  }
311
368
  }
312
369
  // --- video ---
313
370
  /**
314
371
  * Asks the device to record its screen into the attempt directory. Taps stay
315
372
  * visible in the recording (agent-device's touch indicator), which is the
316
- * closest a phone comes to a cursor.
373
+ * closest a phone comes to a cursor, unless `videoTouches` is `false`. A
374
+ * device provider that records its devices records the attempt instead,
375
+ * when the slot rides one of its leases.
317
376
  */
318
377
  async startVideo(operation) {
319
378
  const attempt = this.attempt;
@@ -321,14 +380,26 @@ export class AgentDeviceSurface {
321
380
  throw invalidState('startVideo outside an attempt');
322
381
  if (attempt.video !== undefined)
323
382
  throw invalidState('a video is already recording');
383
+ if (this.leaseRecorder !== undefined) {
384
+ await this.startLeaseRecording(attempt, this.leaseRecorder, operation.signal);
385
+ return;
386
+ }
324
387
  const relative = path.join('video', 'video.mp4');
325
388
  const absolute = path.join(attempt.artifactsDir, relative);
326
389
  mkdirSync(path.dirname(absolute), { recursive: true });
327
390
  // Marked before the device is asked: a start that outlives its budget
328
391
  // still records, and `endAttempt` must be able to stop it.
329
- const recording = { relative, absolute, startedAt: new Date().toISOString() };
392
+ const recording = { kind: 'screen', relative, absolute, startedAt: new Date().toISOString() };
330
393
  attempt.video = recording;
331
- await this.command('start video recording', (client) => client.recording.record({ action: 'start', path: absolute, quality: 'medium' }), operation.signal);
394
+ await this.command('start video recording',
395
+ // The whole screen: an app scope refuses to start while no app is open, before `app.open()` and after `closeApp()`.
396
+ (client) => client.recording.record({
397
+ action: 'start',
398
+ path: absolute,
399
+ quality: 'medium',
400
+ recordingScope: 'device',
401
+ ...(this.options.videoTouches === false ? { hideTouches: true } : {}),
402
+ }), operation.signal);
332
403
  // The device confirmed: it is recording from about now.
333
404
  recording.startedAt = new Date().toISOString();
334
405
  }
@@ -340,7 +411,13 @@ export class AgentDeviceSurface {
340
411
  const video = attempt.video;
341
412
  if (video === undefined)
342
413
  return [];
343
- const result = await this.command('stop video recording', (client) => client.recording.record({ action: 'stop' }), operation.signal);
414
+ if (video.kind === 'lease') {
415
+ const segment = await this.stopLeaseRecording(video, attempt.artifactsDir, operation.signal);
416
+ // Cleared only now: a stop that failed leaves the recording for `endAttempt`.
417
+ attempt.video = undefined;
418
+ return [segment];
419
+ }
420
+ const result = await this.stopScreenRecording(operation.signal);
344
421
  // Cleared only now: a stop that failed leaves the recording for `endAttempt`.
345
422
  attempt.video = undefined;
346
423
  // The device may finalize the file under a path of its own choosing; the
@@ -352,8 +429,47 @@ export class AgentDeviceSurface {
352
429
  return [];
353
430
  return [{ path: video.relative, startedAt: video.startedAt }];
354
431
  }
432
+ /**
433
+ * Stops the device's screen recording. A device-scope recording started
434
+ * with no session made one of its own, and stopping it ends that session,
435
+ * whatever the test opened in it since, so the session is then as
436
+ * `closeApp` leaves it.
437
+ */
438
+ async stopScreenRecording(signal) {
439
+ const result = await this.command('stop video recording', (client) => client.recording.record({ action: 'stop' }), signal);
440
+ if (result.recordOnlySession === true) {
441
+ this.sessionApp = undefined;
442
+ this.screenReplaced();
443
+ }
444
+ return result;
445
+ }
446
+ /**
447
+ * Starts the device provider's recording of the leased device. Marked
448
+ * the moment `record` is called, before it settles, as the screen
449
+ * recording is before its start command: a start that outlives its budget
450
+ * may still come up, and `endAttempt` then stops it.
451
+ */
452
+ async startLeaseRecording(attempt, recorder, signal) {
453
+ const { provider, lease, runId, targetName, env } = recorder;
454
+ const started = recordLease(provider, lease, { runId, targetName, attemptId: attempt.attemptId, env, signal });
455
+ attempt.video = { kind: 'lease', recorder, started };
456
+ await raceAbort(started, signal, 'start video recording');
457
+ }
458
+ /** Ends the provider's recording as the attempt's one segment, a file in its `video` directory or a link. */
459
+ async stopLeaseRecording(video, artifactsDir, signal) {
460
+ const { provider, lease } = video.recorder;
461
+ video.stopping ??= (async () => stopProviderRecording(await video.started, { artifactsDir, provider: `device provider "${provider.name}"`, leaseId: lease.id, signal }))().finally(() => {
462
+ video.stopping = undefined;
463
+ });
464
+ return raceAbort(video.stopping, signal, 'stop video recording');
465
+ }
355
466
  async dispose(context) {
356
467
  const client = this.client;
468
+ const attempt = this.attempt;
469
+ // A worker torn down mid-attempt must not leave the provider recording the device.
470
+ if (attempt?.video?.kind === 'lease') {
471
+ await withinCleanupBudget(this.stopLeaseRecording(attempt.video, attempt.artifactsDir, context.signal), context).catch(() => undefined);
472
+ }
357
473
  this.client = undefined;
358
474
  this.attempt = undefined;
359
475
  this.generation = new Map();
@@ -374,13 +490,15 @@ export class AgentDeviceSurface {
374
490
  */
375
491
  async openApp(app, options, signal) {
376
492
  assertAppId(app);
493
+ if (options.permissions !== undefined)
494
+ assertPermissions('device.openApp({ permissions })', options.permissions, 'INVALID_ARGUMENT');
377
495
  const relaunch = options.relaunch === true;
378
496
  // The engine's own launch options belong to a fresh launch of the pinned
379
497
  // app. A foreground-only open of a running app takes no arguments, and a
380
498
  // permission change there would terminate the app it means to keep.
381
499
  const configured = relaunch && app === this.pinnedApp;
382
- const launchArguments = options.launchArguments ?? (configured ? this.options.launchArguments : undefined);
383
- const permissions = options.permissions ?? (configured ? this.options.permissions : undefined);
500
+ const launchArguments = options.launchArguments ?? (configured ? this.app.launchArguments : undefined);
501
+ const permissions = options.permissions ?? (configured ? this.app.permissions : undefined);
384
502
  if (permissions !== undefined)
385
503
  await this.presetPermissions(app, permissions, signal);
386
504
  await this.open(app, relaunch, launchArguments, signal);
@@ -456,7 +574,7 @@ export class AgentDeviceSurface {
456
574
  async openLink(url, app, signal) {
457
575
  const target = app ?? this.pinnedApp;
458
576
  if (target === undefined && this.options.platform === 'ios') {
459
- throw new TestError('INVALID_ARGUMENT', 'openLink needs an app on iOS: pass `app`, or pin one with the engine option `app` or `appPath`; the session observes the app a link is opened into');
577
+ throw new TestError('INVALID_ARGUMENT', "openLink needs an app on iOS: pass `app`, or pin one with the target's app.bundleId or app.appPath; the session observes the app a link is opened into");
460
578
  }
461
579
  const result = await this.command('device.openLink', (client) => client.apps.open({
462
580
  ...this.selection(),
@@ -506,15 +624,15 @@ export class AgentDeviceSurface {
506
624
  * with no data; a plain install replaces the binary and keeps its data.
507
625
  */
508
626
  async installApp(appPath, options, signal) {
509
- const build = appPath ?? this.options.appPath;
627
+ const build = appPath ?? this.app.appPath;
510
628
  if (build === undefined)
511
- throw invalidState('installApp needs a build: pass a path, or name one with the engine option `appPath`');
629
+ throw invalidState("installApp needs a build: pass a path, or name one with the target's app.appPath");
512
630
  const resolved = path.resolve(this.projectRoot, build);
513
631
  const selection = this.selection();
514
- const engineBuild = appPath === undefined || (this.options.appPath !== undefined && resolved === path.resolve(this.projectRoot, this.options.appPath));
515
- const app = options.app ?? (engineBuild ? this.options.app : undefined) ?? (options.reinstall === true ? this.pinnedApp : undefined);
632
+ const engineBuild = appPath === undefined || (this.app.appPath !== undefined && resolved === path.resolve(this.projectRoot, this.app.appPath));
633
+ const app = options.app ?? (engineBuild ? this.app.bundleId : undefined) ?? (options.reinstall === true ? this.pinnedApp : undefined);
516
634
  if (options.reinstall === true && app === undefined) {
517
- throw invalidState('reinstall needs an app: pass `app`, or pin one with the engine option `app` or `appPath`');
635
+ throw invalidState("reinstall needs an app: pass `app`, or pin one with the target's app.bundleId or app.appPath");
518
636
  }
519
637
  const result = (await this.command(`install ${resolved}`, (client) => options.reinstall === true && app !== undefined
520
638
  ? client.apps.reinstall({ ...selection, app, appPath: resolved })
@@ -1015,14 +1133,14 @@ export class AgentDeviceSurface {
1015
1133
  async restart(operation) {
1016
1134
  const app = this.pinnedApp;
1017
1135
  if (app === undefined)
1018
- throw unsupported(`app.restart needs the engine option \`app\`, or ${UNINSTALLED_BUILD}`);
1136
+ throw unsupported(`app.restart needs the target's \`app.bundleId\`, or ${UNINSTALLED_BUILD}`);
1019
1137
  await this.openApp(app, { relaunch: true }, operation.signal);
1020
1138
  }
1021
1139
  /** Clears the pinned app's persisted state and relaunches it: the device equivalent of a fresh context. */
1022
1140
  async reset(operation) {
1023
1141
  const app = this.pinnedApp;
1024
1142
  if (app === undefined)
1025
- throw unsupported(`app.clearState needs the engine option \`app\`, or ${UNINSTALLED_BUILD}`);
1143
+ throw unsupported(`app.clearState needs the target's \`app.bundleId\`, or ${UNINSTALLED_BUILD}`);
1026
1144
  await this.command('clear app state', (client) => client.settings.update({ setting: 'clear-app-state', state: 'clear', app }), operation.signal);
1027
1145
  await this.openApp(app, { relaunch: true }, operation.signal);
1028
1146
  }