@loadmill/droid-cua 2.22.0 → 2.24.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.
Files changed (38) hide show
  1. package/README.md +25 -0
  2. package/build/index.js +43 -1
  3. package/build/src/browser/playwright/actions.js +65 -1
  4. package/build/src/cli/skill-command.js +240 -0
  5. package/build/src/commands/help.js +2 -1
  6. package/build/src/core/cua-models.js +29 -11
  7. package/build/src/core/droid-cua-run-usage.js +16 -7
  8. package/build/src/core/execution-engine.js +334 -5
  9. package/build/src/core/prompts.js +1 -1
  10. package/build/src/core/report/render-html-report.js +8 -3
  11. package/build/src/core/report/run-log-model.js +42 -0
  12. package/build/src/core/runtime-config.js +23 -41
  13. package/build/src/core/session.js +6 -0
  14. package/build/src/core/tool-handlers/loadmill.js +3 -0
  15. package/build/src/core/tool-handlers/test-design-complete.js +1 -0
  16. package/build/src/core/tool-handlers/type-sensitive-text.js +1 -0
  17. package/build/src/device/android/actions.js +6 -2
  18. package/build/src/device/android/connection.js +58 -23
  19. package/build/src/device/assertions.js +15 -8
  20. package/build/src/device/ios/connection.js +36 -30
  21. package/build/src/device/openai.js +68 -2
  22. package/build/src/device/scroll-gesture.js +22 -9
  23. package/build/src/integrations/loadmill/client.js +20 -4
  24. package/build/src/modes/design-mode-ink.js +10 -1
  25. package/build/src/modes/design-mode.js +7 -1
  26. package/build/src/modes/execution-mode.js +84 -8
  27. package/build/src/prompts/design.js +21 -1
  28. package/build/src/utils/console-output.js +5 -0
  29. package/build/src/utils/cua-debug-tracer.js +50 -7
  30. package/package.json +2 -1
  31. package/skill/droid-cua/SKILL.md +36 -0
  32. package/skill/droid-cua/agents/openai.yaml +6 -0
  33. package/skill/droid-cua/assets/droid-cua-app-icon.png +0 -0
  34. package/skill/droid-cua/references/authoring-tests.md +39 -0
  35. package/skill/droid-cua/references/onboarding-and-troubleshooting.md +74 -0
  36. package/skill/droid-cua/references/project-and-auth.md +38 -0
  37. package/skill/droid-cua/references/running-and-healing.md +70 -0
  38. package/build/src/core/cua-backends.js +0 -11
@@ -4,15 +4,15 @@ import { connectToBrowser, getBrowserInfo, getCurrentPlatform as getBrowserCurre
4
4
  import { normalizeBrowserInfoForSession } from "../browser/normalize.js";
5
5
  import { sendCUARequest } from "../device/openai.js";
6
6
  import { buildExecutionRecoveryPrompt } from "../core/prompts.js";
7
- import { isAssertion } from "../device/assertions.js";
7
+ import { extractAssertionPrompt, isAssertion } from "../device/assertions.js";
8
+ import { handleAssertionToolCall } from "../core/tool-handlers/assert.js";
8
9
  import { logger } from "../utils/logger.js";
9
10
  import { emitDesktopDebug } from "../utils/desktop-debug.js";
10
11
  import { printCliOutput } from "../utils/console-output.js";
11
- // Mirrors the codes emitted by the Loadmill LLM gateway (see services/app/private/llm
12
- // in the server repo): a transient burst limit plus day/week/fixed-budget request and
13
- // token quotas. Free plans run on the fixed-budget quota.
12
+ // Mirrors the quota codes emitted by the Loadmill LLM gateway (see
13
+ // services/app/private/llm in the server repo). Free plans run on the
14
+ // fixed-budget quota.
14
15
  const LLM_USAGE_LIMIT_ERROR_CODES = new Set([
15
- "llm_rate_limit_exceeded",
16
16
  "llm_day_quota_exceeded",
17
17
  "llm_week_quota_exceeded",
18
18
  "llm_fixed_budget_quota_exceeded",
@@ -21,6 +21,8 @@ const LLM_USAGE_LIMIT_ERROR_CODES = new Set([
21
21
  "llm_fixed_budget_token_quota_exceeded",
22
22
  ]);
23
23
  const LOADMILL_UPGRADE_URL = "https://www.loadmill.com/book-a-demo";
24
+ const LLM_REQUEST_RATE_LIMIT_ERROR_CODE = "llm_rate_limit_exceeded";
25
+ const DEFAULT_ASSERTION_CONTEXT = "No additional prior context is needed; validate directly from the current screen.";
24
26
  function isLlmUsageLimitError(err) {
25
27
  if (!err || typeof err !== "object") {
26
28
  return false;
@@ -30,9 +32,15 @@ function isLlmUsageLimitError(err) {
30
32
  }
31
33
  return err.status === 429 && err.type === "rate_limit_error";
32
34
  }
35
+ function isLlmRequestRateLimitError(err) {
36
+ return err && typeof err === "object" && err.code === LLM_REQUEST_RATE_LIMIT_ERROR_CODE;
37
+ }
33
38
  function getLlmUsageLimitMessage() {
34
39
  return `Execution stopped: you've reached the AI usage limit for this account. Contact the Loadmill team to upgrade: ${LOADMILL_UPGRADE_URL}`;
35
40
  }
41
+ function getLlmRequestRateLimitMessage() {
42
+ return "Execution stopped: too many AI requests are running for this account right now. Please retry shortly or reduce parallel test runs.";
43
+ }
36
44
  /**
37
45
  * Execution Mode - Run test scripts line-by-line
38
46
  * Each instruction is executed in isolation (messages cleared after each turn)
@@ -105,10 +113,11 @@ export class ExecutionMode {
105
113
  lines.push(` Retries: ${this.stats.retryCount}`);
106
114
  return lines;
107
115
  }
108
- buildStepContext(instructionIndex) {
116
+ buildStepContext(instructionIndex, instruction = null) {
109
117
  return {
110
118
  instructionIndex,
111
119
  stepId: `step-${String(instructionIndex + 1).padStart(4, "0")}`,
120
+ ...(typeof instruction === "string" ? { instruction } : {}),
112
121
  };
113
122
  }
114
123
  emit(addOutput, type, text, context = {}, stepContext = null, extra = {}) {
@@ -123,6 +132,45 @@ export class ExecutionMode {
123
132
  payload: extra.payload
124
133
  });
125
134
  }
135
+ resetInstructionConversationState() {
136
+ this.session.updateResponseId(null);
137
+ if (typeof this.session.clearTranscript === "function") {
138
+ this.session.clearTranscript();
139
+ return;
140
+ }
141
+ if (Array.isArray(this.session.transcript)) {
142
+ this.session.transcript = [];
143
+ }
144
+ }
145
+ async executeVerificationInstruction(instruction, context, stepContext = null) {
146
+ const toolResult = await handleAssertionToolCall({
147
+ engine: this.engine,
148
+ callId: stepContext?.stepId ? `${stepContext.stepId}-assert` : "instruction-assert",
149
+ argumentsText: JSON.stringify({
150
+ condition: extractAssertionPrompt(instruction),
151
+ context: DEFAULT_ASSERTION_CONTEXT,
152
+ }),
153
+ context,
154
+ stepContext,
155
+ });
156
+ const assertionStatus = toolResult.summaryUpdates?.assertionStatus;
157
+ if (assertionStatus === "passed") {
158
+ this.stats.assertionsPassed++;
159
+ }
160
+ else if (assertionStatus === "failed" || assertionStatus === "failed_skipped") {
161
+ this.stats.assertionsFailed++;
162
+ }
163
+ if (toolResult.kind === "retry_instruction") {
164
+ this.stats.retryCount++;
165
+ this.session.clearMessages();
166
+ return await this.executeInstruction(instruction, context, 0, stepContext);
167
+ }
168
+ if (toolResult.kind === "fail") {
169
+ return { success: false, error: toolResult.error || "Assertion failed." };
170
+ }
171
+ this.session.clearMessages();
172
+ return { success: true };
173
+ }
126
174
  /**
127
175
  * Execute all instructions in the test script
128
176
  * @param {Object} context - Additional context (Ink context with addOutput)
@@ -137,7 +185,8 @@ export class ExecutionMode {
137
185
  // Start timing
138
186
  this.stats.startTime = Date.now();
139
187
  for (let i = 0; i < this.instructions.length; i++) {
140
- const stepContext = this.buildStepContext(i);
188
+ const instruction = this.instructions[i];
189
+ const stepContext = this.buildStepContext(i, instruction);
141
190
  // Check if execution should be stopped
142
191
  if (this.shouldStop) {
143
192
  this.emit(addOutput, 'info', 'Test execution stopped by user.', runContext, stepContext, {
@@ -158,7 +207,6 @@ export class ExecutionMode {
158
207
  return { success: false, error: 'Stopped by user' };
159
208
  }
160
209
  }
161
- const instruction = this.instructions[i];
162
210
  this.emit(addOutput, 'user', instruction, runContext, stepContext, {
163
211
  eventType: 'instruction_started',
164
212
  payload: {
@@ -236,10 +284,17 @@ export class ExecutionMode {
236
284
  const MAX_RETRIES = 10;
237
285
  const instructionContext = {
238
286
  ...context,
287
+ currentInstruction: instruction,
239
288
  futureInstructions: this.instructions.slice((stepContext?.instructionIndex ?? -1) + 1),
240
289
  };
290
+ if (retryCount === 0) {
291
+ this.resetInstructionConversationState();
292
+ }
241
293
  this.session.addToTranscript(`[User] ${instruction}`);
242
294
  this.session.addMessage("user", instruction);
295
+ if (isAssertion(instruction)) {
296
+ return await this.executeVerificationInstruction(instruction, instructionContext, stepContext);
297
+ }
243
298
  try {
244
299
  const screenshotBase64 = await this.getScreenshotAsBase64(this.session.deviceId, this.session.deviceInfo);
245
300
  const currentScreenshot = await this.engine.recordScreenshot?.(screenshotBase64, {
@@ -309,6 +364,26 @@ export class ExecutionMode {
309
364
  stack: err.stack
310
365
  });
311
366
  const addOutput = context.addOutput || printCliOutput;
367
+ if (isLlmRequestRateLimitError(err)) {
368
+ const stopMessage = getLlmRequestRateLimitMessage();
369
+ logger.debug('Stopping execution because an LLM request rate limit was reached', {
370
+ instruction,
371
+ message: err.message,
372
+ status: err.status,
373
+ code: err.code,
374
+ type: err.type,
375
+ });
376
+ this.emit(addOutput, 'error', stopMessage, context, stepContext, {
377
+ eventType: 'error',
378
+ payload: {
379
+ message: stopMessage,
380
+ status: err.status,
381
+ code: err.code,
382
+ type: err.type,
383
+ }
384
+ });
385
+ return { success: false, error: stopMessage, usageLimitReached: true };
386
+ }
312
387
  if (isLlmUsageLimitError(err)) {
313
388
  const stopMessage = getLlmUsageLimitMessage();
314
389
  logger.debug('Stopping execution because an LLM usage limit was reached', {
@@ -449,6 +524,7 @@ export class ExecutionMode {
449
524
  });
450
525
  // Replace the system message with the enhanced one
451
526
  this.session.installRecoverySystemPrompt(recoverySystemPrompt);
527
+ this.session.updateResponseId(null);
452
528
  // Wait a bit before retrying
453
529
  await new Promise(resolve => setTimeout(resolve, 1000));
454
530
  // Retry the same instruction with incremented counter
@@ -139,7 +139,9 @@ Test design exploration is complete. Use the validated completion summary below
139
139
  Do not call tools.
140
140
  Do not continue exploring.
141
141
  Do not add explanation.
142
- Output only the final result in the required design-mode format.
142
+ Output only the exact final result format below: one "Suggested test name:" line, then one fenced code block.
143
+ Do not use TEST NAME, GOAL, STEPS, VERIFICATIONS, FINAL STATE, markdown headings, numbered lists, or bullets.
144
+ The fenced code block must contain only executable test instructions, one per line, ending with exit.
143
145
 
144
146
  Completion summary:
145
147
  Suggested test name: ${suggestedTestName || "(choose a short kebab-case name)"}
@@ -151,3 +153,21 @@ Verification points:
151
153
  ${formatStringList(summary.verificationPoints)}
152
154
  ${notesForScript ? `Notes for script:\n${notesForScript}\n` : ""}`;
153
155
  }
156
+ export function buildDesignScriptFormatRetryPrompt(previousOutput = "") {
157
+ const previousOutputText = typeof previousOutput === "string" ? previousOutput.trim() : "";
158
+ return `AUTHORING FORMAT CORRECTION:
159
+ Your previous answer did not use the required design-mode format.
160
+
161
+ Output only:
162
+ Suggested test name: short-kebab-case-name
163
+
164
+ \`\`\`
165
+ one executable instruction per line
166
+ exit
167
+ \`\`\`
168
+
169
+ Do not include TEST NAME, GOAL, STEPS, VERIFICATIONS, FINAL STATE, markdown headings, numbered lists, bullets, or explanation.${previousOutputText ? `
170
+
171
+ Previous output to reformat:
172
+ ${previousOutputText}` : ""}`;
173
+ }
@@ -1,4 +1,5 @@
1
1
  import { classifyCaptureSource } from "../core/capture-sources.js";
2
+ import { stepCompletionData } from "../core/report/run-log-model.js";
2
3
  function isHiddenExecutionOutput(item) {
3
4
  const captureSource = item?.payload?.captureSource;
4
5
  return classifyCaptureSource(captureSource).mediaKind === "hidden-debug";
@@ -24,6 +25,10 @@ export function formatCliOutput(item) {
24
25
  if (!text || text.trim().length === 0) {
25
26
  return null;
26
27
  }
28
+ const stepCompletion = stepCompletionData(text);
29
+ if (stepCompletion) {
30
+ return `assistant: Step complete — ${stepCompletion.instruction}`;
31
+ }
27
32
  return text
28
33
  .split("\n")
29
34
  .map((line) => `assistant: ${line}`)
@@ -92,6 +92,54 @@ function summarizeRequestInput(input) {
92
92
  };
93
93
  });
94
94
  }
95
+ function extractImageBase64Length(imageUrl) {
96
+ return typeof imageUrl === "string" ? imageUrl.replace("data:image/png;base64,", "").length : 0;
97
+ }
98
+ function summarizeRequestScreenshots(input, deviceInfo) {
99
+ const screenshots = [];
100
+ for (const item of input) {
101
+ if (item?.role && Array.isArray(item.content)) {
102
+ for (const entry of item.content) {
103
+ if (entry?.type !== "input_image" || typeof entry.image_url !== "string")
104
+ continue;
105
+ screenshots.push({
106
+ source: `message:${item.role}`,
107
+ base64Length: extractImageBase64Length(entry.image_url),
108
+ });
109
+ }
110
+ continue;
111
+ }
112
+ if (item?.type === "computer_call_output" && typeof item.output?.image_url === "string") {
113
+ screenshots.push({
114
+ source: "computer_call_output",
115
+ callId: item.call_id,
116
+ base64Length: extractImageBase64Length(item.output.image_url),
117
+ });
118
+ continue;
119
+ }
120
+ if (item?.type === "function_call_output" && Array.isArray(item.output)) {
121
+ for (const entry of item.output) {
122
+ if (entry?.type !== "input_image" || typeof entry.image_url !== "string")
123
+ continue;
124
+ screenshots.push({
125
+ source: "function_call_output",
126
+ callId: item.call_id,
127
+ base64Length: extractImageBase64Length(entry.image_url),
128
+ });
129
+ }
130
+ }
131
+ }
132
+ if (screenshots.length === 0) {
133
+ return null;
134
+ }
135
+ return {
136
+ count: screenshots.length,
137
+ sources: screenshots.map((entry) => entry.source),
138
+ base64Lengths: screenshots.map((entry) => entry.base64Length),
139
+ width: deviceInfo?.scaled_width ?? null,
140
+ height: deviceInfo?.scaled_height ?? null,
141
+ };
142
+ }
95
143
  function redactRequestForFileLog(requestParams) {
96
144
  return {
97
145
  ...requestParams,
@@ -223,6 +271,7 @@ export class CuaDebugTracer {
223
271
  const localRequestId = crypto.randomUUID();
224
272
  const requestLog = redactRequestForFileLog(requestParams);
225
273
  const messages = summarizeRequestInput(input);
274
+ const screenshot = summarizeRequestScreenshots(input, deviceInfo);
226
275
  const acknowledgedSafetyChecksSent = input
227
276
  .filter((item) => item?.type === "computer_call_output")
228
277
  .flatMap((item) => {
@@ -260,13 +309,7 @@ export class CuaDebugTracer {
260
309
  inputItemTypes,
261
310
  inputCallIds,
262
311
  messages,
263
- screenshot: screenshotBase64
264
- ? {
265
- width: deviceInfo?.scaled_width ?? null,
266
- height: deviceInfo?.scaled_height ?? null,
267
- base64Length: screenshotBase64.length
268
- }
269
- : null,
312
+ screenshot,
270
313
  safetyChecks: {
271
314
  previousPending: previousPendingSafetyChecks,
272
315
  previousPendingCount: previousPendingSafetyChecks.length,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loadmill/droid-cua",
3
- "version": "2.22.0",
3
+ "version": "2.24.0",
4
4
  "description": "AI-powered Android testing agent using OpenAI's computer-use model and ADB",
5
5
  "main": "build/index.js",
6
6
  "type": "module",
@@ -10,6 +10,7 @@
10
10
  "files": [
11
11
  "build/**/*",
12
12
  "bin/**/*",
13
+ "skill/**/*",
13
14
  "README.md",
14
15
  "LICENSE"
15
16
  ],
@@ -0,0 +1,36 @@
1
+ ---
2
+ name: droid-cua
3
+ description: Write, run, monitor, and troubleshoot Droid CUA mobile and web tests. Use when an agent needs to onboard @loadmill/droid-cua, configure Loadmill authentication, discover Android devices or AVDs, prepare iOS simulators or local browsers, author or revise .dcua tests and project context, run tests locally or on supported cloud devices, analyze reports and debug artifacts, distinguish test failures from product bugs, or perform guarded test healing.
4
+ ---
5
+
6
+ # Droid CUA
7
+
8
+ Use the existing `droid-cua` CLI and repository files. Do not invent commands, flags, or product capabilities.
9
+
10
+ ## Workflow
11
+
12
+ 1. Inspect the active test project before creating files. Find existing `.dcua` tests, `context.md`, `test-data.md`, `.secrets`, `.env`, and `ci/droid-cua.json` without reading secret values. Do not search other projects or the user's home directory for credentials.
13
+ 2. Determine the requested platform and target from the task or project. Ask the user only when the choice is unclear.
14
+ 3. Check authentication and environment readiness before the first run. Read [project-and-auth.md](references/project-and-auth.md) for project boundaries and token handling. Read [onboarding-and-troubleshooting.md](references/onboarding-and-troubleshooting.md) when setup, dependency, device, simulator, browser, or connection work is needed.
15
+ 4. Author or update the smallest useful test. Read [authoring-tests.md](references/authoring-tests.md) before changing `.dcua`, context, test data, secrets, or config.
16
+ 5. Run the narrowest relevant test and monitor it to completion. Read [running-and-healing.md](references/running-and-healing.md) for commands, targets, artifacts, failure classification, and guarded healing.
17
+ 6. Report the outcome, evidence paths, changes made, and whether the failure belongs to the test, environment, or product.
18
+
19
+ ## Safety
20
+
21
+ - Never print, open, or echo secret values. Check only whether required keys are configured.
22
+ - Never search for credentials outside the active test project or source another project's `.env` file.
23
+ - Get approval before installing dependencies, running `sudo`, accepting licenses, or changing system configuration.
24
+ - During setup, do not create or edit user tests. Rerun only the failed readiness check, then resume the original task.
25
+ - During healing, edit only `.dcua`, `context.md`, `test-data.md`, or Droid CUA config. Never modify application code under the guise of healing a test.
26
+ - Never weaken or remove an assertion merely to make a failing run pass.
27
+ - Make one minimal change and run the affected test again. Stop after two edit/rerun cycles and present the evidence.
28
+ - Stop immediately when evidence indicates a likely application regression; do not rewrite the test to match broken behavior.
29
+ - Gracefully allow cloud runs to finish cleanup. Avoid terminating a metered device session unless necessary.
30
+
31
+ ## Operating principles
32
+
33
+ - Prefer an existing project convention over creating a parallel structure.
34
+ - Prefer headless runs for agent work because they are observable and return deterministic exit codes.
35
+ - Use explicit, user-observable outcomes in tests rather than implementation details or coordinates.
36
+ - Diagnose the first divergence between the instruction and observed behavior before changing anything.
@@ -0,0 +1,6 @@
1
+ interface:
2
+ display_name: "Droid Cua"
3
+ short_description: "Write, run, monitor, and troubleshoot Droid CUA tests"
4
+ icon_small: "./assets/droid-cua-app-icon.png"
5
+ icon_large: "./assets/droid-cua-app-icon.png"
6
+ brand_color: "#1895F7"
@@ -0,0 +1,39 @@
1
+ # Authoring tests
2
+
3
+ ## Inspect first
4
+
5
+ Find nearby `.dcua` tests and project files before writing. Match their directory layout, naming, vocabulary, starting state, and config conventions. Do not open `.env` or `.secrets`.
6
+
7
+ ## Write `.dcua` instructions
8
+
9
+ - Put one clear instruction or assertion on each non-empty line.
10
+ - Describe visible UI intent, not coordinates, selectors, implementation details, or model strategy.
11
+ - Prefer a high-level business intent when `context.md` makes the flow unambiguous. Do not translate every tap and keystroke into a brittle micro-script.
12
+ - Break an instruction down only when the agent demonstrably needs help finding a control or completing part of the flow.
13
+ - State the starting condition when it is not guaranteed by the environment.
14
+ - Use explicit assertions for important outcomes. Prefer stable visible facts such as a screen title, confirmation message, changed value, or navigation result.
15
+ - Use exact visible labels when naming controls or outcomes.
16
+ - Keep each test focused on one behavior. Split unrelated flows.
17
+ - Use `//` comments sparingly for human context; comments are not executed.
18
+ - Never include secret values in the test.
19
+
20
+ Example:
21
+
22
+ ```text
23
+ Sign in using the USER_EMAIL and USER_PASSWORD secrets.
24
+ Assert that the Home screen is visible and shows the signed-in account.
25
+ ```
26
+
27
+ Use the secret key names that exist in `.secrets`; do not inspect their values. If key discovery is necessary, filter the file so only keys reach output, for example `sed -n 's/^\([^#][^=]*\)=.*/\1/p' .secrets`. If a required key is absent, ask the user to add it without sharing its value.
28
+
29
+ ## Put knowledge in the right place
30
+
31
+ - Put durable product behavior and terminology in `context.md`, not repeated in every test.
32
+ - Put non-secret reusable people, accounts, products, and scenario combinations in `test-data.md`.
33
+ - Put credentials and sensitive values in `.secrets`.
34
+ - Put reproducible headless settings in `ci/droid-cua.json`.
35
+ - Keep environment and provider credentials in `.env` or the shell/CI secret store.
36
+
37
+ Use `--scenario "<goal>" --test-data <path>` only when the user wants generated permutations or cases. A normal `.dcua` run does not automatically consume `test-data.md`.
38
+
39
+ After editing, show the user the test instructions and explain any new context, data keys, or secret keys required. Never show secret values.
@@ -0,0 +1,74 @@
1
+ # Onboarding and troubleshooting
2
+
3
+ Follow the Desktop Device Setup Wizard sequence: choose a platform, run checks, discover/connect a target, verify runtime access during the requested run, then continue the original task. Run authentication as the separate first preflight described in `project-and-auth.md`.
4
+
5
+ Do not create or modify user test files merely to perform setup. Get approval before installing software, changing system configuration, running `sudo`, or accepting a license. After remediation, rerun only the failed check.
6
+
7
+ ## Android
8
+
9
+ Resolve `adb` and `emulator` from `ANDROID_HOME`, `ANDROID_SDK_ROOT`, the macOS default `~/Library/Android/sdk`, the Windows default `%LOCALAPPDATA%\Android\Sdk`, then `PATH`. Respect explicit `DROID_CUA_ANDROID_ADB_PATH` and `DROID_CUA_ANDROID_EMULATOR_PATH` overrides.
10
+
11
+ Run these checks:
12
+
13
+ ```sh
14
+ adb version
15
+ emulator -list-avds
16
+ adb devices
17
+ ```
18
+
19
+ - `adb` is blocking.
20
+ - The emulator CLI and existence of an AVD are individually non-blocking.
21
+ - Target availability is blocking and passes when `adb devices` contains a `device` entry or `emulator -list-avds` contains a launchable AVD.
22
+ - For `unauthorized`, ask the user to accept the computer authorization prompt on the device.
23
+ - For `offline`, reconnect the device or restart ADB, then retry.
24
+ - If nothing is available, guide the user to connect a device or create an AVD in Android Studio Device Manager.
25
+
26
+ Use `--avd adb:<serial>` for an attached target and `--avd avd:<name>` to launch an AVD.
27
+
28
+ ## iOS
29
+
30
+ Local iOS execution requires macOS. Run checks in this order:
31
+
32
+ ```sh
33
+ xcrun --version
34
+ xcode-select -p
35
+ xcodebuild -license check
36
+ xcodebuild -checkFirstLaunchStatus
37
+ node --version
38
+ npm --version
39
+ appium --version
40
+ appium driver list --installed
41
+ xcrun simctl list devices --json
42
+ ```
43
+
44
+ Require `xcode-select -p` to point to an `/Xcode*.app/Contents/Developer` path, an installed XCUITest driver, and at least one available iPhone simulator. A booted simulator is helpful but not blocking.
45
+
46
+ Offer only the relevant remediation, with approval:
47
+
48
+ ```sh
49
+ sudo xcode-select -s /Applications/Xcode.app/Contents/Developer
50
+ sudo xcodebuild -license accept
51
+ sudo xcodebuild -runFirstLaunch
52
+ npm install -g appium
53
+ appium driver install xcuitest
54
+ ```
55
+
56
+ Use `--platform ios --avd "<simulator name>"` for the run.
57
+
58
+ ## Web
59
+
60
+ Droid CUA automatically discovers Chrome or Edge on macOS and Windows. Check the standard application locations, then run the requested test with `--target web --browser <chrome|edge> --session-mode fresh`. A successful Playwright launch is the runtime verification; do not create a persistent setup-only test.
61
+
62
+ On Linux, automatic installed-browser discovery is not currently supported. Do not claim that a PATH-installed browser will be found.
63
+
64
+ ## Cloud targets
65
+
66
+ For LambdaTest, require `LAMBDATEST_USERNAME`, `LAMBDATEST_ACCESS_KEY`, a device name, OS version, and `.apk` or `.ipa`. The CLI uploads the app for each run.
67
+
68
+ For AWS Device Farm, require `AWS_DEVICE_FARM_ACCESS_KEY_ID`, `AWS_DEVICE_FARM_SECRET_ACCESS_KEY`, and `AWS_DEVICE_FARM_PROJECT_ARN`. Discover targets with:
69
+
70
+ ```sh
71
+ droid-cua --device-source aws-device-farm --list-devices
72
+ ```
73
+
74
+ Do not document BrowserStack CLI runs; BrowserStack is not consistently exposed by the current CLI.
@@ -0,0 +1,38 @@
1
+ # Project files and authentication
2
+
3
+ ## Project files
4
+
5
+ - `.dcua`: executable test instructions. Each non-empty line is one instruction; `//` begins a comment.
6
+ - `context.md`: durable knowledge about product terminology, screens, navigation, and behavior. Pass it with `--context`, or reference it from `ci/droid-cua.json`.
7
+ - `test-data.md`: non-secret entities and scenario inputs. It is used with `--scenario` and `--test-data`; it is not automatically loaded by every normal test run.
8
+ - `.secrets`: dotenv-style values that Droid CUA may type into the tested application. The model receives the keys, not the resolved values. Headless execution loads this file from the current working directory; `--secrets` overrides it.
9
+ - `.env`: infrastructure credentials and runtime environment variables loaded from the current working directory, including `LOADMILL_API_TOKEN`.
10
+ - `ci/droid-cua.json`: optional checked-in headless config. Paths inside it are relative to the config file.
11
+ - `logs/`: structured JSONL and screenshot artifacts created by `--debug`.
12
+ - HTML reports: self-contained evidence produced by `--report`.
13
+
14
+ Keep `.env` and `.secrets` out of version control. Use `git check-ignore -q .env` and `git check-ignore -q .secrets` to check ignore coverage without opening either file.
15
+
16
+ ## Loadmill authentication
17
+
18
+ Loadmill is the default LLM provider. Before a run, check only for presence:
19
+
20
+ ```sh
21
+ test -n "${LOADMILL_API_TOKEN:-}" || grep -Eq '^LOADMILL_API_TOKEN=.+$' .env 2>/dev/null
22
+ ```
23
+
24
+ Run this check from the active test project's working directory. Do not print the variable, run `env`, open `.env`, or ask the user to paste the token. Do not search parent directories, sibling repositories, the user's home directory, or the Droid CUA source repository for credentials.
25
+
26
+ If it is missing, instruct the user to:
27
+
28
+ 1. Sign in at <https://app.loadmill.com>.
29
+ 2. Open **Settings → Security**.
30
+ 3. Create an API token with a description such as `Droid CUA CLI`.
31
+ 4. Copy it when shown; Loadmill shows it once.
32
+ 5. Ensure `.env` is ignored by version control before storing a project token; add `.env` to `.gitignore` if needed.
33
+ 6. Save it as `LOADMILL_API_TOKEN=...` in the active test project's `.env`, a shell environment variable, or a CI secret.
34
+ 7. Confirm when configuration is complete, without sharing the value.
35
+
36
+ Do not create or populate the token value for the user. Do not copy a token from another file or source another project's `.env`. Sourcing an unrelated file can import development endpoints, proxy settings, and unrelated secrets. When the active project's `.env` contains the key, run `droid-cua` normally from that project; the CLI loads that file automatically.
37
+
38
+ If the user explicitly selects direct OpenAI access, use `--llm-provider openai` with one of the supported OpenAI CUA models: `gpt-5.6-terra`, `gpt-5.6-luna`, or `gpt-5.4`. Check only for the presence of `OPENAI_API_KEY`. If it is missing, instruct the user to create an API key in their OpenAI account and save it in the active project's gitignored `.env`, their shell environment, or a CI secret. Apply the same secret-handling rules: never request, print, inspect, copy, or source the key. Do not silently switch providers to bypass missing Loadmill authentication.
@@ -0,0 +1,70 @@
1
+ # Running, monitoring, and guarded healing
2
+
3
+ ## Construct the run
4
+
5
+ Prefer a single test before a directory or scenario run. Preserve an existing provider/model choice in `ci/droid-cua.json`; otherwise use the Loadmill provider. Use Loadmill Smart as the recommended default, or Loadmill Beacon when the user prioritizes faster, lower-cost execution:
6
+
7
+ ```sh
8
+ # Recommended default
9
+ --llm-provider loadmill --cua-model loadmill-smart
10
+
11
+ # Faster, lower-cost option
12
+ --llm-provider loadmill --cua-model loadmill-beacon
13
+ ```
14
+
15
+ Both Loadmill models require `LOADMILL_API_TOKEN`. Do not switch between Smart and Beacon silently; preserve the project's model or explain the speed/cost tradeoff and follow the user's choice. If the user chooses direct OpenAI access instead, require `OPENAI_API_KEY` and use `--llm-provider openai` with one of `gpt-5.6-terra`, `gpt-5.6-luna`, or `gpt-5.4`. Do not mix a provider with another provider's model, silently switch providers, or override an existing project choice without explaining it.
16
+
17
+ Include `--report reports/<test-name>.html` for normal test runs unless the user opts out. Prefer an explicit filename over a directory so the result is easy to locate. Add `--debug` when diagnosis may be needed.
18
+
19
+ ```sh
20
+ # Attached Android device or running emulator
21
+ droid-cua --llm-provider loadmill --cua-model loadmill-smart --avd adb:<serial> --instructions tests/example.dcua --context tests/context.md --debug --report reports/example.html
22
+
23
+ # Launchable Android AVD
24
+ droid-cua --avd avd:<avd-name> --instructions tests/example.dcua --config ci/droid-cua.json --debug --report reports/example.html
25
+
26
+ # iOS Simulator
27
+ droid-cua --llm-provider loadmill --cua-model loadmill-smart --platform ios --avd "<simulator-name>" --instructions tests/example.dcua --debug --report reports/example.html
28
+
29
+ # Local web
30
+ droid-cua --llm-provider loadmill --cua-model loadmill-smart --target web --browser chrome --session-mode fresh --instructions tests/example.dcua --debug --report reports/example.html
31
+
32
+ # LambdaTest
33
+ droid-cua --llm-provider loadmill --cua-model loadmill-smart --device-source lambdatest --platform android --device-name "<name>" --os-version "<version>" --app <app.apk> --instructions tests/example.dcua --debug --report reports/example.html
34
+
35
+ # AWS Device Farm
36
+ droid-cua --llm-provider loadmill --cua-model loadmill-smart --device-source aws-device-farm --platform android --device-name "<name>" --os-version "<version>" --app <app.apk> --instructions tests/example.dcua --debug --report reports/example.html
37
+
38
+ # Direct OpenAI alternative
39
+ droid-cua --llm-provider openai --cua-model gpt-5.4 --avd adb:<serial> --instructions tests/example.dcua --context tests/context.md --debug --report reports/example.html
40
+ ```
41
+
42
+ Use `.ipa` and `--platform ios` for supported cloud iOS runs. Preserve existing config flags rather than replacing them with defaults.
43
+
44
+ ## Monitor and collect evidence
45
+
46
+ Remain attached until the command exits and cloud cleanup finishes. Read terminal progress as it appears; do not treat a long model step as success or failure before completion.
47
+
48
+ - Exit `0`: all requested tests passed.
49
+ - Exit `1`: configuration, connection, execution, or assertion failure.
50
+ - Exit `130`: interrupted run.
51
+ - `--debug`: writes structured execution JSONL and adjacent screenshots under `logs/`.
52
+ - `--report`: writes a self-contained HTML report. Prefer an explicit `.html` file path such as `reports/example.html` so the evidence location is unambiguous.
53
+
54
+ After the run, verify that the report exists and resolve its absolute path. Open it for the user when the agent environment supports local-file opening and policy permits; otherwise provide a clickable local-file link or the absolute path so the user can open it directly.
55
+
56
+ Report the outcome, failing instruction and first divergence when applicable, relevant error text, screenshot/debug/report paths, target, provider/model, and command with secret values omitted.
57
+
58
+ ## Classify the first divergence
59
+
60
+ 1. **Setup or authentication**: missing/invalid provider credential, dependency, license, browser, or executable. Return to onboarding and rerun only the failed check.
61
+ 2. **Target connectivity**: no device, unauthorized/offline ADB state, unavailable simulator, cloud credential/device/app problem, or lost session. Restore the same target, then rerun.
62
+ 3. **Test ambiguity or stale test knowledge**: instruction has multiple interpretations, relies on an unstable label, lacks a starting condition, or context contradicts the visible product. A minimal test/context correction is allowed.
63
+ 4. **Application regression**: the app crashes, displays an error, violates an explicit requirement, or consistently fails a stable assertion after correct setup. Stop and report a bug with evidence.
64
+ 5. **Transient execution**: temporary network, service, model, or timing failure without evidence of a deterministic product/test issue. Retry the unchanged targeted test once.
65
+
66
+ ## Guarded healing
67
+
68
+ Before editing, state the classification and evidence. Change only `.dcua`, `context.md`, `test-data.md`, or Droid CUA config. Never edit app code, delete coverage, loosen the expected outcome, or add broad waits simply to obtain green output.
69
+
70
+ Make one minimal change, explain why it corrects the test rather than hides a product defect, and rerun only the affected test. Allow at most two edit/rerun cycles. If it still fails, stop with the original and final evidence, changes attempted, and recommended owner/action.
@@ -1,11 +0,0 @@
1
- export const CUA_BACKENDS = ["openai", "fara"];
2
- export const DEFAULT_CUA_BACKEND = "openai";
3
- export const DEFAULT_FARA_BASE_URL = "http://localhost:5000/v1";
4
- export const DEFAULT_FARA_MODEL = "Fara-7B";
5
- const VALID_CUA_BACKENDS = new Set(CUA_BACKENDS);
6
- export function isValidCuaBackend(value) {
7
- return VALID_CUA_BACKENDS.has(value);
8
- }
9
- export function resolveCuaBackend(value) {
10
- return isValidCuaBackend(value) ? value : DEFAULT_CUA_BACKEND;
11
- }