@loadmill/droid-cua 2.19.0 → 2.22.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 +52 -0
- package/build/index.js +109 -4
- package/build/src/browser/playwright/welcome-page.js +20 -0
- package/build/src/cli/aws-device-farm-app-upload.js +76 -0
- package/build/src/cli/cloud-status-line.js +98 -0
- package/build/src/cli/headless-runner.js +180 -17
- package/build/src/cli/headless-target.js +57 -22
- package/build/src/commands/loadmill.js +8 -1
- package/build/src/core/droid-cua-run-usage.js +106 -0
- package/build/src/core/droid-cua-segment-events.js +32 -0
- package/build/src/core/report/run-log-model.js +4 -0
- package/build/src/device/cloud/aws-device-farm/adapter.js +388 -0
- package/build/src/device/cloud/aws-device-farm/upload-cache.js +42 -0
- package/build/src/device/cloud/cloud-appium-client.js +2 -1
- package/build/src/device/cloud/connection.js +71 -14
- package/build/src/device/cloud/progress.js +24 -0
- package/build/src/device/cloud/registry.js +2 -1
- package/build/src/device/connection.js +3 -2
- package/build/src/device/loadmill.js +11 -0
- package/build/src/integrations/loadmill/client.js +46 -0
- package/build/src/integrations/loadmill/executor.js +14 -6
- package/build/src/integrations/loadmill/interpreter.js +4 -4
- package/build/src/integrations/loadmill/run-reporter.js +113 -0
- package/build/src/integrations/loadmill/session-user.js +39 -0
- package/build/src/integrations/segment/droid-cua-segment.js +64 -0
- package/package.json +5 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Loadmill API client for interacting with test flows
|
|
3
3
|
*/
|
|
4
|
+
import { extractLoadmillSessionUser } from "./session-user.js";
|
|
4
5
|
const DEFAULT_BASE_URL = "https://app.loadmill.com/api";
|
|
5
6
|
function normalizeApiBaseUrl(rawBaseUrl) {
|
|
6
7
|
const candidate = (rawBaseUrl || DEFAULT_BASE_URL).trim();
|
|
@@ -19,6 +20,10 @@ function normalizeApiBaseUrl(rawBaseUrl) {
|
|
|
19
20
|
return DEFAULT_BASE_URL;
|
|
20
21
|
}
|
|
21
22
|
}
|
|
23
|
+
function normalizeSiteBaseUrl(rawBaseUrl) {
|
|
24
|
+
const apiBaseUrl = normalizeApiBaseUrl(rawBaseUrl);
|
|
25
|
+
return apiBaseUrl.replace(/\/api$/, "");
|
|
26
|
+
}
|
|
22
27
|
/**
|
|
23
28
|
* @param {{
|
|
24
29
|
* token?: string | null,
|
|
@@ -33,6 +38,9 @@ export function createLoadmillApiClient({ token = null, baseUrl = null, onUnauth
|
|
|
33
38
|
function getBaseUrl() {
|
|
34
39
|
return normalizeApiBaseUrl(baseUrl);
|
|
35
40
|
}
|
|
41
|
+
function getSiteBaseUrl() {
|
|
42
|
+
return normalizeSiteBaseUrl(baseUrl);
|
|
43
|
+
}
|
|
36
44
|
async function apiRequest(endpoint, options = {}) {
|
|
37
45
|
const resolvedToken = getApiToken();
|
|
38
46
|
if (!resolvedToken) {
|
|
@@ -124,9 +132,38 @@ export function createLoadmillApiClient({ token = null, baseUrl = null, onUnauth
|
|
|
124
132
|
}),
|
|
125
133
|
});
|
|
126
134
|
}
|
|
135
|
+
async function reportDroidRun(payload) {
|
|
136
|
+
return await apiRequest(`/droid-cua/runs`, {
|
|
137
|
+
method: "POST",
|
|
138
|
+
body: JSON.stringify(payload),
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
async function getCurrentUser() {
|
|
142
|
+
const resolvedToken = getApiToken();
|
|
143
|
+
if (!resolvedToken) {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
const response = await fetch(`${getSiteBaseUrl()}/auth/logged-in-user`, {
|
|
147
|
+
method: "GET",
|
|
148
|
+
headers: {
|
|
149
|
+
Accept: "application/json",
|
|
150
|
+
Authorization: `Bearer ${resolvedToken}`,
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
if (!response.ok) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
try {
|
|
157
|
+
return extractLoadmillSessionUser(await response.json());
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
127
163
|
return {
|
|
128
164
|
getApiToken,
|
|
129
165
|
getBaseUrl,
|
|
166
|
+
getSiteBaseUrl,
|
|
130
167
|
searchFlows,
|
|
131
168
|
runTestSuite,
|
|
132
169
|
runTestFlow,
|
|
@@ -134,6 +171,8 @@ export function createLoadmillApiClient({ token = null, baseUrl = null, onUnauth
|
|
|
134
171
|
getFlowRunStatus,
|
|
135
172
|
getTestSuiteFlowRun,
|
|
136
173
|
explainFailure,
|
|
174
|
+
reportDroidRun,
|
|
175
|
+
getCurrentUser,
|
|
137
176
|
};
|
|
138
177
|
}
|
|
139
178
|
export function getApiToken() {
|
|
@@ -185,6 +224,13 @@ export async function getFlowRunStatus(flowRunId) {
|
|
|
185
224
|
onUnauthorized: globalThis.__DROID_CUA_HANDLE_LOADMILL_UNAUTHORIZED__,
|
|
186
225
|
}).getFlowRunStatus(flowRunId);
|
|
187
226
|
}
|
|
227
|
+
export async function reportDroidRun(payload) {
|
|
228
|
+
return await createLoadmillApiClient({
|
|
229
|
+
token: process.env.LOADMILL_API_TOKEN,
|
|
230
|
+
baseUrl: process.env.LOADMILL_BASE_URL,
|
|
231
|
+
onUnauthorized: globalThis.__DROID_CUA_HANDLE_LOADMILL_UNAUTHORIZED__,
|
|
232
|
+
}).reportDroidRun(payload);
|
|
233
|
+
}
|
|
188
234
|
export async function getTestSuiteFlowRun(flowRunId) {
|
|
189
235
|
return await createLoadmillApiClient({
|
|
190
236
|
token: process.env.LOADMILL_API_TOKEN,
|
|
@@ -57,13 +57,13 @@ ${futureScript}`
|
|
|
57
57
|
}
|
|
58
58
|
];
|
|
59
59
|
}
|
|
60
|
-
async function selectRelevantParameterNames({ openAiClient, command, flowName, futureInstructions, parameters, }) {
|
|
60
|
+
async function selectRelevantParameterNames({ openAiClient, helperModel, command, flowName, futureInstructions, parameters, }) {
|
|
61
61
|
if (!openAiClient || !Array.isArray(parameters) || parameters.length === 0) {
|
|
62
62
|
return [];
|
|
63
63
|
}
|
|
64
64
|
try {
|
|
65
65
|
const response = await openAiClient.chat.completions.create({
|
|
66
|
-
model: LOADMILL_MODEL_ALIASES.DEFAULT_AGENT_MODEL,
|
|
66
|
+
model: helperModel || LOADMILL_MODEL_ALIASES.DEFAULT_AGENT_MODEL,
|
|
67
67
|
messages: buildParameterSelectionMessages({
|
|
68
68
|
command,
|
|
69
69
|
flowName,
|
|
@@ -98,7 +98,7 @@ function findSelectedSuiteFlowRun(finalResult, selectedFlow) {
|
|
|
98
98
|
}
|
|
99
99
|
return flowRuns.find((flowRun) => flowRun?.testSuiteFlowId === selectedFlow?.id) || null;
|
|
100
100
|
}
|
|
101
|
-
async function buildRuntimeParameterContext({ loadmillClient, finalResult, selectedFlow, command, flowName, futureInstructions, openAiClient, }) {
|
|
101
|
+
async function buildRuntimeParameterContext({ loadmillClient, finalResult, selectedFlow, command, flowName, futureInstructions, openAiClient, helperModel, }) {
|
|
102
102
|
const selectedSuiteFlowRun = findSelectedSuiteFlowRun(finalResult, selectedFlow);
|
|
103
103
|
if (!selectedSuiteFlowRun?.id || typeof loadmillClient.getTestSuiteFlowRun !== "function") {
|
|
104
104
|
return null;
|
|
@@ -112,6 +112,7 @@ async function buildRuntimeParameterContext({ loadmillClient, finalResult, selec
|
|
|
112
112
|
flowName,
|
|
113
113
|
futureInstructions,
|
|
114
114
|
parameters,
|
|
115
|
+
helperModel,
|
|
115
116
|
});
|
|
116
117
|
const candidates = selectLoadmillParameterCandidates(parameters, selectedParameterNames);
|
|
117
118
|
const note = renderLoadmillParameterContextNote(candidates);
|
|
@@ -198,7 +199,7 @@ async function pollForCompletion(runId, loadmillClient, options = {}) {
|
|
|
198
199
|
* @returns {Promise<{success: boolean, error?: string, result?: Object}>}
|
|
199
200
|
*/
|
|
200
201
|
export async function executeLoadmillCommand(userInput, options = {}) {
|
|
201
|
-
const { onProgress = () => { }, loadmillClient = createLoadmillApiClient(), openAiClient = null, futureInstructions = [], pollTimeoutMs, pollIntervalMs, now, sleepFn, } = options;
|
|
202
|
+
const { onProgress = () => { }, loadmillClient = createLoadmillApiClient(), openAiClient = null, helperModel = null, futureInstructions = [], pollTimeoutMs, pollIntervalMs, now, sleepFn, } = options;
|
|
202
203
|
// Check for API token
|
|
203
204
|
if (!loadmillClient.getApiToken()) {
|
|
204
205
|
return {
|
|
@@ -215,7 +216,10 @@ export async function executeLoadmillCommand(userInput, options = {}) {
|
|
|
215
216
|
try {
|
|
216
217
|
// Step 1: Interpret the command
|
|
217
218
|
onProgress({ step: "interpreting", message: "Interpreting command..." });
|
|
218
|
-
const interpreted = await interpretLoadmillCommand(userInput, {
|
|
219
|
+
const interpreted = await interpretLoadmillCommand(userInput, {
|
|
220
|
+
client: openAiClient,
|
|
221
|
+
...(helperModel ? { model: helperModel } : {}),
|
|
222
|
+
});
|
|
219
223
|
logger.debug("Loadmill command interpreted", interpreted);
|
|
220
224
|
// Step 2: Search for flows
|
|
221
225
|
onProgress({ step: "searching", message: `Searching for flows matching "${interpreted.searchQuery}"...` });
|
|
@@ -229,7 +233,10 @@ export async function executeLoadmillCommand(userInput, options = {}) {
|
|
|
229
233
|
}
|
|
230
234
|
// Step 3: Select best match
|
|
231
235
|
onProgress({ step: "selecting", message: `Found ${flows.length} flow(s). Selecting best match...` });
|
|
232
|
-
const { selectedFlow, confidence } = await selectBestFlow(flows, interpreted.searchQuery, {
|
|
236
|
+
const { selectedFlow, confidence } = await selectBestFlow(flows, interpreted.searchQuery, {
|
|
237
|
+
client: openAiClient,
|
|
238
|
+
...(helperModel ? { model: helperModel } : {}),
|
|
239
|
+
});
|
|
233
240
|
if (!selectedFlow) {
|
|
234
241
|
return {
|
|
235
242
|
success: false,
|
|
@@ -272,6 +279,7 @@ export async function executeLoadmillCommand(userInput, options = {}) {
|
|
|
272
279
|
flowName,
|
|
273
280
|
futureInstructions,
|
|
274
281
|
openAiClient,
|
|
282
|
+
helperModel,
|
|
275
283
|
})
|
|
276
284
|
: null;
|
|
277
285
|
return {
|
|
@@ -8,9 +8,9 @@ import { buildLoadmillCommandInterpretationMessages, buildLoadmillFlowSelectionM
|
|
|
8
8
|
* @param {string} userInput - Natural language command
|
|
9
9
|
* @returns {Promise<{searchQuery: string, parameters: Object}>}
|
|
10
10
|
*/
|
|
11
|
-
export async function interpretLoadmillCommand(userInput, { client }) {
|
|
11
|
+
export async function interpretLoadmillCommand(userInput, { client, model = LOADMILL_MODEL_ALIASES.FAST }) {
|
|
12
12
|
const response = await client.chat.completions.create({
|
|
13
|
-
model
|
|
13
|
+
model,
|
|
14
14
|
messages: buildLoadmillCommandInterpretationMessages(userInput),
|
|
15
15
|
response_format: { type: "json_object" }
|
|
16
16
|
});
|
|
@@ -27,7 +27,7 @@ export async function interpretLoadmillCommand(userInput, { client }) {
|
|
|
27
27
|
* @param {string} originalQuery - Original user query
|
|
28
28
|
* @returns {Promise<{selectedFlow: Object|null, confidence: number}>}
|
|
29
29
|
*/
|
|
30
|
-
export async function selectBestFlow(flows, originalQuery, { client }) {
|
|
30
|
+
export async function selectBestFlow(flows, originalQuery, { client, model = LOADMILL_MODEL_ALIASES.FAST }) {
|
|
31
31
|
// Ensure flows is an array
|
|
32
32
|
if (!flows || !Array.isArray(flows) || flows.length === 0) {
|
|
33
33
|
return { selectedFlow: null, confidence: 0 };
|
|
@@ -42,7 +42,7 @@ export async function selectBestFlow(flows, originalQuery, { client }) {
|
|
|
42
42
|
return `${i + 1}. ID: ${f.id}, Name: "${name}"${suite ? `, Suite: "${suite}"` : ""}`;
|
|
43
43
|
}).join("\n");
|
|
44
44
|
const response = await client.chat.completions.create({
|
|
45
|
-
model
|
|
45
|
+
model,
|
|
46
46
|
messages: buildLoadmillFlowSelectionMessages(originalQuery, flowList),
|
|
47
47
|
response_format: { type: "json_object" }
|
|
48
48
|
});
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
const VALID_STATUSES = new Set(["pass", "fail", "error", "skipped", "stopped"]);
|
|
2
|
+
function trimToNull(value) {
|
|
3
|
+
if (typeof value !== "string") {
|
|
4
|
+
return null;
|
|
5
|
+
}
|
|
6
|
+
const trimmed = value.trim();
|
|
7
|
+
return trimmed || null;
|
|
8
|
+
}
|
|
9
|
+
function nonNegativeNumberOrNull(value) {
|
|
10
|
+
return typeof value === "number" && Number.isFinite(value) ? Math.max(0, value) : null;
|
|
11
|
+
}
|
|
12
|
+
function nonNegativeIntegerOrNull(value) {
|
|
13
|
+
const normalized = nonNegativeNumberOrNull(value);
|
|
14
|
+
return normalized == null ? null : Math.floor(normalized);
|
|
15
|
+
}
|
|
16
|
+
function normalizeSource(value) {
|
|
17
|
+
return value === "ci" ? "ci" : "desktop";
|
|
18
|
+
}
|
|
19
|
+
function normalizePlatform(value) {
|
|
20
|
+
const platform = trimToNull(value);
|
|
21
|
+
return platform === "browser" ? "web" : platform;
|
|
22
|
+
}
|
|
23
|
+
function normalizeTestName(value) {
|
|
24
|
+
return value ? value.replace(/\.dcua$/i, "") : null;
|
|
25
|
+
}
|
|
26
|
+
function normalizeRunType(value, testCases) {
|
|
27
|
+
if (value === "project" || value === "test") {
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
return testCases > 1 ? "project" : "test";
|
|
31
|
+
}
|
|
32
|
+
export function normalizeDroidRunStatus({ status = null, outcome = null, success = null, error = null } = {}) {
|
|
33
|
+
if (VALID_STATUSES.has(status)) {
|
|
34
|
+
return status;
|
|
35
|
+
}
|
|
36
|
+
if (outcome === "pass" || success === true) {
|
|
37
|
+
return "pass";
|
|
38
|
+
}
|
|
39
|
+
if (error === "Stopped by user") {
|
|
40
|
+
return "stopped";
|
|
41
|
+
}
|
|
42
|
+
if (outcome === "skipped") {
|
|
43
|
+
return "skipped";
|
|
44
|
+
}
|
|
45
|
+
if (outcome === "failure" || (typeof error === "string" && error.startsWith("Assertion failed:"))) {
|
|
46
|
+
return "fail";
|
|
47
|
+
}
|
|
48
|
+
return "error";
|
|
49
|
+
}
|
|
50
|
+
export function buildDroidRunReportPayload(input = {}) {
|
|
51
|
+
const completedAt = trimToNull(input.completedAt) || new Date().toISOString();
|
|
52
|
+
const testCases = nonNegativeIntegerOrNull(input.testCases);
|
|
53
|
+
const payload = {
|
|
54
|
+
runType: normalizeRunType(input.runType, testCases),
|
|
55
|
+
testCases,
|
|
56
|
+
durationMs: nonNegativeIntegerOrNull(input.durationMs),
|
|
57
|
+
platform: normalizePlatform(input.platform),
|
|
58
|
+
deviceSource: trimToNull(input.deviceSource),
|
|
59
|
+
deviceName: trimToNull(input.deviceName),
|
|
60
|
+
osVersion: trimToNull(input.osVersion),
|
|
61
|
+
instructionsTotal: nonNegativeIntegerOrNull(input.instructionsTotal),
|
|
62
|
+
instructionsCompleted: nonNegativeIntegerOrNull(input.instructionsCompleted),
|
|
63
|
+
actionsTotal: nonNegativeIntegerOrNull(input.actionsTotal),
|
|
64
|
+
assertionsPassed: nonNegativeIntegerOrNull(input.assertionsPassed),
|
|
65
|
+
assertionsFailed: nonNegativeIntegerOrNull(input.assertionsFailed),
|
|
66
|
+
retries: nonNegativeIntegerOrNull(input.retries),
|
|
67
|
+
error: trimToNull(input.error),
|
|
68
|
+
};
|
|
69
|
+
for (const key of Object.keys(payload)) {
|
|
70
|
+
if (payload[key] == null) {
|
|
71
|
+
delete payload[key];
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
runId: trimToNull(input.runId),
|
|
76
|
+
source: normalizeSource(input.source),
|
|
77
|
+
projectName: trimToNull(input.projectName),
|
|
78
|
+
testName: normalizeTestName(trimToNull(input.testName)),
|
|
79
|
+
status: normalizeDroidRunStatus(input),
|
|
80
|
+
completedAt,
|
|
81
|
+
payload,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* @param {{
|
|
86
|
+
* loadmillClient?: { getApiToken?: () => string | null, reportDroidRun?: (payload: object) => Promise<unknown> } | null,
|
|
87
|
+
* onWarning?: ((message: string) => void) | null,
|
|
88
|
+
* [key: string]: unknown,
|
|
89
|
+
* }} input
|
|
90
|
+
*/
|
|
91
|
+
export async function reportDroidRunCompletion({ loadmillClient, onWarning = null, ...input } = {}) {
|
|
92
|
+
if (!loadmillClient?.getApiToken?.()) {
|
|
93
|
+
return { reported: false, reason: "missing_token" };
|
|
94
|
+
}
|
|
95
|
+
if (typeof loadmillClient.reportDroidRun !== "function") {
|
|
96
|
+
return { reported: false, reason: "unsupported_client" };
|
|
97
|
+
}
|
|
98
|
+
const payload = buildDroidRunReportPayload(input);
|
|
99
|
+
if (!payload.runId) {
|
|
100
|
+
return { reported: false, reason: "missing_run_id" };
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
await loadmillClient.reportDroidRun(payload);
|
|
104
|
+
return { reported: true };
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
108
|
+
if (typeof onWarning === "function") {
|
|
109
|
+
onWarning(`Failed to report Droid run to Loadmill: ${message}`);
|
|
110
|
+
}
|
|
111
|
+
return { reported: false, reason: "request_failed", error: message };
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
function trimToNull(value) {
|
|
2
|
+
if (typeof value !== "string") {
|
|
3
|
+
return null;
|
|
4
|
+
}
|
|
5
|
+
const trimmed = value.trim();
|
|
6
|
+
return trimmed || null;
|
|
7
|
+
}
|
|
8
|
+
function extractUserDisplayNameFromRecord(record) {
|
|
9
|
+
for (const key of ["displayName", "fullName", "name", "email"]) {
|
|
10
|
+
const value = trimToNull(record[key]);
|
|
11
|
+
if (value) {
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
const nested = record.user;
|
|
16
|
+
if (nested && typeof nested === "object") {
|
|
17
|
+
return extractUserDisplayNameFromRecord(nested);
|
|
18
|
+
}
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
export function extractLoadmillSessionUser(payload) {
|
|
22
|
+
if (!payload || typeof payload !== "object") {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
const record = payload;
|
|
26
|
+
const id = trimToNull(record.id) || trimToNull(record.userId);
|
|
27
|
+
const email = trimToNull(record.email);
|
|
28
|
+
const displayName = extractUserDisplayNameFromRecord(record);
|
|
29
|
+
const teamId = trimToNull(record.teamId);
|
|
30
|
+
if (!id) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
id,
|
|
35
|
+
email,
|
|
36
|
+
displayName,
|
|
37
|
+
teamId,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { DroidCuaSegmentEvent, buildDroidCuaTestRunCompletedProperties, } from "../../core/droid-cua-segment-events.js";
|
|
2
|
+
const DEFAULT_DROID_CUA_SEGMENT_WRITE_KEY = "ZuHGZkEXjhEkvzVPMnVDJfWiNsD1nGhG";
|
|
3
|
+
const SEGMENT_TRACK_URL = "https://api.segment.io/v1/track";
|
|
4
|
+
function trimToNull(value) {
|
|
5
|
+
if (typeof value !== "string") {
|
|
6
|
+
return null;
|
|
7
|
+
}
|
|
8
|
+
const trimmed = value.trim();
|
|
9
|
+
return trimmed || null;
|
|
10
|
+
}
|
|
11
|
+
function getSegmentWriteKey() {
|
|
12
|
+
if (process.env.DROID_CUA_SEGMENT_DISABLED === "1") {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
if (process.env.DROID_CUA_SEGMENT_DISABLED === "true") {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
return trimToNull(process.env.SEGMENT_WRITE_KEY) || DEFAULT_DROID_CUA_SEGMENT_WRITE_KEY;
|
|
19
|
+
}
|
|
20
|
+
function buildBasicAuthHeader(writeKey) {
|
|
21
|
+
return `Basic ${Buffer.from(`${writeKey}:`).toString("base64")}`;
|
|
22
|
+
}
|
|
23
|
+
function warnSegmentError(scope, error) {
|
|
24
|
+
console.warn(`[segment] ${scope}`, error);
|
|
25
|
+
}
|
|
26
|
+
export async function reportDroidCuaCiTestRunCompleted(input = {}) {
|
|
27
|
+
const writeKey = getSegmentWriteKey();
|
|
28
|
+
if (!writeKey) {
|
|
29
|
+
return { reported: false, reason: "disabled" };
|
|
30
|
+
}
|
|
31
|
+
const userId = trimToNull(input.userId);
|
|
32
|
+
const anonymousId = trimToNull(input.anonymousId) || (input.runId ? `droid-cua-ci:${input.runId}` : null);
|
|
33
|
+
if (!userId && !anonymousId) {
|
|
34
|
+
return { reported: false, reason: "missing_identity" };
|
|
35
|
+
}
|
|
36
|
+
const body = {
|
|
37
|
+
...(userId ? { userId } : { anonymousId }),
|
|
38
|
+
event: DroidCuaSegmentEvent.TEST_RUN_COMPLETED,
|
|
39
|
+
properties: buildDroidCuaTestRunCompletedProperties(input),
|
|
40
|
+
};
|
|
41
|
+
try {
|
|
42
|
+
const response = await fetch(SEGMENT_TRACK_URL, {
|
|
43
|
+
method: "POST",
|
|
44
|
+
headers: {
|
|
45
|
+
Authorization: buildBasicAuthHeader(writeKey),
|
|
46
|
+
"Content-Type": "application/json",
|
|
47
|
+
},
|
|
48
|
+
body: JSON.stringify(body),
|
|
49
|
+
});
|
|
50
|
+
if (!response.ok) {
|
|
51
|
+
const detail = await response.text();
|
|
52
|
+
return { reported: false, reason: "request_failed", error: `Segment API error (${response.status}): ${detail}` };
|
|
53
|
+
}
|
|
54
|
+
return { reported: true };
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
warnSegmentError("CI test run completed reporting failed", error);
|
|
58
|
+
return {
|
|
59
|
+
reported: false,
|
|
60
|
+
reason: "request_failed",
|
|
61
|
+
error: error instanceof Error ? error.message : String(error),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@loadmill/droid-cua",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.22.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",
|
|
@@ -20,8 +20,11 @@
|
|
|
20
20
|
"bench:run": "tsx index.js --platform android --instructions benchmarks/benchmark.dcua --context benchmarks/context.md --debug",
|
|
21
21
|
"bench:report": "node scripts/benchmark-report.js",
|
|
22
22
|
"bench:report:web": "node scripts/benchmark-report-web.js",
|
|
23
|
+
"obs:droid": "node scripts/obs-droid-demo.mjs",
|
|
23
24
|
"execution:profile": "node scripts/execution-profile.js",
|
|
24
25
|
"test": "node --test unit-tests/*.test.js",
|
|
26
|
+
"test:aws-device-farm:android": "node integration-tests/aws-device-farm-android.test.js",
|
|
27
|
+
"test:aws-device-farm:ios": "node integration-tests/aws-device-farm-ios.test.js",
|
|
25
28
|
"build": "tsc",
|
|
26
29
|
"clean": "rm -rf build",
|
|
27
30
|
"typecheck": "tsc --noEmit",
|
|
@@ -58,6 +61,7 @@
|
|
|
58
61
|
"author": "Loadmill",
|
|
59
62
|
"license": "UNLICENSED",
|
|
60
63
|
"dependencies": {
|
|
64
|
+
"@aws-sdk/client-device-farm": "^3.1065.0",
|
|
61
65
|
"dotenv": "^16.4.7",
|
|
62
66
|
"ink": "^4.4.1",
|
|
63
67
|
"ink-spinner": "^5.0.0",
|