@loadmill/droid-cua 2.8.0 → 2.9.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.
@@ -20,153 +20,171 @@ function normalizeApiBaseUrl(rawBaseUrl) {
20
20
  }
21
21
  }
22
22
  /**
23
- * Get Loadmill API token from environment
24
- * @returns {string|null}
23
+ * @param {{
24
+ * token?: string | null,
25
+ * baseUrl?: string | null,
26
+ * onUnauthorized?: (() => Promise<unknown> | unknown) | null,
27
+ * }} [options]
25
28
  */
26
- export function getApiToken() {
27
- return process.env.LOADMILL_API_TOKEN || null;
28
- }
29
- /**
30
- * Get Loadmill base URL from environment
31
- * @returns {string}
32
- */
33
- export function getBaseUrl() {
34
- return normalizeApiBaseUrl(process.env.LOADMILL_BASE_URL);
35
- }
36
- /**
37
- * Make an authenticated request to Loadmill API
38
- * @param {string} endpoint - API endpoint (without base URL)
39
- * @param {Object} options - Fetch options
40
- * @returns {Promise<Object>}
41
- */
42
- async function apiRequest(endpoint, options = {}) {
43
- const token = getApiToken();
44
- if (!token) {
45
- throw new Error("LOADMILL_API_TOKEN environment variable is not set");
29
+ export function createLoadmillApiClient({ token = null, baseUrl = null, onUnauthorized = null, } = {}) {
30
+ function getApiToken() {
31
+ return typeof token === "string" && token.trim() ? token.trim() : null;
32
+ }
33
+ function getBaseUrl() {
34
+ return normalizeApiBaseUrl(baseUrl);
46
35
  }
47
- const baseUrl = getBaseUrl();
48
- const url = `${baseUrl}${endpoint}`;
49
- const response = await fetch(url, {
50
- ...options,
51
- headers: {
52
- "Authorization": `Bearer ${token}`,
53
- "Content-Type": "application/json",
54
- ...options.headers,
55
- },
56
- });
57
- if (response.status === 401 || response.status === 403) {
58
- const unauthorizedHandler = globalThis.__DROID_CUA_HANDLE_LOADMILL_UNAUTHORIZED__;
59
- if (typeof unauthorizedHandler === "function") {
60
- await unauthorizedHandler();
36
+ async function apiRequest(endpoint, options = {}) {
37
+ const resolvedToken = getApiToken();
38
+ if (!resolvedToken) {
39
+ throw new Error("LOADMILL_API_TOKEN environment variable is not set");
40
+ }
41
+ const resolvedBaseUrl = getBaseUrl();
42
+ const url = `${resolvedBaseUrl}${endpoint}`;
43
+ const response = await fetch(url, {
44
+ ...options,
45
+ headers: {
46
+ "Authorization": `Bearer ${resolvedToken}`,
47
+ "Content-Type": "application/json",
48
+ ...options.headers,
49
+ },
50
+ });
51
+ if ((response.status === 401 || response.status === 403) && typeof onUnauthorized === "function") {
52
+ await onUnauthorized();
53
+ }
54
+ if (!response.ok) {
55
+ const errorText = await response.text();
56
+ throw new Error(`Loadmill API error (${response.status}): ${errorText}`);
61
57
  }
58
+ return response.json();
62
59
  }
63
- if (!response.ok) {
64
- const errorText = await response.text();
65
- throw new Error(`Loadmill API error (${response.status}): ${errorText}`);
60
+ async function searchFlows(searchQuery, options = {}) {
61
+ const { limit = 10 } = options;
62
+ const encodedQuery = encodeURIComponent(searchQuery);
63
+ const result = await apiRequest(`/test-flows?search=${encodedQuery}&limit=${limit}`);
64
+ if (Array.isArray(result)) {
65
+ return result;
66
+ }
67
+ if (result && Array.isArray(result.flows)) {
68
+ return result.flows;
69
+ }
70
+ if (result && Array.isArray(result.data)) {
71
+ return result.data;
72
+ }
73
+ if (result && Array.isArray(result.items)) {
74
+ return result.items;
75
+ }
76
+ if (result && Array.isArray(result.testFlows)) {
77
+ return result.testFlows;
78
+ }
79
+ console.error("[Loadmill] Unexpected API response format:", JSON.stringify(result, null, 2));
80
+ return [];
66
81
  }
67
- return response.json();
68
- }
69
- /**
70
- * Search for test flows by query
71
- * @param {string} searchQuery - Search query string
72
- * @param {Object} options - Additional options
73
- * @param {number} options.limit - Maximum number of results (default: 10)
74
- * @returns {Promise<Array>} - Array of matching flows
75
- */
76
- export async function searchFlows(searchQuery, options = {}) {
77
- const { limit = 10 } = options;
78
- const encodedQuery = encodeURIComponent(searchQuery);
79
- const result = await apiRequest(`/test-flows?search=${encodedQuery}&limit=${limit}`);
80
- // Handle different response formats
81
- if (Array.isArray(result)) {
82
- return result;
82
+ async function runTestSuite(suiteId, { flowIds = [], parameters = {} } = {}) {
83
+ const body = {};
84
+ if (flowIds.length > 0) {
85
+ body.flowIds = flowIds;
86
+ }
87
+ if (Object.keys(parameters).length > 0) {
88
+ body.parameters = parameters;
89
+ }
90
+ return await apiRequest(`/test-suites/${suiteId}/run`, {
91
+ method: "POST",
92
+ body: JSON.stringify(body),
93
+ });
83
94
  }
84
- if (result && Array.isArray(result.flows)) {
85
- return result.flows;
95
+ async function runTestFlow(flowId, suiteId, { parameters = {} } = {}) {
96
+ const body = {
97
+ flows: [flowId],
98
+ inlineParameterOverride: true,
99
+ sharedFlowVersionOverrides: [],
100
+ };
101
+ if (Object.keys(parameters).length > 0) {
102
+ body.overrideParameters = parameters;
103
+ }
104
+ return await apiRequest(`/test-suites/${suiteId}/run?ui=true`, {
105
+ method: "POST",
106
+ body: JSON.stringify(body),
107
+ });
86
108
  }
87
- if (result && Array.isArray(result.data)) {
88
- return result.data;
109
+ async function getTestRunStatus(testRunId) {
110
+ return await apiRequest(`/test-suites-runs/${testRunId}`);
89
111
  }
90
- if (result && Array.isArray(result.items)) {
91
- return result.items;
112
+ async function getFlowRunStatus(flowRunId) {
113
+ return await apiRequest(`/test-flows-runs/${flowRunId}`);
92
114
  }
93
- if (result && Array.isArray(result.testFlows)) {
94
- return result.testFlows;
115
+ async function explainFailure(testRunId) {
116
+ return await apiRequest(`/explain-failures`, {
117
+ method: "POST",
118
+ body: JSON.stringify({
119
+ testRunId,
120
+ testRunType: "flowRun"
121
+ }),
122
+ });
95
123
  }
96
- // Log unexpected response format for debugging
97
- console.error("[Loadmill] Unexpected API response format:", JSON.stringify(result, null, 2));
98
- return [];
124
+ return {
125
+ getApiToken,
126
+ getBaseUrl,
127
+ searchFlows,
128
+ runTestSuite,
129
+ runTestFlow,
130
+ getTestRunStatus,
131
+ getFlowRunStatus,
132
+ explainFailure,
133
+ };
99
134
  }
100
- /**
101
- * Run a test suite with specified flows
102
- * @param {string} suiteId - Test suite ID
103
- * @param {Object} options - Run options
104
- * @param {string[]} options.flowIds - Array of flow IDs to run
105
- * @param {Object} options.parameters - Parameters to pass to the flows
106
- * @returns {Promise<Object>} - Run result with runId
107
- */
108
- export async function runTestSuite(suiteId, { flowIds = [], parameters = {} } = {}) {
109
- const body = {};
110
- if (flowIds.length > 0) {
111
- body.flowIds = flowIds;
112
- }
113
- if (Object.keys(parameters).length > 0) {
114
- body.parameters = parameters;
115
- }
116
- return apiRequest(`/test-suites/${suiteId}/run`, {
117
- method: "POST",
118
- body: JSON.stringify(body),
119
- });
135
+ export function getApiToken() {
136
+ return createLoadmillApiClient({
137
+ token: process.env.LOADMILL_API_TOKEN,
138
+ baseUrl: process.env.LOADMILL_BASE_URL,
139
+ }).getApiToken();
120
140
  }
121
- /**
122
- * Run a single test flow
123
- * @param {string} flowId - Flow ID to run
124
- * @param {string} suiteId - Test suite ID containing the flow
125
- * @param {Object} options - Run options
126
- * @param {Object} options.parameters - Parameters to pass to the flow
127
- * @returns {Promise<Object>} - Run result with testSuiteRunId
128
- */
129
- export async function runTestFlow(flowId, suiteId, { parameters = {} } = {}) {
130
- const body = {
131
- flows: [flowId],
132
- inlineParameterOverride: true,
133
- sharedFlowVersionOverrides: [],
134
- };
135
- if (Object.keys(parameters).length > 0) {
136
- body.overrideParameters = parameters;
137
- }
138
- return apiRequest(`/test-suites/${suiteId}/run?ui=true`, {
139
- method: "POST",
140
- body: JSON.stringify(body),
141
- });
141
+ export function getBaseUrl() {
142
+ return createLoadmillApiClient({
143
+ token: process.env.LOADMILL_API_TOKEN,
144
+ baseUrl: process.env.LOADMILL_BASE_URL,
145
+ }).getBaseUrl();
146
+ }
147
+ export async function searchFlows(searchQuery, options = {}) {
148
+ // Legacy env-based wrapper kept for older call sites. New execution flows
149
+ // should pass an explicit client created from resolved run credentials.
150
+ return await createLoadmillApiClient({
151
+ token: process.env.LOADMILL_API_TOKEN,
152
+ baseUrl: process.env.LOADMILL_BASE_URL,
153
+ onUnauthorized: globalThis.__DROID_CUA_HANDLE_LOADMILL_UNAUTHORIZED__,
154
+ }).searchFlows(searchQuery, options);
155
+ }
156
+ export async function runTestSuite(suiteId, options = {}) {
157
+ return await createLoadmillApiClient({
158
+ token: process.env.LOADMILL_API_TOKEN,
159
+ baseUrl: process.env.LOADMILL_BASE_URL,
160
+ onUnauthorized: globalThis.__DROID_CUA_HANDLE_LOADMILL_UNAUTHORIZED__,
161
+ }).runTestSuite(suiteId, options);
162
+ }
163
+ export async function runTestFlow(flowId, suiteId, options = {}) {
164
+ return await createLoadmillApiClient({
165
+ token: process.env.LOADMILL_API_TOKEN,
166
+ baseUrl: process.env.LOADMILL_BASE_URL,
167
+ onUnauthorized: globalThis.__DROID_CUA_HANDLE_LOADMILL_UNAUTHORIZED__,
168
+ }).runTestFlow(flowId, suiteId, options);
142
169
  }
143
- /**
144
- * Get the status of a test run
145
- * @param {string} testRunId - Test run ID
146
- * @returns {Promise<Object>} - Test run status
147
- */
148
170
  export async function getTestRunStatus(testRunId) {
149
- return apiRequest(`/test-suites-runs/${testRunId}`);
171
+ return await createLoadmillApiClient({
172
+ token: process.env.LOADMILL_API_TOKEN,
173
+ baseUrl: process.env.LOADMILL_BASE_URL,
174
+ onUnauthorized: globalThis.__DROID_CUA_HANDLE_LOADMILL_UNAUTHORIZED__,
175
+ }).getTestRunStatus(testRunId);
150
176
  }
151
- /**
152
- * Get the status of a flow run
153
- * @param {string} flowRunId - Flow run ID
154
- * @returns {Promise<Object>} - Flow run status
155
- */
156
177
  export async function getFlowRunStatus(flowRunId) {
157
- return apiRequest(`/test-flows-runs/${flowRunId}`);
178
+ return await createLoadmillApiClient({
179
+ token: process.env.LOADMILL_API_TOKEN,
180
+ baseUrl: process.env.LOADMILL_BASE_URL,
181
+ onUnauthorized: globalThis.__DROID_CUA_HANDLE_LOADMILL_UNAUTHORIZED__,
182
+ }).getFlowRunStatus(flowRunId);
158
183
  }
159
- /**
160
- * Get AI-generated explanation for a failed test run
161
- * @param {string} testRunId - The flow run ID (from testSuiteFlowRuns[].id)
162
- * @returns {Promise<Object>} - Explanation with reason, suggestion, etc.
163
- */
164
184
  export async function explainFailure(testRunId) {
165
- return apiRequest(`/explain-failures`, {
166
- method: "POST",
167
- body: JSON.stringify({
168
- testRunId,
169
- testRunType: "flowRun"
170
- }),
171
- });
185
+ return await createLoadmillApiClient({
186
+ token: process.env.LOADMILL_API_TOKEN,
187
+ baseUrl: process.env.LOADMILL_BASE_URL,
188
+ onUnauthorized: globalThis.__DROID_CUA_HANDLE_LOADMILL_UNAUTHORIZED__,
189
+ }).explainFailure(testRunId);
172
190
  }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Orchestrates Loadmill flow execution
3
3
  */
4
- import { searchFlows, runTestFlow, getTestRunStatus, getApiToken, explainFailure } from "./client.js";
4
+ import { createLoadmillApiClient } from "./client.js";
5
5
  import { interpretLoadmillCommand, selectBestFlow } from "./interpreter.js";
6
6
  import { logger } from "../../utils/logger.js";
7
7
  const POLL_INTERVAL_MS = 5000; // 5 seconds
@@ -20,11 +20,11 @@ function sleep(ms) {
20
20
  * @param {Function} onStatusUpdate - Callback for status updates
21
21
  * @returns {Promise<Object>} - Final run status
22
22
  */
23
- async function pollForCompletion(runId, onStatusUpdate = () => { }) {
23
+ async function pollForCompletion(runId, loadmillClient, onStatusUpdate = () => { }) {
24
24
  const startTime = Date.now();
25
25
  while (Date.now() - startTime < TIMEOUT_MS) {
26
26
  try {
27
- const status = await getTestRunStatus(runId);
27
+ const status = await loadmillClient.getTestRunStatus(runId);
28
28
  onStatusUpdate(status);
29
29
  // Check if run is complete
30
30
  if (status.status === "PASSED" || status.status === "passed") {
@@ -36,7 +36,7 @@ async function pollForCompletion(runId, onStatusUpdate = () => { }) {
36
36
  try {
37
37
  const failedFlowRun = status.testSuiteFlowRuns?.find(f => f.status === "FAILED");
38
38
  if (failedFlowRun) {
39
- const explanation = await explainFailure(failedFlowRun.id);
39
+ const explanation = await loadmillClient.explainFailure(failedFlowRun.id);
40
40
  if (explanation?.reason) {
41
41
  failureReason = explanation.reason;
42
42
  }
@@ -69,22 +69,28 @@ async function pollForCompletion(runId, onStatusUpdate = () => { }) {
69
69
  * @returns {Promise<{success: boolean, error?: string, result?: Object}>}
70
70
  */
71
71
  export async function executeLoadmillCommand(userInput, options = {}) {
72
- const { onProgress = () => { } } = options;
72
+ const { onProgress = () => { }, loadmillClient = createLoadmillApiClient(), openAiClient = null, } = options;
73
73
  // Check for API token
74
- if (!getApiToken()) {
74
+ if (!loadmillClient.getApiToken()) {
75
75
  return {
76
76
  success: false,
77
77
  error: "LOADMILL_API_TOKEN environment variable is not set. Please set it in your .env file."
78
78
  };
79
79
  }
80
+ if (!openAiClient) {
81
+ return {
82
+ success: false,
83
+ error: "OpenAI API key is not configured."
84
+ };
85
+ }
80
86
  try {
81
87
  // Step 1: Interpret the command
82
88
  onProgress({ step: "interpreting", message: "Interpreting command..." });
83
- const interpreted = await interpretLoadmillCommand(userInput);
89
+ const interpreted = await interpretLoadmillCommand(userInput, { client: openAiClient });
84
90
  logger.debug("Loadmill command interpreted", interpreted);
85
91
  // Step 2: Search for flows
86
92
  onProgress({ step: "searching", message: `Searching for flows matching "${interpreted.searchQuery}"...` });
87
- const flows = await searchFlows(interpreted.searchQuery);
93
+ const flows = await loadmillClient.searchFlows(interpreted.searchQuery);
88
94
  // Ensure flows is an array
89
95
  if (!Array.isArray(flows) || flows.length === 0) {
90
96
  return {
@@ -94,7 +100,7 @@ export async function executeLoadmillCommand(userInput, options = {}) {
94
100
  }
95
101
  // Step 3: Select best match
96
102
  onProgress({ step: "selecting", message: `Found ${flows.length} flow(s). Selecting best match...` });
97
- const { selectedFlow, confidence } = await selectBestFlow(flows, interpreted.searchQuery);
103
+ const { selectedFlow, confidence } = await selectBestFlow(flows, interpreted.searchQuery, { client: openAiClient });
98
104
  if (!selectedFlow) {
99
105
  return {
100
106
  success: false,
@@ -119,7 +125,7 @@ export async function executeLoadmillCommand(userInput, options = {}) {
119
125
  step: "running",
120
126
  message: `Running flow "${flowName}" (confidence: ${(confidence * 100).toFixed(0)}%)...`
121
127
  });
122
- const runResult = await runTestFlow(selectedFlow.id, selectedFlow.testSuiteId, {
128
+ const runResult = await loadmillClient.runTestFlow(selectedFlow.id, selectedFlow.testSuiteId, {
123
129
  parameters: interpreted.parameters
124
130
  });
125
131
  const runId = runResult.testSuiteRunId || runResult.id || runResult.runId;
@@ -131,7 +137,7 @@ export async function executeLoadmillCommand(userInput, options = {}) {
131
137
  }
132
138
  // Step 5: Poll for completion
133
139
  onProgress({ step: "polling", message: `Test started (ID: ${runId}). Waiting for completion...`, runId });
134
- const finalResult = await pollForCompletion(runId, (status) => {
140
+ const finalResult = await pollForCompletion(runId, loadmillClient, (status) => {
135
141
  onProgress({ step: "polling", message: `Status: ${status.status}...`, runId });
136
142
  });
137
143
  return {
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Loadmill integration public exports
3
3
  */
4
- export { getApiToken, searchFlows, runTestFlow, getFlowRunStatus } from "./client.js";
4
+ export { createLoadmillApiClient, getApiToken, searchFlows, runTestFlow, getFlowRunStatus } from "./client.js";
5
5
  export { interpretLoadmillCommand, selectBestFlow } from "./interpreter.js";
6
6
  export { executeLoadmillCommand } from "./executor.js";
@@ -1,24 +1,14 @@
1
1
  /**
2
2
  * AI-powered text interpretation for Loadmill commands
3
3
  */
4
- import OpenAI from "openai";
5
4
  import { buildLoadmillCommandInterpretationMessages, buildLoadmillFlowSelectionMessages, } from "../../prompts/loadmill.js";
6
- let openai = null;
7
- function getOpenAI() {
8
- if (!openai) {
9
- openai = new OpenAI({
10
- apiKey: process.env.OPENAI_API_KEY,
11
- });
12
- }
13
- return openai;
14
- }
15
5
  /**
16
6
  * Interpret a natural language Loadmill command into structured data
17
7
  * @param {string} userInput - Natural language command
18
8
  * @returns {Promise<{searchQuery: string, parameters: Object, action: 'run'|'search'}>}
19
9
  */
20
- export async function interpretLoadmillCommand(userInput) {
21
- const response = await getOpenAI().chat.completions.create({
10
+ export async function interpretLoadmillCommand(userInput, { client }) {
11
+ const response = await client.chat.completions.create({
22
12
  model: "gpt-4o-mini",
23
13
  messages: buildLoadmillCommandInterpretationMessages(userInput),
24
14
  response_format: { type: "json_object" }
@@ -37,7 +27,7 @@ export async function interpretLoadmillCommand(userInput) {
37
27
  * @param {string} originalQuery - Original user query
38
28
  * @returns {Promise<{selectedFlow: Object|null, confidence: number}>}
39
29
  */
40
- export async function selectBestFlow(flows, originalQuery) {
30
+ export async function selectBestFlow(flows, originalQuery, { client }) {
41
31
  // Ensure flows is an array
42
32
  if (!flows || !Array.isArray(flows) || flows.length === 0) {
43
33
  return { selectedFlow: null, confidence: 0 };
@@ -51,7 +41,7 @@ export async function selectBestFlow(flows, originalQuery) {
51
41
  const suite = f.testSuiteDescription || "";
52
42
  return `${i + 1}. ID: ${f.id}, Name: "${name}"${suite ? `, Suite: "${suite}"` : ""}`;
53
43
  }).join("\n");
54
- const response = await getOpenAI().chat.completions.create({
44
+ const response = await client.chat.completions.create({
55
45
  model: "gpt-4o-mini",
56
46
  messages: buildLoadmillFlowSelectionMessages(originalQuery, flowList),
57
47
  response_format: { type: "json_object" }
@@ -1,5 +1,4 @@
1
1
  import { getScreenshotAsBase64 } from "../device/connection.js";
2
- import { sendCUARequest, reviseTestScript } from "../device/openai.js";
3
2
  import { buildDesignModePrompt, buildDesignRecoveryPrompt } from "../core/prompts.js";
4
3
  import { saveTest } from "../test-store/test-manager.js";
5
4
  import { logger } from "../utils/logger.js";
@@ -23,6 +22,8 @@ export class DesignModeInk {
23
22
  this.baseDesignPrompt = null;
24
23
  this.consecutiveErrorCount = 0;
25
24
  this.maxConsecutiveErrors = 3;
25
+ this.sendCUARequest = executionEngine.sendCUARequest;
26
+ this.reviseTestScript = executionEngine.reviseTestScript;
26
27
  }
27
28
  /**
28
29
  * Start design mode conversation
@@ -183,7 +184,7 @@ export class DesignModeInk {
183
184
  }
184
185
  // Get screenshot and send to model
185
186
  const screenshotBase64 = await getScreenshotAsBase64(this.session.deviceId, this.session.deviceInfo);
186
- const response = await sendCUARequest({
187
+ const response = await this.sendCUARequest({
187
188
  messages: this.session.messages,
188
189
  screenshotBase64,
189
190
  previousResponseId: this.session.previousResponseId,
@@ -271,7 +272,7 @@ export class DesignModeInk {
271
272
  const revision = confirm;
272
273
  // Use simple chat completion for revision instead of CUA
273
274
  addOutput({ type: 'info', text: 'Revising test script...' });
274
- currentScript = await reviseTestScript(currentScript, revision);
275
+ currentScript = await this.reviseTestScript(currentScript, revision);
275
276
  // Display the revised script
276
277
  addOutput({ type: 'info', text: '' });
277
278
  addOutput({ type: 'system', text: '=== Generated Test Script ===' });
@@ -1,6 +1,5 @@
1
1
  import readline from "readline";
2
2
  import { getScreenshotAsBase64 } from "../device/connection.js";
3
- import { sendCUARequest, reviseTestScript } from "../device/openai.js";
4
3
  import { buildDesignModePrompt, buildDesignRecoveryPrompt } from "../core/prompts.js";
5
4
  import { saveTest } from "../test-store/test-manager.js";
6
5
  import { logger } from "../utils/logger.js";
@@ -20,6 +19,8 @@ export class DesignMode {
20
19
  this.baseDesignPrompt = null;
21
20
  this.consecutiveErrorCount = 0;
22
21
  this.maxConsecutiveErrors = 3;
22
+ this.sendCUARequest = executionEngine.sendCUARequest;
23
+ this.reviseTestScript = executionEngine.reviseTestScript;
23
24
  }
24
25
  /**
25
26
  * Start design mode conversation
@@ -147,7 +148,7 @@ export class DesignMode {
147
148
  const keypressHandler = this.setupEscDetection();
148
149
  // Get screenshot and send to model
149
150
  const screenshotBase64 = await getScreenshotAsBase64(this.session.deviceId, this.session.deviceInfo);
150
- const response = await sendCUARequest({
151
+ const response = await this.sendCUARequest({
151
152
  messages: this.session.messages,
152
153
  screenshotBase64,
153
154
  previousResponseId: this.session.previousResponseId,
@@ -262,7 +263,7 @@ export class DesignMode {
262
263
  const revision = confirm;
263
264
  // Use simple chat completion for revision instead of CUA
264
265
  console.log("Revising test script...");
265
- currentScript = await reviseTestScript(currentScript, revision);
266
+ currentScript = await this.reviseTestScript(currentScript, revision);
266
267
  // Display the revised script
267
268
  console.log("\n=== Generated Test Script ===");
268
269
  console.log(currentScript);
@@ -228,6 +228,14 @@ export class ExecutionMode {
228
228
  }
229
229
  const navigationTarget = extractNavigateTarget(instruction);
230
230
  this.session.addToTranscript(`[Navigate] ${navigationTarget}`);
231
+ emitDesktopDebug("runner.navigate.started", "execution", {
232
+ runId: context?.runId,
233
+ stepId: stepContext?.stepId,
234
+ instructionIndex: stepContext?.instructionIndex
235
+ }, {
236
+ instruction,
237
+ target: navigationTarget
238
+ });
231
239
  this.emit(addOutput, "system", `Navigating to ${navigationTarget}...`, context, stepContext, {
232
240
  eventType: "system_message",
233
241
  payload: {
@@ -250,6 +258,15 @@ export class ExecutionMode {
250
258
  navigatedUrl: this.session.currentUrl,
251
259
  });
252
260
  this.session.installRecoverySystemPrompt(navigationRecoveryPrompt);
261
+ emitDesktopDebug("runner.navigate.completed", "execution", {
262
+ runId: context?.runId,
263
+ stepId: stepContext?.stepId,
264
+ instructionIndex: stepContext?.instructionIndex
265
+ }, {
266
+ instruction,
267
+ target: navigationTarget,
268
+ url: normalizedUrl
269
+ });
253
270
  this.emit(addOutput, "success", `Navigation complete: ${normalizedUrl}`, context, stepContext, {
254
271
  eventType: "system_message",
255
272
  payload: {
@@ -266,6 +283,15 @@ export class ExecutionMode {
266
283
  retryCount,
267
284
  message,
268
285
  });
286
+ emitDesktopDebug("runner.navigate.failed", "execution", {
287
+ runId: context?.runId,
288
+ stepId: stepContext?.stepId,
289
+ instructionIndex: stepContext?.instructionIndex
290
+ }, {
291
+ instruction,
292
+ target: navigationTarget,
293
+ message
294
+ });
269
295
  this.emit(addOutput, "error", `Navigation failed: ${message}`, context, stepContext, {
270
296
  eventType: "error",
271
297
  payload: {