@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.
- package/README.md +25 -0
- package/build/index.js +43 -1
- package/build/src/browser/playwright/actions.js +65 -1
- package/build/src/cli/skill-command.js +240 -0
- package/build/src/commands/help.js +2 -1
- package/build/src/core/cua-models.js +29 -11
- package/build/src/core/droid-cua-run-usage.js +16 -7
- package/build/src/core/execution-engine.js +334 -5
- package/build/src/core/prompts.js +1 -1
- package/build/src/core/report/render-html-report.js +8 -3
- package/build/src/core/report/run-log-model.js +42 -0
- package/build/src/core/runtime-config.js +23 -41
- package/build/src/core/session.js +6 -0
- package/build/src/core/tool-handlers/loadmill.js +3 -0
- package/build/src/core/tool-handlers/test-design-complete.js +1 -0
- package/build/src/core/tool-handlers/type-sensitive-text.js +1 -0
- package/build/src/device/android/actions.js +6 -2
- package/build/src/device/android/connection.js +58 -23
- package/build/src/device/assertions.js +15 -8
- package/build/src/device/ios/connection.js +36 -30
- package/build/src/device/openai.js +68 -2
- package/build/src/device/scroll-gesture.js +22 -9
- package/build/src/integrations/loadmill/client.js +20 -4
- package/build/src/modes/design-mode-ink.js +10 -1
- package/build/src/modes/design-mode.js +7 -1
- package/build/src/modes/execution-mode.js +84 -8
- package/build/src/prompts/design.js +21 -1
- package/build/src/utils/console-output.js +5 -0
- package/build/src/utils/cua-debug-tracer.js +50 -7
- package/package.json +2 -1
- package/skill/droid-cua/SKILL.md +36 -0
- package/skill/droid-cua/agents/openai.yaml +6 -0
- package/skill/droid-cua/assets/droid-cua-app-icon.png +0 -0
- package/skill/droid-cua/references/authoring-tests.md +39 -0
- package/skill/droid-cua/references/onboarding-and-troubleshooting.md +74 -0
- package/skill/droid-cua/references/project-and-auth.md +38 -0
- package/skill/droid-cua/references/running-and-healing.md +70 -0
- 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
|
|
12
|
-
// in the server repo)
|
|
13
|
-
//
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
Binary file
|
|
@@ -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
|
-
}
|