@loadmill/droid-cua 2.19.0 → 2.21.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 CHANGED
@@ -160,6 +160,24 @@ LAMBDATEST_USERNAME=... LAMBDATEST_ACCESS_KEY=... droid-cua \
160
160
  --app ./app/build/outputs/apk/debug/app-debug.apk \
161
161
  --instructions tests/login.dcua \
162
162
  --config ci/droid-cua.json
163
+
164
+ # List AWS Device Farm remote-access devices
165
+ AWS_DEVICE_FARM_ACCESS_KEY_ID=... \
166
+ AWS_DEVICE_FARM_SECRET_ACCESS_KEY=... \
167
+ AWS_DEVICE_FARM_PROJECT_ARN=... \
168
+ droid-cua --device-source aws-device-farm --list-devices
169
+
170
+ # Headless AWS Device Farm real-device run
171
+ AWS_DEVICE_FARM_ACCESS_KEY_ID=... \
172
+ AWS_DEVICE_FARM_SECRET_ACCESS_KEY=... \
173
+ AWS_DEVICE_FARM_PROJECT_ARN=... \
174
+ droid-cua \
175
+ --device-source aws-device-farm \
176
+ --platform android \
177
+ --device-name "Google Pixel 10 Pro" \
178
+ --os-version "16" \
179
+ --app ./app/build/outputs/apk/debug/app-debug.apk \
180
+ --instructions tests/login.dcua
163
181
  ```
164
182
 
165
183
  Example headless config:
@@ -194,6 +212,7 @@ Supported CLI options include:
194
212
  - `--device-name`
195
213
  - `--os-version`
196
214
  - `--app`
215
+ - `--list-devices`
197
216
  - `--browser`
198
217
  - `--instructions`
199
218
  - `--config`
@@ -246,6 +265,20 @@ LambdaTest CLI runs:
246
265
  - The CLI uploads the app to LambdaTest for each run and closes the cloud session when the run finishes or fails.
247
266
  - Each CLI LambdaTest run gets a unique LambdaTest build name so repeated local runs are easy to distinguish in the LambdaTest UI.
248
267
 
268
+ AWS Device Farm CLI runs:
269
+ - Use `--device-source aws-device-farm` with `--platform`, `--device-name`, `--os-version`, `--app`, and `--instructions`.
270
+ - Set `AWS_DEVICE_FARM_ACCESS_KEY_ID`, `AWS_DEVICE_FARM_SECRET_ACCESS_KEY`, and `AWS_DEVICE_FARM_PROJECT_ARN` in the environment (an IAM user with Device Farm access and the ARN of a Device Farm project in `us-west-2`).
271
+ - `--device-source aws-device-farm --list-devices` prints the remote-access device catalog (name, platform, OS) so you can pick valid `--device-name`/`--os-version` values, then exits.
272
+ - Android runs require an `.apk`; iOS runs require an `.ipa`. `.app` bundles are not supported.
273
+ - Re-running with the same app binary skips the upload: uploads are cached by checksum and reused for up to 30 days (Device Farm's upload retention).
274
+ - Device provisioning takes ~1 minute; the CLI shows live progress while it waits.
275
+ - The session is stopped when the run finishes, fails, or is interrupted with Ctrl-C — billing stops as soon as the stop is requested, so the CLI does not wait for the session to fully wind down.
276
+
277
+ AWS Device Farm in the desktop app:
278
+ - Open **Integrations → AWS Device Farm** and enter an AWS Access Key ID + Secret Access Key. The keys are validated on connect, a `droid-cua` Device Farm project is created automatically (the IAM user needs the minimal Device Farm policy, including `ListProjects`/`CreateProject`), and the remote-access device catalog loads. Credentials are stored in plaintext `desktop-config.json`, the same as BrowserStack/LambdaTest.
279
+ - On the Devices page, pick AWS Device Farm as the source, choose a device + an uploaded `.apk`/`.ipa`, and start a Design/Run session. Provisioning takes ~1 minute and live progress flows into the device log.
280
+ - **Pause caveat:** a Device Farm session paused for more than 5 minutes may be ended by AWS and need reconnecting.
281
+
249
282
  Minimal GitHub Actions shape:
250
283
  ```yaml
251
284
  jobs:
@@ -273,6 +306,25 @@ jobs:
273
306
  LAMBDATEST_ACCESS_KEY: ${{ secrets.LAMBDATEST_ACCESS_KEY }}
274
307
  ```
275
308
 
309
+ The same shape works for AWS Device Farm — swap the run step and env:
310
+ ```yaml
311
+ - run: |
312
+ droid-cua \
313
+ --device-source aws-device-farm \
314
+ --platform android \
315
+ --device-name "Google Pixel 10 Pro" \
316
+ --os-version "16" \
317
+ --app ./app/build/outputs/apk/debug/app-debug.apk \
318
+ --instructions tests/login.dcua \
319
+ --config ci/droid-cua.json \
320
+ --debug
321
+ env:
322
+ OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
323
+ AWS_DEVICE_FARM_ACCESS_KEY_ID: ${{ secrets.AWS_DEVICE_FARM_ACCESS_KEY_ID }}
324
+ AWS_DEVICE_FARM_SECRET_ACCESS_KEY: ${{ secrets.AWS_DEVICE_FARM_SECRET_ACCESS_KEY }}
325
+ AWS_DEVICE_FARM_PROJECT_ARN: ${{ secrets.AWS_DEVICE_FARM_PROJECT_ARN }}
326
+ ```
327
+
276
328
  Example without a config file:
277
329
  ```sh
278
330
  droid-cua \
package/build/index.js CHANGED
@@ -24,8 +24,11 @@ import { listHeadlessInstructionFiles, resolveHeadlessInstructionsTarget } from
24
24
  import { runHeadlessInstructionRuns } from "./src/cli/headless-runner.js";
25
25
  import { planScenarioRuns } from "./src/cli/scenario-planner.js";
26
26
  import { resolveLambdaTestCredentials, uploadLambdaTestCliApp } from "./src/cli/lambdatest-app-upload.js";
27
+ import { resolveAwsDeviceFarmCredentials, uploadAwsDeviceFarmCliApp } from "./src/cli/aws-device-farm-app-upload.js";
28
+ import { createStatusLine } from "./src/cli/cloud-status-line.js";
27
29
  import { createLoadmillApiClient } from "./src/integrations/loadmill/client.js";
28
30
  import { getCloudProviderAdapter } from "./src/device/cloud/registry.js";
31
+ import { setCloudProgressListener } from "./src/device/cloud/progress.js";
29
32
  dotenv.config();
30
33
  const args = minimist(process.argv.slice(2));
31
34
  const targetConfig = resolveHeadlessTarget(args);
@@ -131,12 +134,27 @@ async function resolveLambdaTestDashboardUrl(credentials, sessionId, { attempts
131
134
  return null;
132
135
  }
133
136
  async function main() {
137
+ if (targetConfig.listDevices) {
138
+ const credentials = resolveAwsDeviceFarmCredentials(process.env);
139
+ const adapter = getCloudProviderAdapter("aws-device-farm");
140
+ const devices = await adapter.getAvailableDevices(credentials);
141
+ for (const device of devices) {
142
+ console.log(`${device.name} — ${device.platform} ${device.osVersion}`);
143
+ }
144
+ console.log(`\n${devices.length} remote-access devices available.`);
145
+ console.log(`Run with: --device-name "<name>" --os-version "<osVersion>"`);
146
+ process.exit(0);
147
+ }
134
148
  const isHeadlessInstructionsRun = Boolean(instructionsFile);
135
149
  const isBrowserTarget = targetConfig.target === "web";
136
150
  const isLambdaTestTarget = targetConfig.deviceSource === "lambdatest";
151
+ const isAwsDeviceFarmTarget = targetConfig.deviceSource === "aws-device-farm";
152
+ const isCloudTarget = isLambdaTestTarget || isAwsDeviceFarmTarget;
137
153
  let lambdaTestSessionConnected = false;
138
154
  let lambdaTestSessionUrl = null;
139
155
  let lambdaTestCredentials = null;
156
+ let awsSessionConnected = false;
157
+ let awsDeviceFarmStatusLine = null;
140
158
  const headlessDebug = createHeadlessDebugArtifacts({
141
159
  cwd: process.cwd(),
142
160
  enabled: isHeadlessInstructionsRun && debugMode
@@ -149,8 +167,8 @@ async function main() {
149
167
  if (isBrowserTarget && !isHeadlessInstructionsRun) {
150
168
  throw new Error("--target web is only supported with --instructions headless execution.");
151
169
  }
152
- if (isLambdaTestTarget && !isHeadlessInstructionsRun) {
153
- throw new Error("--device-source lambdatest is only supported with --instructions headless execution.");
170
+ if (isCloudTarget && !isHeadlessInstructionsRun) {
171
+ throw new Error(`--device-source ${targetConfig.deviceSource} is only supported with --instructions headless execution.`);
154
172
  }
155
173
  if (!isHeadlessInstructionsRun && args.scenario != null) {
156
174
  throw new Error("--scenario can only be used with --instructions headless execution.");
@@ -206,7 +224,7 @@ async function main() {
206
224
  }
207
225
  }
208
226
  // If no device specified, show interactive selection menu
209
- if (!isBrowserTarget && !isLambdaTestTarget && !avdName && !platform) {
227
+ if (!isBrowserTarget && !isCloudTarget && !avdName && !platform) {
210
228
  const selection = await selectDevice();
211
229
  platform = selection.platform;
212
230
  avdName = selection.deviceName;
@@ -268,6 +286,85 @@ async function main() {
268
286
  console.log(`Using real resolution: ${deviceInfo.device_width}x${deviceInfo.device_height}`);
269
287
  console.log(`Model sees resolution: ${deviceInfo.scaled_width}x${deviceInfo.scaled_height}`);
270
288
  }
289
+ else if (isAwsDeviceFarmTarget) {
290
+ const awsCredentials = resolveAwsDeviceFarmCredentials(process.env);
291
+ awsDeviceFarmStatusLine = createStatusLine();
292
+ const statusLine = awsDeviceFarmStatusLine;
293
+ setCloudProgressListener((event) => statusLine.handleEvent(event));
294
+ console.log(`Uploading app to AWS Device Farm: ${path.resolve(targetConfig.appPath)}`);
295
+ statusLine.start("Uploading app to AWS Device Farm");
296
+ const upload = await uploadAwsDeviceFarmCliApp({
297
+ appPath: targetConfig.appPath,
298
+ platform: targetConfig.platform,
299
+ credentials: awsCredentials,
300
+ });
301
+ statusLine.stop();
302
+ console.log(`Uploaded app to AWS Device Farm: ${upload.appRef}`);
303
+ // A leaked metered session bills idle time until the provider's 5-minute
304
+ // timeout and holds one of the account's two concurrency slots. Register the
305
+ // Ctrl-C handler BEFORE provisioning so an interrupt during the ~1min
306
+ // PREPARING poll aborts the connect and lets AWS cleanup finish before exit.
307
+ const provisioningAbortController = new AbortController();
308
+ let awsSigintHandled = false;
309
+ let awsInterruptRequested = false;
310
+ process.once("SIGINT", async () => {
311
+ if (awsSigintHandled)
312
+ return;
313
+ awsSigintHandled = true;
314
+ awsInterruptRequested = true;
315
+ console.log("\nStopping AWS Device Farm session...");
316
+ provisioningAbortController.abort(new Error("Stopped by user"));
317
+ if (!awsSessionConnected) {
318
+ process.exitCode = 130;
319
+ return;
320
+ }
321
+ await disconnectDevice({ waitForCompletion: false });
322
+ process.exit(130);
323
+ });
324
+ try {
325
+ statusLine.start("Provisioning AWS Device Farm device");
326
+ targetId = await connectToDevice({
327
+ source: "aws-device-farm",
328
+ providerId: "aws-device-farm",
329
+ platform: targetConfig.platform,
330
+ deviceName: targetConfig.deviceName,
331
+ osVersion: targetConfig.osVersion,
332
+ appRef: upload.appRef,
333
+ credentials: awsCredentials,
334
+ sessionName: `droid-cua-cli-${new Date().toISOString()}`,
335
+ signal: provisioningAbortController.signal,
336
+ }, null, { log: (message) => statusLine.update(message) });
337
+ awsSessionConnected = true;
338
+ if (awsInterruptRequested) {
339
+ try {
340
+ await disconnectDevice({ waitForCompletion: false });
341
+ }
342
+ finally {
343
+ statusLine.stop();
344
+ awsSessionConnected = false;
345
+ process.exitCode = 130;
346
+ }
347
+ return;
348
+ }
349
+ deviceInfo = await getDeviceInfo(targetId);
350
+ statusLine.stop();
351
+ }
352
+ catch (error) {
353
+ statusLine.stop();
354
+ if (awsSessionConnected) {
355
+ await disconnectDevice({ waitForCompletion: false });
356
+ awsSessionConnected = false;
357
+ }
358
+ if (awsInterruptRequested) {
359
+ process.exitCode = 130;
360
+ return;
361
+ }
362
+ throw error;
363
+ }
364
+ console.log(`Connected to AWS Device Farm device "${targetConfig.deviceName}" (${targetId})`);
365
+ console.log(`Using real resolution: ${deviceInfo.device_width}x${deviceInfo.device_height}`);
366
+ console.log(`Model sees resolution: ${deviceInfo.scaled_width}x${deviceInfo.scaled_height}`);
367
+ }
271
368
  else {
272
369
  targetId = await connectToDevice(avdName, platform);
273
370
  deviceInfo = await getDeviceInfo(targetId);
@@ -334,7 +431,7 @@ async function main() {
334
431
  recordScreenshots,
335
432
  screenshotDir,
336
433
  isBrowserTarget,
337
- isLambdaTestTarget,
434
+ isCloudTarget,
338
435
  report: reportConfig
339
436
  });
340
437
  exitCode = result.exitCode;
@@ -358,6 +455,14 @@ async function main() {
358
455
  console.log(`LambdaTest session id: ${targetId}`);
359
456
  }
360
457
  }
458
+ if (awsSessionConnected) {
459
+ console.log("Ending AWS Device Farm session...");
460
+ awsDeviceFarmStatusLine.start("Ending AWS Device Farm session");
461
+ await disconnectDevice({ waitForCompletion: false });
462
+ awsDeviceFarmStatusLine.stop();
463
+ awsSessionConnected = false;
464
+ console.log("AWS Device Farm session ended.");
465
+ }
361
466
  }
362
467
  if (exitCode !== null) {
363
468
  process.exit(exitCode);
@@ -79,6 +79,25 @@ function buildWelcomePageHtml() {
79
79
 
80
80
  .subhead { margin-top: 22px; font-size: 18px; line-height: 1.65; }
81
81
 
82
+ .return-action {
83
+ margin-top: 30px;
84
+ display: inline-flex;
85
+ align-items: center;
86
+ justify-content: center;
87
+ min-height: 46px;
88
+ padding: 0 22px;
89
+ border-radius: 999px;
90
+ background: #0f172a;
91
+ color: #ffffff;
92
+ font-size: 15px;
93
+ font-weight: 700;
94
+ text-decoration: none;
95
+ box-shadow: 0 14px 30px rgba(15, 23, 42, 0.18);
96
+ }
97
+
98
+ .return-action:hover { background: #1e293b; }
99
+ .return-action:focus-visible { outline: 3px solid rgba(79, 70, 229, 0.34); outline-offset: 3px; }
100
+
82
101
  .divider {
83
102
  width: 48px;
84
103
  height: 2px;
@@ -121,6 +140,7 @@ function buildWelcomePageHtml() {
121
140
  </div>
122
141
  <h1>Test Browser Ready</h1>
123
142
  <p class="subhead">This is the browser the Loadmill Droid CUA agent will drive when you start a test.</p>
143
+ <a class="return-action" href="droid-cua://focus">Return to the Droid app</a>
124
144
  <div class="divider" role="presentation"></div>
125
145
  <p class="pointer">Switch back to Loadmill Droid CUA to design or run a test — you should keep this window in the background.</p>
126
146
  <p class="tip">
@@ -0,0 +1,76 @@
1
+ import path from "path";
2
+ import { stat } from "fs/promises";
3
+ import { awsDeviceFarmAdapter } from "../device/cloud/aws-device-farm/adapter.js";
4
+ import { resolveUploadedApp } from "../device/cloud/aws-device-farm/upload-cache.js";
5
+ function trimNullableString(value) {
6
+ if (typeof value !== "string") {
7
+ return null;
8
+ }
9
+ const trimmed = value.trim();
10
+ return trimmed || null;
11
+ }
12
+ export function resolveAwsDeviceFarmCredentials(env = process.env) {
13
+ const username = trimNullableString(env.AWS_DEVICE_FARM_ACCESS_KEY_ID);
14
+ const accessKey = trimNullableString(env.AWS_DEVICE_FARM_SECRET_ACCESS_KEY);
15
+ const projectArn = trimNullableString(env.AWS_DEVICE_FARM_PROJECT_ARN);
16
+ if (!username || !accessKey || !projectArn) {
17
+ throw new Error("AWS_DEVICE_FARM_ACCESS_KEY_ID, AWS_DEVICE_FARM_SECRET_ACCESS_KEY and AWS_DEVICE_FARM_PROJECT_ARN are required for --device-source aws-device-farm.");
18
+ }
19
+ return {
20
+ username,
21
+ accessKey,
22
+ projectArn,
23
+ };
24
+ }
25
+ export function validateAwsDeviceFarmAppPath(appPath, platform) {
26
+ const normalizedPlatform = trimNullableString(platform)?.toLowerCase();
27
+ if (normalizedPlatform !== "android" && normalizedPlatform !== "ios") {
28
+ throw new Error("--platform must be one of: android, ios.");
29
+ }
30
+ const normalizedAppPath = trimNullableString(appPath);
31
+ if (!normalizedAppPath) {
32
+ throw new Error("--app is required with --device-source aws-device-farm.");
33
+ }
34
+ const resolvedPath = path.resolve(normalizedAppPath);
35
+ const extension = path.extname(resolvedPath).toLowerCase();
36
+ if (extension === ".app") {
37
+ throw new Error("AWS Device Farm real-device sessions require an .apk for Android or an .ipa for iOS. .app bundles are not supported.");
38
+ }
39
+ if (normalizedPlatform === "android" && extension !== ".apk") {
40
+ throw new Error("AWS Device Farm Android sessions require an .apk build.");
41
+ }
42
+ if (normalizedPlatform === "ios" && extension !== ".ipa") {
43
+ throw new Error("AWS Device Farm iOS sessions require an .ipa build.");
44
+ }
45
+ return resolvedPath;
46
+ }
47
+ export async function uploadAwsDeviceFarmCliApp({ appPath, platform, credentials, adapter = awsDeviceFarmAdapter, } = {}) {
48
+ const resolvedPath = validateAwsDeviceFarmAppPath(appPath, platform);
49
+ let fileStats;
50
+ try {
51
+ fileStats = await stat(resolvedPath);
52
+ }
53
+ catch (error) {
54
+ const message = error instanceof Error ? error.message : "App file is unavailable.";
55
+ throw new Error(`App file is unavailable: ${message}`);
56
+ }
57
+ if (!fileStats.isFile()) {
58
+ throw new Error("Selected app path is not a file.");
59
+ }
60
+ // sha256+projectArn upload cache — re-runs of the same binary reuse the
61
+ // existing upload ARN instead of re-uploading (Device Farm has no
62
+ // server-side dedup; uploads age out after ~30 days).
63
+ const { app: uploadedRef } = await resolveUploadedApp({
64
+ adapter,
65
+ credentials,
66
+ appPath: resolvedPath,
67
+ });
68
+ if (!uploadedRef?.remotePath || typeof uploadedRef.remotePath !== "string") {
69
+ throw new Error("AWS Device Farm did not return a valid app reference.");
70
+ }
71
+ return {
72
+ appRef: uploadedRef.remotePath,
73
+ appPath: resolvedPath,
74
+ uploadedRef,
75
+ };
76
+ }
@@ -0,0 +1,98 @@
1
+ const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
2
+ /**
3
+ * Heroku-style volatile status line for long-running cloud-provider steps.
4
+ * On a TTY each progress update overwrites the previous one (spinner + step
5
+ * name + elapsed + latest detail) and the whole line is cleared when the step
6
+ * finishes. On a non-TTY (CI, piped output) it degrades to plain log lines,
7
+ * printed only when the detail changes. Feed it cloud progress events via
8
+ * handleEvent: "phase" events time named sub-steps and "note" events keep
9
+ * persistent info; both are printed (prefixed with `indent`) when the step
10
+ * finishes.
11
+ */
12
+ export function createStatusLine({ stream = process.stderr, indent = "" } = {}) {
13
+ const tty = stream.isTTY;
14
+ let active = false;
15
+ let timer = null;
16
+ let frame = 0;
17
+ let stepName = "";
18
+ let detail = "";
19
+ let startedAt = 0;
20
+ let lastPrintedDetail = "";
21
+ let entries = [];
22
+ let currentPhase = null;
23
+ const render = () => {
24
+ frame = (frame + 1) % SPINNER_FRAMES.length;
25
+ const seconds = Math.floor((Date.now() - startedAt) / 1000);
26
+ const line = `${SPINNER_FRAMES[frame]} ${stepName} (${seconds}s)${detail ? ` — ${detail}` : ""}`;
27
+ stream.write(`\r\x1b[2K${line}`);
28
+ };
29
+ const closePhase = () => {
30
+ if (currentPhase) {
31
+ currentPhase.ms = Date.now() - currentPhase.startedAt;
32
+ currentPhase = null;
33
+ }
34
+ };
35
+ return {
36
+ start(name) {
37
+ active = true;
38
+ stepName = name;
39
+ detail = "";
40
+ lastPrintedDetail = "";
41
+ entries = [];
42
+ currentPhase = null;
43
+ startedAt = Date.now();
44
+ if (tty) {
45
+ // First frame is deferred so any line the caller just printed lands
46
+ // on its own row before this step starts drawing.
47
+ timer = setInterval(render, 120);
48
+ }
49
+ },
50
+ update(message) {
51
+ if (!active) {
52
+ return;
53
+ }
54
+ detail = message;
55
+ if (tty) {
56
+ render();
57
+ }
58
+ else if (message !== lastPrintedDetail) {
59
+ lastPrintedDetail = message;
60
+ console.log(`… ${stepName}: ${message}`);
61
+ }
62
+ },
63
+ handleEvent(event) {
64
+ if (!active) {
65
+ return;
66
+ }
67
+ if (event.kind === "phase") {
68
+ closePhase();
69
+ currentPhase = { kind: "phase", name: event.message, startedAt: Date.now(), ms: 0 };
70
+ entries.push(currentPhase);
71
+ }
72
+ else if (event.kind === "note") {
73
+ entries.push({ kind: "note", text: event.message });
74
+ }
75
+ this.update(event.message);
76
+ },
77
+ stop() {
78
+ active = false;
79
+ if (timer) {
80
+ clearInterval(timer);
81
+ timer = null;
82
+ }
83
+ if (tty) {
84
+ stream.write("\r\x1b[2K");
85
+ }
86
+ closePhase();
87
+ for (const entry of entries) {
88
+ if (entry.kind === "phase") {
89
+ stream.write(`${indent}\x1b[32m✔\x1b[0m ${entry.name} \x1b[90m(${entry.ms}ms)\x1b[0m\n`);
90
+ }
91
+ else {
92
+ stream.write(`${indent}\x1b[34mℹ\x1b[0m ${entry.text}\n`);
93
+ }
94
+ }
95
+ entries = [];
96
+ }
97
+ };
98
+ }
@@ -56,7 +56,7 @@ function emitCliAppContextDebug(runId, appContext, appContextText) {
56
56
  });
57
57
  }
58
58
  }
59
- function buildHeadlessEngineOptions({ runtimeContext, headlessConfig, executionPrompt, screenshotRecorder, recordScreenshots, screenshotDir, isBrowserTarget, isLambdaTestTarget, secrets = null }) {
59
+ function buildHeadlessEngineOptions({ runtimeContext, headlessConfig, executionPrompt, screenshotRecorder, recordScreenshots, screenshotDir, isBrowserTarget, isCloudTarget, secrets = null }) {
60
60
  const executionEngineOptions = {
61
61
  toolCapabilities: {
62
62
  assert: true,
@@ -95,14 +95,14 @@ function buildHeadlessEngineOptions({ runtimeContext, headlessConfig, executionP
95
95
  engineOptions.handleModelAction = (browserSessionId, action, scale, actionContext) => handleBrowserModelAction(browserSessionId, action, scale, actionContext);
96
96
  engineOptions.getCurrentPlatform = getBrowserCurrentPlatform;
97
97
  }
98
- else if (isLambdaTestTarget) {
98
+ else if (isCloudTarget) {
99
99
  engineOptions.connectToDevice = async () => {
100
- throw new Error("LambdaTest cloud session disconnected; automatic CLI reconnect is not supported yet.");
100
+ throw new Error("Cloud session disconnected; automatic CLI reconnect is not supported yet.");
101
101
  };
102
102
  }
103
103
  return engineOptions;
104
104
  }
105
- async function runHeadlessTest({ runId, testPath, testName, instructions, session, deviceInfo, headlessConfig, appContext, testDataContext = "", runtimeContext, headlessDebug, recordScreenshots, screenshotDir, isBrowserTarget, isLambdaTestTarget, secrets = null, report = null, isBatchRun = false }) {
105
+ async function runHeadlessTest({ runId, testPath, testName, instructions, session, deviceInfo, headlessConfig, appContext, testDataContext = "", runtimeContext, headlessDebug, recordScreenshots, screenshotDir, isBrowserTarget, isCloudTarget, secrets = null, report = null, isBatchRun = false }) {
106
106
  let executionMode = null;
107
107
  const reportEnabled = Boolean(report?.enabled);
108
108
  const collectedEvents = [];
@@ -146,7 +146,7 @@ async function runHeadlessTest({ runId, testPath, testName, instructions, sessio
146
146
  recordScreenshots,
147
147
  screenshotDir,
148
148
  isBrowserTarget,
149
- isLambdaTestTarget,
149
+ isCloudTarget,
150
150
  secrets
151
151
  });
152
152
  const engine = new ExecutionEngine(session, engineOptions);
@@ -156,6 +156,7 @@ async function runHeadlessTest({ runId, testPath, testName, instructions, sessio
156
156
  addOutput,
157
157
  openAiClient: runtimeContext.openAiClient,
158
158
  loadmillClient: runtimeContext.loadmillClient,
159
+ runtimeConfig: runtimeContext.config,
159
160
  runtimeCredentials: runtimeContext.credentials,
160
161
  // Inline screenshots as data URLs so the HTML report is self-contained.
161
162
  ...(reportEnabled ? { includeScreenshotDataUrl: true } : {}),
@@ -223,7 +224,7 @@ async function runHeadlessTest({ runId, testPath, testName, instructions, sessio
223
224
  throw error;
224
225
  }
225
226
  }
226
- export async function runHeadlessInstructionRuns({ runs, instructionsTarget, session, deviceInfo, headlessConfig, appContext, runtimeContext, headlessDebug, secrets = null, recordScreenshots, screenshotDir, isBrowserTarget, isLambdaTestTarget, report = null, runHeadlessTestImpl = runHeadlessTest }) {
227
+ export async function runHeadlessInstructionRuns({ runs, instructionsTarget, session, deviceInfo, headlessConfig, appContext, runtimeContext, headlessDebug, secrets = null, recordScreenshots, screenshotDir, isBrowserTarget, isCloudTarget, report = null, runHeadlessTestImpl = runHeadlessTest }) {
227
228
  const isBatchRun = instructionsTarget?.type === "directory" || instructionsTarget?.type === "scenario";
228
229
  if (instructionsTarget?.type === "scenario") {
229
230
  console.log(`\nRunning ${runs.length} scenario case${runs.length === 1 ? "" : "s"}\n`);
@@ -259,7 +260,7 @@ export async function runHeadlessInstructionRuns({ runs, instructionsTarget, ses
259
260
  recordScreenshots,
260
261
  screenshotDir: testScreenshotDir,
261
262
  isBrowserTarget,
262
- isLambdaTestTarget,
263
+ isCloudTarget,
263
264
  report,
264
265
  isBatchRun
265
266
  });