@loadmill/droid-cua 2.25.0 → 2.27.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 +13 -5
- package/build/index.js +210 -2
- package/build/src/cli/cloud-status-line.js +58 -26
- package/build/src/cli/headless-execution-config.js +11 -0
- package/build/src/cli/headless-report.js +105 -15
- package/build/src/cli/headless-runner.js +125 -24
- package/build/src/cli/headless-target.js +63 -7
- package/build/src/cli/loadmill-cloud-credentials.js +11 -0
- package/build/src/cli/loadmill-cloud-usage.js +18 -0
- package/build/src/commands/help.js +5 -0
- package/build/src/core/cua-models.js +7 -0
- package/build/src/core/droid-cua-run-usage.js +8 -2
- package/build/src/core/execution-engine.js +6 -2
- package/build/src/core/report/render-html-report.js +69 -12
- package/build/src/core/runtime-config.js +10 -4
- package/build/src/device/assertions.js +3 -0
- package/build/src/device/cloud/aws-device-farm/upload-cache.js +1 -1
- package/build/src/device/cloud/connection.js +18 -6
- package/build/src/device/cloud/loadmill-cloud/adapter.js +537 -0
- package/build/src/device/cloud/loadmill-cloud/artifact-downloader.js +191 -0
- package/build/src/device/cloud/loadmill-cloud/control-plane-client.js +423 -0
- package/build/src/device/cloud/loadmill-cloud/errors.js +16 -0
- package/build/src/device/cloud/loadmill-cloud/format-bytes.js +18 -0
- package/build/src/device/cloud/loadmill-cloud/timing.js +14 -0
- package/build/src/device/cloud/progress.js +29 -0
- package/build/src/device/cloud/registry.js +2 -1
- package/build/src/device/connection.js +3 -0
- package/build/src/device/openai.js +1 -0
- package/build/src/integrations/loadmill/client.js +14 -0
- package/build/src/integrations/loadmill/report-upload-manager.js +221 -0
- package/build/src/integrations/loadmill/run-reporter.js +6 -2
- package/build/src/modes/execution-mode.js +2 -1
- package/build/src/prompts/design.js +9 -1
- package/build/src/prompts/execution.js +10 -7
- package/build/src/utils/console-output.js +3 -0
- package/package.json +1 -1
- package/skill/droid-cua/references/running-and-healing.md +3 -3
|
@@ -18,10 +18,13 @@ const MODEL_PRICING = Object.freeze({
|
|
|
18
18
|
"gpt-5.4": Object.freeze({
|
|
19
19
|
...OPENAI_GPT_5_4_PRICING,
|
|
20
20
|
}),
|
|
21
|
-
// Loadmill Smart currently routes to GPT-5.
|
|
21
|
+
// Loadmill Smart currently routes to GPT-5.6 Terra behind the Loadmill LLM gateway.
|
|
22
22
|
// Keep this explicit so pricing can be updated here if the backing model changes.
|
|
23
23
|
"loadmill-smart": Object.freeze({
|
|
24
|
-
...
|
|
24
|
+
...OPENAI_GPT_5_6_TERRA_PRICING,
|
|
25
|
+
}),
|
|
26
|
+
"loadmill-pulse": Object.freeze({
|
|
27
|
+
...OPENAI_GPT_5_6_LUNA_PRICING,
|
|
25
28
|
}),
|
|
26
29
|
});
|
|
27
30
|
function finiteNumber(value) {
|
|
@@ -50,6 +53,9 @@ export function normalizeDroidCuaCostModel(model) {
|
|
|
50
53
|
if (model === "loadmill-smart" || model === "smart") {
|
|
51
54
|
return "loadmill-smart";
|
|
52
55
|
}
|
|
56
|
+
if (model === "loadmill-pulse" || model === "pulse") {
|
|
57
|
+
return "loadmill-pulse";
|
|
58
|
+
}
|
|
53
59
|
return null;
|
|
54
60
|
}
|
|
55
61
|
export function createDroidCuaRunUsageTotals({ model = null } = {}) {
|
|
@@ -251,6 +251,7 @@ export class ExecutionEngine {
|
|
|
251
251
|
this.navigateToUrl = options.navigateToUrl || navigateBrowserToUrl;
|
|
252
252
|
this.closeCurrentTab = options.closeCurrentTab || closeCurrentBrowserTab;
|
|
253
253
|
this.executeLoadmillInstruction = options.executeLoadmillInstruction || executeLoadmillInstruction;
|
|
254
|
+
this.connectToDevice = options.connectToDevice || null;
|
|
254
255
|
this.toolCapabilities = options.toolCapabilities || {};
|
|
255
256
|
this.secretsByKey = options.secretsByKey && typeof options.secretsByKey === "object" ? options.secretsByKey : {};
|
|
256
257
|
this.baseSystemPrompt = options.baseSystemPrompt || session?.systemPrompt || null;
|
|
@@ -525,7 +526,7 @@ export class ExecutionEngine {
|
|
|
525
526
|
type: "assistant",
|
|
526
527
|
text: textPart.text,
|
|
527
528
|
eventType: "assistant_message",
|
|
528
|
-
...eventMeta(),
|
|
529
|
+
...eventMeta(options.explainMode ? { payload: { explainMode: true } } : {}),
|
|
529
530
|
});
|
|
530
531
|
}
|
|
531
532
|
this.session.addToTranscript(`[Assistant] ${textPart.text}`);
|
|
@@ -688,7 +689,10 @@ export class ExecutionEngine {
|
|
|
688
689
|
const items = response.output || [];
|
|
689
690
|
const actionableItems = extractActionableOutputItems(items);
|
|
690
691
|
const pendingSafetyChecks = collectPendingSafetyChecks(items);
|
|
691
|
-
this.emitResponseOutputItems(items, addOutput, eventMeta,
|
|
692
|
+
this.emitResponseOutputItems(items, addOutput, eventMeta, {
|
|
693
|
+
...options,
|
|
694
|
+
explainMode: options.explainMode === true || context?.runtimeConfig?.explainMode === true,
|
|
695
|
+
});
|
|
692
696
|
if (actionableItems.length === 0) {
|
|
693
697
|
// No actions = turn complete
|
|
694
698
|
break;
|
|
@@ -83,13 +83,60 @@ function buildVisibleEvents(events) {
|
|
|
83
83
|
// full-size lightbox overlay can be rendered. The href links to the CSS `:target`
|
|
84
84
|
// overlay, so click-to-enlarge works even with JavaScript disabled.
|
|
85
85
|
function screenshotImg(event, shots) {
|
|
86
|
-
const url = event.payload?.screenshotDataUrl;
|
|
87
|
-
|
|
86
|
+
const url = event.payload?.screenshotDataUrl ?? event.payload?.screenshotUrl;
|
|
87
|
+
const isAllowedUrl = typeof url === "string" &&
|
|
88
|
+
(url.startsWith("data:") || url.startsWith("/api/droid-cua/runs/"));
|
|
89
|
+
if (!isAllowedUrl)
|
|
88
90
|
return "";
|
|
89
91
|
const index = shots.length + 1;
|
|
90
92
|
shots.push({ index, url });
|
|
91
93
|
return `<a class="dc-shot-link" href="#dc-shot-${index}" aria-label="Open screenshot ${index}"><img class="dc-shot" src="${escapeHtml(url)}" alt="Screenshot ${index}" /></a>`;
|
|
92
94
|
}
|
|
95
|
+
function icon(name, shapes) {
|
|
96
|
+
return `<svg class="dc-icon" data-icon="${name}" aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${shapes}</svg>`;
|
|
97
|
+
}
|
|
98
|
+
const ICONS = {
|
|
99
|
+
arrowUpDown: icon("arrow-up-down", `<path d="m21 16-4 4-4-4"/><path d="M17 20V4"/><path d="m3 8 4-4 4 4"/><path d="M7 4v16"/>`),
|
|
100
|
+
botMessageSquare: icon("bot-message-square", `<path d="M12 6V2H8"/><path d="m8 18-4 4V8a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2Z"/><path d="M2 12h2"/><path d="M9 11v2"/><path d="M15 11v2"/><path d="M20 12h2"/>`),
|
|
101
|
+
camera: icon("camera", `<path d="M14.5 4h-5L7 7H4a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2h-3l-2.5-3z"/><circle cx="12" cy="13" r="3"/>`),
|
|
102
|
+
compass: icon("compass", `<path d="m16.24 7.76-1.804 5.411a2 2 0 0 1-1.265 1.265L7.76 16.24l1.804-5.411a2 2 0 0 1 1.265-1.265z"/><circle cx="12" cy="12" r="10"/>`),
|
|
103
|
+
flag: icon("flag", `<path d="M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z"/><line x1="4" x2="4" y1="22" y2="15"/>`),
|
|
104
|
+
keyboard: icon("keyboard", `<path d="M10 8h.01"/><path d="M12 12h.01"/><path d="M14 8h.01"/><path d="M16 12h.01"/><path d="M18 8h.01"/><path d="M6 8h.01"/><path d="M7 16h10"/><path d="M8 12h.01"/><rect width="20" height="16" x="2" y="4" rx="2"/>`),
|
|
105
|
+
keyboardOff: icon("keyboard-off", `<path d="M20 4a2 2 0 0 1 2 2"/><path d="M22 6v10.41"/><path d="M7 16h9"/><path d="M9.69 4H20"/><path d="M14 8h.01"/><path d="M18 8h.01"/><path d="m2 2 20 20"/><path d="M20 20H4a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2"/><path d="M6 8h.01"/><path d="M8 12h.01"/>`),
|
|
106
|
+
keyRound: icon("key-round", `<path d="M2.586 17.414A2 2 0 0 0 2 18.828V21a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h1a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h.172a2 2 0 0 0 1.414-.586l.814-.814a6.5 6.5 0 1 0-4-4z"/><circle cx="16.5" cy="7.5" r=".5" fill="currentColor"/>`),
|
|
107
|
+
mousePointer: icon("mouse-pointer-2", `<path d="M4.037 4.688a.495.495 0 0 1 .651-.651l16 6.5a.5.5 0 0 1-.063.947l-6.124 1.58a2 2 0 0 0-1.438 1.435l-1.579 6.126a.5.5 0 0 1-.947.063z"/>`),
|
|
108
|
+
moveDiagonal: icon("move-diagonal-2", `<path d="M19 13v6h-6"/><path d="M5 11V5h6"/><path d="m5 5 14 14"/>`),
|
|
109
|
+
rotateCcw: icon("rotate-ccw", `<path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/>`),
|
|
110
|
+
squareX: icon("square-x", `<rect width="18" height="18" x="3" y="3" rx="2"/><path d="m15 9-6 6"/><path d="m9 9 6 6"/>`),
|
|
111
|
+
timer: icon("timer", `<line x1="10" x2="14" y1="2" y2="2"/><line x1="12" x2="15" y1="14" y2="11"/><circle cx="12" cy="14" r="8"/>`),
|
|
112
|
+
};
|
|
113
|
+
function actionIcon(event) {
|
|
114
|
+
const lowerText = event.text.toLowerCase();
|
|
115
|
+
const toolName = typeof event.payload?.toolName === "string" ? event.payload.toolName : null;
|
|
116
|
+
const isSensitiveTyping = Boolean(event.payload?.sensitive) || lowerText.includes("typing secret");
|
|
117
|
+
if (toolName === "navigate")
|
|
118
|
+
return ICONS.compass;
|
|
119
|
+
if (toolName === "close_current_tab")
|
|
120
|
+
return ICONS.squareX;
|
|
121
|
+
if (toolName === "close_keyboard")
|
|
122
|
+
return ICONS.keyboardOff;
|
|
123
|
+
if (isSensitiveTyping)
|
|
124
|
+
return ICONS.keyRound;
|
|
125
|
+
if (event.actionType === "type" || event.actionType === "keypress" || lowerText.includes("typing"))
|
|
126
|
+
return ICONS.keyboard;
|
|
127
|
+
if (event.actionType === "scroll" || lowerText.includes("scroll"))
|
|
128
|
+
return ICONS.arrowUpDown;
|
|
129
|
+
if (event.actionType === "drag" || lowerText.includes("drag"))
|
|
130
|
+
return ICONS.moveDiagonal;
|
|
131
|
+
if (event.actionType === "screenshot" || lowerText.includes("capturing screen"))
|
|
132
|
+
return ICONS.camera;
|
|
133
|
+
if (event.actionType === "wait" || lowerText.includes("waiting"))
|
|
134
|
+
return ICONS.timer;
|
|
135
|
+
return ICONS.mousePointer;
|
|
136
|
+
}
|
|
137
|
+
function renderAgentText(text) {
|
|
138
|
+
return `<div class="dc-line dc-agent-line">${ICONS.botMessageSquare}<span>${escapeHtml(text)}</span></div>`;
|
|
139
|
+
}
|
|
93
140
|
// ---------- per-event timeline rendering (mirrors ExecutionLogItem.tsx) ----------
|
|
94
141
|
function renderEvent(event, shots) {
|
|
95
142
|
if (isHiddenExecutionLogEvent(event))
|
|
@@ -123,10 +170,16 @@ function renderEvent(event, shots) {
|
|
|
123
170
|
if (isAssistant) {
|
|
124
171
|
const stepCompletion = stepCompletionData(event);
|
|
125
172
|
if (stepCompletion) {
|
|
173
|
+
if (event.payload?.explainMode === true) {
|
|
174
|
+
return `<div class="dc-line">${ICONS.flag}<span>Step complete</span></div>\n${renderAgentText(stepCompletion.reason)}`;
|
|
175
|
+
}
|
|
126
176
|
const tooltip = stepCompletionReasonTooltip(stepCompletion);
|
|
127
|
-
return `<div class="dc-line"${tooltip ? ` title="${escapeHtml(tooltip)}"` : ""}
|
|
177
|
+
return `<div class="dc-line"${tooltip ? ` title="${escapeHtml(tooltip)}"` : ""}>${ICONS.flag}<span>Step complete</span></div>`;
|
|
128
178
|
}
|
|
129
|
-
return
|
|
179
|
+
return renderAgentText(event.text);
|
|
180
|
+
}
|
|
181
|
+
if (event.eventType === "reasoning" || event.kind === "reasoning") {
|
|
182
|
+
return renderAgentText(event.text);
|
|
130
183
|
}
|
|
131
184
|
const assertion = assertionData(event);
|
|
132
185
|
if (assertion) {
|
|
@@ -138,13 +191,13 @@ function renderEvent(event, shots) {
|
|
|
138
191
|
</div>`;
|
|
139
192
|
}
|
|
140
193
|
if (event.eventType === "retry") {
|
|
141
|
-
return `<div class="dc-line dc-retry">${escapeHtml(event.text)}</div>`;
|
|
194
|
+
return `<div class="dc-line dc-retry">${ICONS.rotateCcw}<span>${escapeHtml(event.text)}</span></div>`;
|
|
142
195
|
}
|
|
143
196
|
if (event.kind === "action") {
|
|
144
|
-
return `<div class="dc-line">${escapeHtml(event.text)}
|
|
197
|
+
return `<div class="dc-line">${actionIcon(event)}<span>${escapeHtml(event.text)}</span>${screenshotImg(event, shots)}</div>`;
|
|
145
198
|
}
|
|
146
199
|
if (event.eventType === "screenshot_captured") {
|
|
147
|
-
return `<div class="dc-line">${escapeHtml(event.text)}
|
|
200
|
+
return `<div class="dc-line">${ICONS.camera}<span>${escapeHtml(event.text)}</span>${screenshotImg(event, shots)}</div>`;
|
|
148
201
|
}
|
|
149
202
|
return `<div class="dc-line dc-${escapeHtml(event.kind)}">${escapeHtml(event.text)}</div>`;
|
|
150
203
|
}
|
|
@@ -243,13 +296,16 @@ body { margin: 0; background: #f1f5f9; color: #0f172a; font-family: -apple-syste
|
|
|
243
296
|
.dc-row { display: flex; }
|
|
244
297
|
.dc-row-right { justify-content: flex-end; }
|
|
245
298
|
.dc-bubble { display: inline-block; max-width: 70%; background: #f1f5f9; border: 1px solid #e2e8f0; border-radius: 16px; border-bottom-right-radius: 6px; padding: 8px 14px; color: #0f172a; }
|
|
246
|
-
.dc-assistant { max-width: 82%; color: #334155; }
|
|
247
299
|
.dc-line { display: flex; align-items: center; gap: 8px; color: #475569; }
|
|
300
|
+
.dc-agent-line { align-items: flex-start; }
|
|
301
|
+
.dc-agent-line .dc-icon { margin-top: 4px; }
|
|
302
|
+
.dc-icon { width: 14px; height: 14px; flex: 0 0 auto; color: #94a3b8; }
|
|
248
303
|
.dc-retry { color: #b45309; }
|
|
249
304
|
.dc-error { color: #b91c1c; }
|
|
250
305
|
.dc-warning { color: #b45309; }
|
|
251
|
-
.dc-shot-link { display: inline-flex; margin-left: 8px; vertical-align: middle; }
|
|
252
|
-
.dc-shot {
|
|
306
|
+
.dc-shot-link { display: inline-flex; align-items: center; justify-content: center; position: relative; min-width: 34px; min-height: 36px; margin-left: 8px; vertical-align: middle; }
|
|
307
|
+
.dc-shot-link::before, .dc-lightbox:target::before { content: ""; position: absolute; width: 14px; height: 14px; border: 2px solid #cbd5e1; border-top-color: #475569; border-radius: 50%; animation: dc-spin .7s linear infinite; }
|
|
308
|
+
.dc-shot { position: relative; z-index: 1; height: 34px; width: auto; border: 1px solid #e2e8f0; border-radius: 4px; cursor: zoom-in; transition: border-color .12s; }
|
|
253
309
|
.dc-shot-link:hover .dc-shot, .dc-shot-link:focus-visible .dc-shot { border-color: #94a3b8; }
|
|
254
310
|
.dc-lightbox { display: none; position: fixed; inset: 0; z-index: 1000; align-items: center; justify-content: center; padding: 32px; background: rgba(15, 23, 42, .82); }
|
|
255
311
|
.dc-lightbox:target { display: flex; }
|
|
@@ -275,9 +331,10 @@ body { margin: 0; background: #f1f5f9; color: #0f172a; font-family: -apple-syste
|
|
|
275
331
|
.dc-pill-error, .dc-pill-errored, .dc-pill-amber { border-color: #fcd34d; background: #fef3c7; color: #b45309; }
|
|
276
332
|
.dc-pill-stopped { border-color: #fdba74; background: #ffedd5; color: #c2410c; }
|
|
277
333
|
.dc-pill-skipped { border-color: #cbd5e1; background: #f1f5f9; color: #334155; }
|
|
334
|
+
@keyframes dc-spin { to { transform: rotate(360deg); } }
|
|
278
335
|
`.trim();
|
|
279
336
|
// ---------- entry point ----------
|
|
280
|
-
export function renderHtmlReport(model, { title } = {}) {
|
|
337
|
+
export function renderHtmlReport(model, { title, includeScripts = true } = {}) {
|
|
281
338
|
const docTitle = title ?? `Run Report: ${model.testName ?? model.suiteName ?? "Test run"}`;
|
|
282
339
|
const heading = model.testName ?? model.suiteName ?? "Test run";
|
|
283
340
|
// Screenshots are collected across the whole document (top-level + child timelines)
|
|
@@ -301,7 +358,7 @@ ${renderTimeline(model.events, shots)}
|
|
|
301
358
|
</div>`;
|
|
302
359
|
}
|
|
303
360
|
const lightboxes = renderLightboxes(shots);
|
|
304
|
-
const script = renderLightboxScript(shots.length);
|
|
361
|
+
const script = includeScripts ? renderLightboxScript(shots.length) : "";
|
|
305
362
|
return `<!DOCTYPE html>
|
|
306
363
|
<html lang="en">
|
|
307
364
|
<head>
|
|
@@ -7,6 +7,7 @@ export const LOADMILL_MODEL_ALIASES = Object.freeze({
|
|
|
7
7
|
DEFAULT_AGENT_MODEL: "default-agent-model",
|
|
8
8
|
FAST: "fast",
|
|
9
9
|
SMART: "smart",
|
|
10
|
+
PULSE: "pulse",
|
|
10
11
|
BEACON: "beacon",
|
|
11
12
|
});
|
|
12
13
|
const LOADMILL_MODEL_ALIAS_SET = new Set(Object.values(LOADMILL_MODEL_ALIASES));
|
|
@@ -90,9 +91,13 @@ export function inferLlmProviderModeFromCredentials(credentials = {}) {
|
|
|
90
91
|
}
|
|
91
92
|
export function resolveGatewayCuaModelAlias({ cuaModel } = {}) {
|
|
92
93
|
const normalizedCuaModel = resolveCuaModel(cuaModel);
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
94
|
+
if (normalizedCuaModel === "loadmill-pulse") {
|
|
95
|
+
return LOADMILL_MODEL_ALIASES.PULSE;
|
|
96
|
+
}
|
|
97
|
+
if (normalizedCuaModel === "loadmill-beacon") {
|
|
98
|
+
return LOADMILL_MODEL_ALIASES.BEACON;
|
|
99
|
+
}
|
|
100
|
+
return LOADMILL_MODEL_ALIASES.SMART;
|
|
96
101
|
}
|
|
97
102
|
export function resolveEffectiveCuaModel({ llmProviderMode = null, cuaModel = null, } = {}) {
|
|
98
103
|
const providerMode = resolveLlmProviderMode(llmProviderMode);
|
|
@@ -118,7 +123,7 @@ export function resolveRuntimeCredentials({ openAiApiKey = null, loadmillApiToke
|
|
|
118
123
|
loadmillBaseUrl: normalizeLoadmillSiteBaseUrl(loadmillBaseUrl),
|
|
119
124
|
};
|
|
120
125
|
}
|
|
121
|
-
export function resolveRuntimeConfig({ llmProviderMode = null, cuaModel = null, stepDelayMs = undefined, contextOptimizationEnabled = undefined, contextOptimizationThreshold = undefined, promptCustomizations = null, appContext = null, } = {}) {
|
|
126
|
+
export function resolveRuntimeConfig({ llmProviderMode = null, cuaModel = null, explainMode = false, stepDelayMs = undefined, contextOptimizationEnabled = undefined, contextOptimizationThreshold = undefined, promptCustomizations = null, appContext = null, } = {}) {
|
|
122
127
|
const normalizedCuaModel = typeof cuaModel === "string" && LOADMILL_MODEL_ALIAS_SET.has(cuaModel)
|
|
123
128
|
? cuaModel
|
|
124
129
|
: resolveCuaModel(cuaModel);
|
|
@@ -128,6 +133,7 @@ export function resolveRuntimeConfig({ llmProviderMode = null, cuaModel = null,
|
|
|
128
133
|
? "loadmill"
|
|
129
134
|
: normalizedProviderMode,
|
|
130
135
|
cuaModel: normalizedCuaModel,
|
|
136
|
+
explainMode: explainMode === true,
|
|
131
137
|
stepDelayMs: normalizeStepDelayMs(Number(stepDelayMs)),
|
|
132
138
|
contextOptimizationEnabled: normalizeContextOptimizationEnabled(contextOptimizationEnabled),
|
|
133
139
|
contextOptimizationThreshold: normalizeContextOptimizationThreshold(contextOptimizationThreshold),
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Assertion handling for script validation
|
|
3
3
|
*/
|
|
4
4
|
import { printCliOutput } from "../utils/console-output.js";
|
|
5
|
+
import { CAPTURE_SOURCES } from "../core/capture-sources.js";
|
|
5
6
|
export { buildAssertionSystemPrompt } from "../prompts/execution.js";
|
|
6
7
|
const ASSERTION_PREFIXES = ["assert", "verify", "validate", "ensure"];
|
|
7
8
|
function hasAssertionPrefix(lower) {
|
|
@@ -54,6 +55,7 @@ export function handleAssertionFailure(assertionPrompt, transcript, isHeadlessMo
|
|
|
54
55
|
assertion: assertionPrompt,
|
|
55
56
|
passed: false,
|
|
56
57
|
details,
|
|
58
|
+
captureSource: CAPTURE_SOURCES.INSTRUCTION_INPUT_ASSERTION,
|
|
57
59
|
...eventPayload
|
|
58
60
|
}
|
|
59
61
|
};
|
|
@@ -77,6 +79,7 @@ export function handleAssertionSuccess(assertionPrompt, context = null, stepCont
|
|
|
77
79
|
payload: {
|
|
78
80
|
assertion: assertionPrompt,
|
|
79
81
|
passed: true,
|
|
82
|
+
captureSource: CAPTURE_SOURCES.INSTRUCTION_INPUT_ASSERTION,
|
|
80
83
|
...eventPayload
|
|
81
84
|
}
|
|
82
85
|
});
|
|
@@ -7,7 +7,7 @@ import { reportCloudProgress } from "../progress.js";
|
|
|
7
7
|
export const DEFAULT_UPLOAD_CACHE_PATH = path.join(os.tmpdir(), "droid-cua", "aws-device-farm-upload-cache.json");
|
|
8
8
|
// Stream the binary through the hash rather than buffering the whole .apk/.ipa
|
|
9
9
|
// (tens–hundreds of MB) in one allocation just to checksum it.
|
|
10
|
-
async function hashFile(filePath) {
|
|
10
|
+
export async function hashFile(filePath) {
|
|
11
11
|
const hash = createHash("sha256");
|
|
12
12
|
for await (const chunk of createReadStream(filePath)) {
|
|
13
13
|
hash.update(chunk);
|
|
@@ -4,7 +4,7 @@ import { CloudAppiumClient } from "./cloud-appium-client.js";
|
|
|
4
4
|
import { reportCloudPhase } from "./progress.js";
|
|
5
5
|
let activeSession = null;
|
|
6
6
|
function buildCloudCapabilities(options) {
|
|
7
|
-
const { providerId, platform, deviceName, osVersion, appRef, credentials, buildName, sessionName } = options;
|
|
7
|
+
const { providerId, platform, deviceName, osVersion, appRef, credentials, buildName, sessionName, cloudSessionId, artifacts } = options;
|
|
8
8
|
const adapter = getCloudProviderAdapter(providerId);
|
|
9
9
|
if (!adapter) {
|
|
10
10
|
throw new Error(`Cloud provider "${providerId}" is not registered.`);
|
|
@@ -20,7 +20,9 @@ function buildCloudCapabilities(options) {
|
|
|
20
20
|
accessKey: credentials?.accessKey,
|
|
21
21
|
projectName: "droid-cua",
|
|
22
22
|
buildName: buildName || `desktop-${new Date().toISOString().slice(0, 10)}`,
|
|
23
|
-
sessionName: sessionName || `${deviceName} (${platform})
|
|
23
|
+
sessionName: sessionName || `${deviceName} (${platform})`,
|
|
24
|
+
cloudSessionId,
|
|
25
|
+
artifacts
|
|
24
26
|
}
|
|
25
27
|
};
|
|
26
28
|
}
|
|
@@ -29,11 +31,19 @@ export async function connectToDevice(options, logOptions = {}) {
|
|
|
29
31
|
if (!options || typeof options !== "object") {
|
|
30
32
|
throw new Error("Cloud device connection requires a provider payload.");
|
|
31
33
|
}
|
|
32
|
-
const { providerId, platform, deviceName, osVersion, appRef, credentials, buildName, sessionName } = options;
|
|
33
|
-
|
|
34
|
+
const { providerId, platform, deviceName, osVersion, appRef, credentials, buildName, sessionName, cloudSessionId, artifacts } = options;
|
|
35
|
+
const isReconnect = providerId === "loadmill-cloud" && Boolean(cloudSessionId);
|
|
36
|
+
if (!providerId ||
|
|
37
|
+
(!isReconnect && (!platform || !deviceName || !osVersion)) ||
|
|
38
|
+
(!appRef && !isReconnect)) {
|
|
34
39
|
throw new Error("Cloud device connection is missing required session details.");
|
|
35
40
|
}
|
|
36
|
-
if (
|
|
41
|
+
if (providerId === "loadmill-cloud") {
|
|
42
|
+
if (!credentials?.bearerToken) {
|
|
43
|
+
throw new Error("Loadmill Cloud connection is missing a Loadmill API token.");
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
else if (!credentials?.username || !credentials?.accessKey) {
|
|
37
47
|
throw new Error("Cloud device connection is missing provider credentials.");
|
|
38
48
|
}
|
|
39
49
|
const { adapter, capabilities } = buildCloudCapabilities({
|
|
@@ -44,7 +54,9 @@ export async function connectToDevice(options, logOptions = {}) {
|
|
|
44
54
|
appRef,
|
|
45
55
|
credentials,
|
|
46
56
|
buildName,
|
|
47
|
-
sessionName
|
|
57
|
+
sessionName,
|
|
58
|
+
cloudSessionId,
|
|
59
|
+
artifacts
|
|
48
60
|
});
|
|
49
61
|
// Optional AbortSignal (Stop/Pause during provisioning). It rides inside the
|
|
50
62
|
// cloud-options object — see the facade in src/device/connection.js — so no
|