testchimp-runner-core 0.0.35 → 0.0.37
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/dist/orchestrator/orchestrator-agent.d.ts.map +1 -1
- package/dist/orchestrator/orchestrator-agent.js +7 -4
- package/dist/orchestrator/orchestrator-agent.js.map +1 -1
- package/dist/orchestrator/orchestrator-prompts.d.ts.map +1 -1
- package/dist/orchestrator/orchestrator-prompts.js +73 -15
- package/dist/orchestrator/orchestrator-prompts.js.map +1 -1
- package/dist/orchestrator/page-som-handler.d.ts +1 -2
- package/dist/orchestrator/page-som-handler.d.ts.map +1 -1
- package/dist/orchestrator/page-som-handler.js +51 -25
- package/dist/orchestrator/page-som-handler.js.map +1 -1
- package/package.json +6 -1
- package/plandocs/BEFORE_AFTER_VERIFICATION.md +0 -148
- package/plandocs/COORDINATE_MODE_DIAGNOSIS.md +0 -144
- package/plandocs/CREDIT_CALLBACK_ARCHITECTURE.md +0 -253
- package/plandocs/HUMAN_LIKE_IMPROVEMENTS.md +0 -642
- package/plandocs/IMPLEMENTATION_STATUS.md +0 -108
- package/plandocs/INTEGRATION_COMPLETE.md +0 -322
- package/plandocs/MULTI_AGENT_ARCHITECTURE_REVIEW.md +0 -844
- package/plandocs/ORCHESTRATOR_MVP_SUMMARY.md +0 -539
- package/plandocs/PHASE1_ABSTRACTION_COMPLETE.md +0 -241
- package/plandocs/PHASE1_FINAL_STATUS.md +0 -210
- package/plandocs/PHASE_1_COMPLETE.md +0 -165
- package/plandocs/PHASE_1_SUMMARY.md +0 -184
- package/plandocs/PLANNING_SESSION_SUMMARY.md +0 -372
- package/plandocs/PROMPT_OPTIMIZATION_ANALYSIS.md +0 -120
- package/plandocs/PROMPT_SANITY_CHECK.md +0 -120
- package/plandocs/SCRIPT_CLEANUP_FEATURE.md +0 -201
- package/plandocs/SCRIPT_GENERATION_ARCHITECTURE.md +0 -364
- package/plandocs/SELECTOR_IMPROVEMENTS.md +0 -139
- package/plandocs/SESSION_SUMMARY_v0.0.33.md +0 -151
- package/plandocs/TROUBLESHOOTING_SESSION.md +0 -72
- package/plandocs/VISION_DIAGNOSTICS_IMPROVEMENTS.md +0 -336
- package/plandocs/VISUAL_AGENT_EVOLUTION_PLAN.md +0 -396
- package/plandocs/WHATS_NEW_v0.0.33.md +0 -183
- package/plandocs/exploratory-mode-support-v2.plan.md +0 -953
- package/plandocs/exploratory-mode-support.plan.md +0 -928
- package/plandocs/journey-id-tracking-addendum.md +0 -227
- package/releasenotes/RELEASE_0.0.26.md +0 -165
- package/releasenotes/RELEASE_0.0.27.md +0 -236
- package/releasenotes/RELEASE_0.0.28.md +0 -286
- package/src/auth-config.ts +0 -84
- package/src/credit-usage-service.ts +0 -188
- package/src/env-loader.ts +0 -103
- package/src/execution-service.ts +0 -996
- package/src/file-handler.ts +0 -104
- package/src/index.ts +0 -432
- package/src/llm-facade.ts +0 -821
- package/src/llm-provider.ts +0 -53
- package/src/model-constants.ts +0 -35
- package/src/orchestrator/decision-parser.ts +0 -139
- package/src/orchestrator/index.ts +0 -58
- package/src/orchestrator/orchestrator-agent.ts +0 -1282
- package/src/orchestrator/orchestrator-prompts.ts +0 -786
- package/src/orchestrator/page-som-handler.ts +0 -1565
- package/src/orchestrator/som-types.ts +0 -188
- package/src/orchestrator/tool-registry.ts +0 -184
- package/src/orchestrator/tools/check-page-ready.ts +0 -75
- package/src/orchestrator/tools/extract-data.ts +0 -92
- package/src/orchestrator/tools/index.ts +0 -15
- package/src/orchestrator/tools/inspect-page.ts +0 -42
- package/src/orchestrator/tools/recall-history.ts +0 -72
- package/src/orchestrator/tools/refresh-som-markers.ts +0 -69
- package/src/orchestrator/tools/take-screenshot.ts +0 -128
- package/src/orchestrator/tools/verify-action-result.ts +0 -159
- package/src/orchestrator/tools/view-previous-screenshot.ts +0 -103
- package/src/orchestrator/types.ts +0 -291
- package/src/playwright-mcp-service.ts +0 -224
- package/src/progress-reporter.ts +0 -144
- package/src/prompts.ts +0 -842
- package/src/providers/backend-proxy-llm-provider.ts +0 -91
- package/src/providers/local-llm-provider.ts +0 -38
- package/src/scenario-service.ts +0 -252
- package/src/scenario-worker-class.ts +0 -1110
- package/src/script-utils.ts +0 -203
- package/src/types.ts +0 -239
- package/src/utils/browser-utils.ts +0 -348
- package/src/utils/coordinate-converter.ts +0 -162
- package/src/utils/page-info-retry.ts +0 -65
- package/src/utils/page-info-utils.ts +0 -285
- package/testchimp-runner-core-0.0.35.tgz +0 -0
- package/tsconfig.json +0 -19
|
@@ -1,1110 +0,0 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import { EventEmitter } from 'events';
|
|
4
|
-
import { getEnhancedPageInfo } from './utils/page-info-utils';
|
|
5
|
-
import { initializeBrowser, captureOptimizedScreenshot } from './utils/browser-utils';
|
|
6
|
-
import { LLMFacade } from './llm-facade';
|
|
7
|
-
import { ScenarioRunJob, ScenarioResponse, ScenarioStep } from './types';
|
|
8
|
-
import { FileHandler } from './file-handler';
|
|
9
|
-
import { AuthConfig } from './auth-config';
|
|
10
|
-
import { generateTestScript } from './script-utils';
|
|
11
|
-
import { DEFAULT_MODEL, VISION_MODEL } from './model-constants';
|
|
12
|
-
import { LLMProvider } from './llm-provider';
|
|
13
|
-
import { ProgressReporter, StepProgress, JobProgress, StepExecutionStatus } from './progress-reporter';
|
|
14
|
-
import { BackendProxyLLMProvider } from './providers/backend-proxy-llm-provider';
|
|
15
|
-
import {
|
|
16
|
-
OrchestratorAgent,
|
|
17
|
-
ToolRegistry,
|
|
18
|
-
JourneyMemory,
|
|
19
|
-
AgentConfig,
|
|
20
|
-
TakeScreenshotTool,
|
|
21
|
-
ViewPreviousScreenshotTool,
|
|
22
|
-
RefreshSomMarkersTool,
|
|
23
|
-
RecallHistoryTool,
|
|
24
|
-
InspectPageTool,
|
|
25
|
-
CheckPageReadyTool,
|
|
26
|
-
ExtractDataTool,
|
|
27
|
-
VerifyActionResultTool
|
|
28
|
-
} from './orchestrator';
|
|
29
|
-
|
|
30
|
-
// Define a simple logging interface for compatibility
|
|
31
|
-
interface OutputChannel {
|
|
32
|
-
appendLine: (text: string) => void;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// Legacy interface for backward compatibility
|
|
36
|
-
interface ScenarioJob {
|
|
37
|
-
id: string;
|
|
38
|
-
scenario: string;
|
|
39
|
-
testName?: string;
|
|
40
|
-
playwrightConfig?: string;
|
|
41
|
-
model?: string;
|
|
42
|
-
scenarioFileName?: string;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const MAX_RETRIES_PER_STEP = 3; // 4 total attempts per sub-action: 3 DOM-only, then 1 potential vision attempt
|
|
47
|
-
const MAX_SUBACTIONS_PER_STEP = 5; // Maximum sub-actions to attempt for a single step (reduced from 10 to prevent excessive retries)
|
|
48
|
-
const MAX_FAILED_ATTEMPTS_PER_STEP = 12; // Hard limit on FAILED attempts per step across all sub-actions
|
|
49
|
-
|
|
50
|
-
export class ScenarioWorker extends EventEmitter {
|
|
51
|
-
private initialized = false;
|
|
52
|
-
private sessionId: string | null = null;
|
|
53
|
-
private llmFacade: LLMFacade;
|
|
54
|
-
private fileHandler?: FileHandler;
|
|
55
|
-
private outputChannel?: OutputChannel;
|
|
56
|
-
private progressReporter?: ProgressReporter;
|
|
57
|
-
private currentJobId?: string;
|
|
58
|
-
|
|
59
|
-
// Orchestrator mode
|
|
60
|
-
private useOrchestrator: boolean = false;
|
|
61
|
-
private orchestratorAgent?: OrchestratorAgent;
|
|
62
|
-
private toolRegistry?: ToolRegistry;
|
|
63
|
-
private orchestratorConfig?: Partial<AgentConfig>;
|
|
64
|
-
private debugMode: boolean = false;
|
|
65
|
-
|
|
66
|
-
constructor(
|
|
67
|
-
fileHandler?: FileHandler,
|
|
68
|
-
llmProvider?: LLMProvider,
|
|
69
|
-
progressReporter?: ProgressReporter,
|
|
70
|
-
authConfig?: AuthConfig,
|
|
71
|
-
backendUrl?: string,
|
|
72
|
-
options?: {
|
|
73
|
-
useOrchestrator?: boolean;
|
|
74
|
-
orchestratorConfig?: Partial<AgentConfig>;
|
|
75
|
-
debugMode?: boolean;
|
|
76
|
-
},
|
|
77
|
-
outputChannel?: OutputChannel
|
|
78
|
-
) {
|
|
79
|
-
super();
|
|
80
|
-
|
|
81
|
-
// Use provided LLM provider or default to backend proxy (backward compatible)
|
|
82
|
-
const actualLLMProvider = llmProvider || new BackendProxyLLMProvider(authConfig, backendUrl);
|
|
83
|
-
this.llmFacade = new LLMFacade(actualLLMProvider);
|
|
84
|
-
|
|
85
|
-
this.fileHandler = fileHandler;
|
|
86
|
-
this.progressReporter = progressReporter;
|
|
87
|
-
this.outputChannel = outputChannel; // Set outputChannel for log routing
|
|
88
|
-
|
|
89
|
-
// Orchestrator setup
|
|
90
|
-
this.useOrchestrator = options?.useOrchestrator || false;
|
|
91
|
-
this.orchestratorConfig = options?.orchestratorConfig;
|
|
92
|
-
this.debugMode = options?.debugMode || false;
|
|
93
|
-
|
|
94
|
-
if (this.useOrchestrator) {
|
|
95
|
-
this.initializeOrchestrator();
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Initialize orchestrator mode with tools
|
|
101
|
-
*/
|
|
102
|
-
private initializeOrchestrator(): void {
|
|
103
|
-
// Create tool registry
|
|
104
|
-
this.toolRegistry = new ToolRegistry();
|
|
105
|
-
|
|
106
|
-
// Create tools (READ-ONLY information gathering only)
|
|
107
|
-
const takeScreenshotTool = new TakeScreenshotTool();
|
|
108
|
-
takeScreenshotTool.setLLMFacade(this.llmFacade); // Inject LLM for vision analysis
|
|
109
|
-
|
|
110
|
-
const viewPreviousScreenshotTool = new ViewPreviousScreenshotTool();
|
|
111
|
-
viewPreviousScreenshotTool.setLLMFacade(this.llmFacade); // Inject LLM for vision analysis
|
|
112
|
-
|
|
113
|
-
const refreshSomMarkersTool = new RefreshSomMarkersTool();
|
|
114
|
-
|
|
115
|
-
const verifyActionTool = new VerifyActionResultTool();
|
|
116
|
-
verifyActionTool.setLLMFacade(this.llmFacade); // Inject LLM for vision comparison
|
|
117
|
-
|
|
118
|
-
// Register 8 information-gathering tools (state changes via Playwright commands)
|
|
119
|
-
this.toolRegistry.register(takeScreenshotTool);
|
|
120
|
-
this.toolRegistry.register(viewPreviousScreenshotTool);
|
|
121
|
-
this.toolRegistry.register(refreshSomMarkersTool);
|
|
122
|
-
this.toolRegistry.register(new RecallHistoryTool());
|
|
123
|
-
this.toolRegistry.register(new InspectPageTool());
|
|
124
|
-
this.toolRegistry.register(new CheckPageReadyTool());
|
|
125
|
-
this.toolRegistry.register(new ExtractDataTool());
|
|
126
|
-
this.toolRegistry.register(verifyActionTool);
|
|
127
|
-
|
|
128
|
-
// Create orchestrator agent
|
|
129
|
-
this.orchestratorAgent = new OrchestratorAgent(
|
|
130
|
-
this.llmFacade,
|
|
131
|
-
this.toolRegistry,
|
|
132
|
-
this.orchestratorConfig,
|
|
133
|
-
this.progressReporter,
|
|
134
|
-
(message, level) => {
|
|
135
|
-
if (level === 'error') {
|
|
136
|
-
this.logError(message);
|
|
137
|
-
} else if (level === 'warn') {
|
|
138
|
-
this.log(`⚠️ ${message}`);
|
|
139
|
-
} else if (level === 'debug') {
|
|
140
|
-
if (this.debugMode) {
|
|
141
|
-
this.log(`🐛 ${message}`);
|
|
142
|
-
}
|
|
143
|
-
} else {
|
|
144
|
-
this.log(message);
|
|
145
|
-
}
|
|
146
|
-
},
|
|
147
|
-
this.debugMode // Pass debug mode
|
|
148
|
-
);
|
|
149
|
-
|
|
150
|
-
// Minimal initialization logging - internal details not needed by consumer
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
private log(message: string): void {
|
|
154
|
-
// Let consumer add timestamps - just report the raw message
|
|
155
|
-
const formattedMessage = `[ScenarioWorker] ${message}`;
|
|
156
|
-
// Always log to console for debug visibility
|
|
157
|
-
console.log(formattedMessage);
|
|
158
|
-
// Also route to outputChannel if provided
|
|
159
|
-
if (this.outputChannel) {
|
|
160
|
-
this.outputChannel.appendLine(formattedMessage);
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
private logError(message: string): void {
|
|
165
|
-
// Let consumer add timestamps - just report the raw message
|
|
166
|
-
const formattedMessage = `[ScenarioWorker] ERROR: ${message}`;
|
|
167
|
-
// Always log to console for debug visibility
|
|
168
|
-
console.error(formattedMessage);
|
|
169
|
-
// Also route to outputChannel if provided
|
|
170
|
-
if (this.outputChannel) {
|
|
171
|
-
this.outputChannel.appendLine(formattedMessage);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* Capture screenshot as data URL
|
|
177
|
-
* Returns data:image/png;base64,... format
|
|
178
|
-
*/
|
|
179
|
-
private async captureStepScreenshot(page: any): Promise<string | undefined> {
|
|
180
|
-
try {
|
|
181
|
-
const screenshot = await page.screenshot({ type: 'png' });
|
|
182
|
-
const base64 = screenshot.toString('base64');
|
|
183
|
-
return `data:image/png;base64,${base64}`;
|
|
184
|
-
} catch (error) {
|
|
185
|
-
this.log(`Failed to capture screenshot: ${error}`);
|
|
186
|
-
return undefined;
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* Report step progress to progress reporter
|
|
192
|
-
*/
|
|
193
|
-
private async reportStepProgress(progress: StepProgress): Promise<void> {
|
|
194
|
-
// Report to progress reporter if available
|
|
195
|
-
await this.progressReporter?.onStepProgress?.(progress);
|
|
196
|
-
|
|
197
|
-
// Still emit events for backward compatibility
|
|
198
|
-
this.emit('stepProgress', progress);
|
|
199
|
-
|
|
200
|
-
// Also log for visibility
|
|
201
|
-
this.progressReporter?.log?.(
|
|
202
|
-
`Step ${progress.stepNumber} [${progress.status}]: ${progress.description}`,
|
|
203
|
-
progress.status === StepExecutionStatus.FAILURE ? 'error' : 'log'
|
|
204
|
-
);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/**
|
|
208
|
-
* Report job progress to progress reporter
|
|
209
|
-
*/
|
|
210
|
-
private async reportJobProgress(progress: JobProgress): Promise<void> {
|
|
211
|
-
// Report to progress reporter if available
|
|
212
|
-
await this.progressReporter?.onJobProgress?.(progress);
|
|
213
|
-
|
|
214
|
-
// Still emit events for backward compatibility
|
|
215
|
-
this.emit('jobProgress', progress);
|
|
216
|
-
|
|
217
|
-
// Also log for visibility
|
|
218
|
-
this.progressReporter?.log?.(`Job ${progress.jobId}: ${progress.status}`);
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
/**
|
|
222
|
-
* Detect if a step is complex and benefits from proactive vision usage
|
|
223
|
-
* Complex steps: form filling, verification, navigation after actions
|
|
224
|
-
*/
|
|
225
|
-
private isComplexStep(stepDescription: string): boolean {
|
|
226
|
-
const description = stepDescription.toLowerCase();
|
|
227
|
-
|
|
228
|
-
// Verification steps - often need visual confirmation
|
|
229
|
-
if (description.includes('verify') || description.includes('check') ||
|
|
230
|
-
description.includes('confirm') || description.includes('ensure')) {
|
|
231
|
-
return true;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// Form-related steps - multiple fields, complex interactions
|
|
235
|
-
if (description.includes('fill') && (description.includes('form') || description.includes('field'))) {
|
|
236
|
-
return true;
|
|
237
|
-
}
|
|
238
|
-
if (description.includes('enter') && description.includes('information')) {
|
|
239
|
-
return true;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// Steps that typically follow navigation (page may still be loading)
|
|
243
|
-
if (description.includes('click') && (
|
|
244
|
-
description.includes('menu') ||
|
|
245
|
-
description.includes('tab') ||
|
|
246
|
-
description.includes('link')
|
|
247
|
-
)) {
|
|
248
|
-
return true;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// Multi-step actions indicated by "and" or commas
|
|
252
|
-
if (description.includes(' and ') || description.split(',').length > 1) {
|
|
253
|
-
return true;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
return false;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
async initialize(): Promise<void> {
|
|
260
|
-
try {
|
|
261
|
-
this.sessionId = `scenario_worker_${Date.now()}`;
|
|
262
|
-
this.initialized = true;
|
|
263
|
-
// Minimal initialization - consumer doesn't need to see internal details
|
|
264
|
-
} catch (error) {
|
|
265
|
-
this.logError(`Scenario worker initialization error: ${error}`);
|
|
266
|
-
throw error;
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
async processScenarioJob(job: ScenarioRunJob): Promise<ScenarioResponse> {
|
|
271
|
-
if (!this.initialized) {
|
|
272
|
-
throw new Error('Scenario worker not initialized');
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
// Set current job ID for progress reporting
|
|
276
|
-
this.currentJobId = job.id;
|
|
277
|
-
|
|
278
|
-
// Log library version once (read from package.json)
|
|
279
|
-
const packageJson = require('../package.json');
|
|
280
|
-
this.log(`testchimp-runner-core v${packageJson.version}`);
|
|
281
|
-
this.log(`📋 Processing scenario: ${job.scenario}`);
|
|
282
|
-
|
|
283
|
-
const startTime = Date.now();
|
|
284
|
-
const steps: ScenarioStep[] = [];
|
|
285
|
-
let generatedScript = '';
|
|
286
|
-
let scriptPath: string | undefined;
|
|
287
|
-
let browser: any | undefined;
|
|
288
|
-
let context: any | undefined;
|
|
289
|
-
let page: any | undefined;
|
|
290
|
-
let overallSuccess = true;
|
|
291
|
-
|
|
292
|
-
try {
|
|
293
|
-
// Report job started
|
|
294
|
-
await this.reportJobProgress({
|
|
295
|
-
jobId: job.id,
|
|
296
|
-
status: 'started',
|
|
297
|
-
testName: job.testName
|
|
298
|
-
});
|
|
299
|
-
|
|
300
|
-
// 1. Break down scenario into steps using LLM
|
|
301
|
-
const scenarioSteps = await this.llmFacade.breakdownScenario(job.scenario, job.model);
|
|
302
|
-
steps.push(...scenarioSteps);
|
|
303
|
-
|
|
304
|
-
// Report total steps
|
|
305
|
-
await this.reportJobProgress({
|
|
306
|
-
jobId: job.id,
|
|
307
|
-
status: 'in_progress',
|
|
308
|
-
testName: job.testName,
|
|
309
|
-
totalSteps: steps.length,
|
|
310
|
-
currentStep: 0
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
// Emit log events for steps breakdown
|
|
314
|
-
this.emit('log', job.id, `\n## Steps Identified (${steps.length} total)\n`);
|
|
315
|
-
for (const step of steps) {
|
|
316
|
-
this.emit('log', job.id, `${step.stepNumber}. ${step.description}\n`);
|
|
317
|
-
}
|
|
318
|
-
this.emit('log', job.id, `\n## Execution Progress\n\n`);
|
|
319
|
-
|
|
320
|
-
// 2. Start a new browser session or use existing one
|
|
321
|
-
if (job.existingBrowser && job.existingContext && job.existingPage) {
|
|
322
|
-
// Use existing browser provided by caller (e.g., scriptservice)
|
|
323
|
-
this.log('Using existing browser/page provided by caller');
|
|
324
|
-
browser = job.existingBrowser;
|
|
325
|
-
context = job.existingContext;
|
|
326
|
-
page = job.existingPage;
|
|
327
|
-
} else {
|
|
328
|
-
// Create new browser (default behavior for local clients)
|
|
329
|
-
// Let the playwrightConfig control headless mode (don't override with hardcoded value)
|
|
330
|
-
// Create logger function from outputChannel for browser initialization
|
|
331
|
-
const logger = this.outputChannel ? (message: string, level?: 'log' | 'error' | 'warn') => {
|
|
332
|
-
this.outputChannel!.appendLine(`[Browser] ${message}`);
|
|
333
|
-
} : undefined;
|
|
334
|
-
const browserInstance = await initializeBrowser(job.playwrightConfig, undefined, undefined, logger);
|
|
335
|
-
browser = browserInstance.browser;
|
|
336
|
-
context = browserInstance.context;
|
|
337
|
-
page = browserInstance.page;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
// LIFECYCLE: Call beforeStartTest if provided
|
|
341
|
-
if (this.progressReporter?.beforeStartTest) {
|
|
342
|
-
await this.progressReporter.beforeStartTest(page, browser, context);
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
// Set reasonable timeout for most operations
|
|
346
|
-
// 5 seconds for element interactions (fast feedback on wrong selectors)
|
|
347
|
-
// Navigation operations should use explicit longer timeouts
|
|
348
|
-
page.setDefaultTimeout(5000);
|
|
349
|
-
|
|
350
|
-
let previousSteps: ScenarioStep[] = [];
|
|
351
|
-
let lastError: string | undefined;
|
|
352
|
-
let consecutiveFailures = 0;
|
|
353
|
-
const MAX_CONSECUTIVE_FAILURES = this.orchestratorConfig?.maxConsecutiveStepFailures || 3;
|
|
354
|
-
const CONTINUE_ON_FAILURE = this.orchestratorConfig?.continueOnStepFailure !== false; // Default true
|
|
355
|
-
|
|
356
|
-
// 3a. ORCHESTRATOR MODE - Use orchestrator agent for execution
|
|
357
|
-
if (this.useOrchestrator && this.orchestratorAgent) {
|
|
358
|
-
this.log(`🤖 Using Orchestrator Mode (continueOnFailure: ${CONTINUE_ON_FAILURE})`);
|
|
359
|
-
|
|
360
|
-
// Initialize journey memory
|
|
361
|
-
const memory: JourneyMemory = {
|
|
362
|
-
history: [],
|
|
363
|
-
experiences: [],
|
|
364
|
-
extractedData: {}
|
|
365
|
-
};
|
|
366
|
-
|
|
367
|
-
// Execute steps using orchestrator
|
|
368
|
-
for (let i = 0; i < steps.length; i++) {
|
|
369
|
-
// Only stop if consecutive failures exceed limit AND continueOnFailure is false
|
|
370
|
-
if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES && !CONTINUE_ON_FAILURE) {
|
|
371
|
-
this.log(`🛑 Stopping execution: ${consecutiveFailures} consecutive failures`);
|
|
372
|
-
// Mark remaining steps as skipped
|
|
373
|
-
for (let j = i; j < steps.length; j++) {
|
|
374
|
-
steps[j].success = false;
|
|
375
|
-
steps[j].error = `Skipped due to ${consecutiveFailures} consecutive failures`;
|
|
376
|
-
steps[j].playwrightCommands = [];
|
|
377
|
-
}
|
|
378
|
-
overallSuccess = false;
|
|
379
|
-
break;
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
// Warn if approaching limit (even with continueOnFailure)
|
|
383
|
-
if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES && CONTINUE_ON_FAILURE) {
|
|
384
|
-
this.log(`⚠️ ${consecutiveFailures} consecutive failures - continuing but scenario may have issues`);
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
const step = steps[i];
|
|
388
|
-
step.stepNumber = i + 1;
|
|
389
|
-
|
|
390
|
-
try {
|
|
391
|
-
// LIFECYCLE: Call beforeStepStart if provided
|
|
392
|
-
if (this.progressReporter?.beforeStepStart) {
|
|
393
|
-
await this.progressReporter.beforeStepStart(
|
|
394
|
-
{
|
|
395
|
-
stepNumber: step.stepNumber,
|
|
396
|
-
description: step.description
|
|
397
|
-
},
|
|
398
|
-
page
|
|
399
|
-
);
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
// Use orchestrator to execute this step
|
|
403
|
-
const result = await this.orchestratorAgent.executeStep(
|
|
404
|
-
page,
|
|
405
|
-
step.description,
|
|
406
|
-
step.stepNumber,
|
|
407
|
-
steps.length,
|
|
408
|
-
steps.map(s => s.description),
|
|
409
|
-
memory,
|
|
410
|
-
job.id
|
|
411
|
-
);
|
|
412
|
-
|
|
413
|
-
// Update step with result
|
|
414
|
-
step.success = result.success;
|
|
415
|
-
step.playwrightCommands = result.commands;
|
|
416
|
-
step.error = result.error;
|
|
417
|
-
|
|
418
|
-
if (result.success) {
|
|
419
|
-
this.log(`✓ Step ${step.stepNumber} completed via orchestrator (${result.iterations} iterations)`);
|
|
420
|
-
consecutiveFailures = 0;
|
|
421
|
-
} else {
|
|
422
|
-
this.log(`✗ Step ${step.stepNumber} failed via orchestrator: ${result.terminationReason}`);
|
|
423
|
-
consecutiveFailures++;
|
|
424
|
-
overallSuccess = false;
|
|
425
|
-
|
|
426
|
-
// CRITICAL: Stop on agent_stuck or infeasible (explicit agent decision)
|
|
427
|
-
// continueOnStepFailure only applies to command failures, not agent decisions
|
|
428
|
-
if (result.terminationReason === 'agent_stuck' || result.terminationReason === 'infeasible') {
|
|
429
|
-
this.log(`🛑 Stopping: Agent declared step ${result.terminationReason} - cannot continue`);
|
|
430
|
-
// Mark remaining steps as skipped
|
|
431
|
-
for (let j = i + 1; j < steps.length; j++) {
|
|
432
|
-
steps[j].success = false;
|
|
433
|
-
steps[j].error = `Skipped: Previous step was ${result.terminationReason}`;
|
|
434
|
-
steps[j].playwrightCommands = [];
|
|
435
|
-
}
|
|
436
|
-
break; // Exit loop
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
// REPORT FINAL STEP RESULT (after orchestrator completes all iterations)
|
|
441
|
-
// This gives the complete accumulated commands, not just one iteration
|
|
442
|
-
await this.reportStepProgress({
|
|
443
|
-
jobId: job.id,
|
|
444
|
-
stepNumber: step.stepNumber,
|
|
445
|
-
description: step.description,
|
|
446
|
-
code: step.playwrightCommands?.join('\n') || '', // All accumulated commands
|
|
447
|
-
status: step.success ? StepExecutionStatus.SUCCESS : StepExecutionStatus.FAILURE,
|
|
448
|
-
error: step.error,
|
|
449
|
-
agentIteration: result.iterations
|
|
450
|
-
});
|
|
451
|
-
|
|
452
|
-
} catch (error: any) {
|
|
453
|
-
this.logError(`Orchestrator execution failed for step ${step.stepNumber}: ${error.message}`);
|
|
454
|
-
step.success = false;
|
|
455
|
-
step.error = error.message;
|
|
456
|
-
consecutiveFailures++;
|
|
457
|
-
overallSuccess = false;
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
previousSteps.push(step);
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
} else {
|
|
464
|
-
// 3b. LEGACY MODE - Use existing retry loop
|
|
465
|
-
// Execute each step (steps may require multiple commands)
|
|
466
|
-
for (let i = 0; i < steps.length; i++) {
|
|
467
|
-
// Check if we should stop execution due to consecutive failures
|
|
468
|
-
if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) {
|
|
469
|
-
this.log(`🛑 Stopping execution: ${consecutiveFailures} consecutive failures detected`);
|
|
470
|
-
this.log(` Remaining ${steps.length - i} steps will be skipped to avoid wasting resources`);
|
|
471
|
-
|
|
472
|
-
// Emit log events about early termination
|
|
473
|
-
this.emit('log', job.id, `\n🛑 EARLY TERMINATION\n`);
|
|
474
|
-
this.emit('log', job.id, `Reason: ${consecutiveFailures} consecutive step failures\n`);
|
|
475
|
-
this.emit('log', job.id, `Steps attempted: ${i}\n`);
|
|
476
|
-
this.emit('log', job.id, `Steps skipped: ${steps.length - i}\n\n`);
|
|
477
|
-
|
|
478
|
-
// Mark remaining steps as skipped
|
|
479
|
-
for (let j = i; j < steps.length; j++) {
|
|
480
|
-
const skippedStep = steps[j];
|
|
481
|
-
skippedStep.stepNumber = j + 1;
|
|
482
|
-
skippedStep.success = false;
|
|
483
|
-
skippedStep.error = `Skipped due to ${consecutiveFailures} consecutive failures in previous steps`;
|
|
484
|
-
skippedStep.playwrightCommands = [];
|
|
485
|
-
previousSteps.push(skippedStep);
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
overallSuccess = false;
|
|
489
|
-
break; // Exit the loop
|
|
490
|
-
}
|
|
491
|
-
const step = steps[i];
|
|
492
|
-
step.stepNumber = i + 1;
|
|
493
|
-
step.retryCount = 0;
|
|
494
|
-
step.subActions = [];
|
|
495
|
-
// Force new array and clear any previous command data
|
|
496
|
-
step.playwrightCommands = [];
|
|
497
|
-
step.playwrightCommand = undefined;
|
|
498
|
-
|
|
499
|
-
let stepSuccess = false;
|
|
500
|
-
let stepError: string | undefined;
|
|
501
|
-
let subActionCount = 0;
|
|
502
|
-
let stepComplete = false;
|
|
503
|
-
let totalFailedAttemptsForStep = 0; // Track FAILED attempts across all sub-actions
|
|
504
|
-
|
|
505
|
-
// Detect if this is a complex step that benefits from proactive vision
|
|
506
|
-
const isComplexStep = this.isComplexStep(step.description);
|
|
507
|
-
|
|
508
|
-
// A step might need multiple commands (sub-actions) to complete
|
|
509
|
-
while (!stepComplete && subActionCount < MAX_SUBACTIONS_PER_STEP && totalFailedAttemptsForStep < MAX_FAILED_ATTEMPTS_PER_STEP) {
|
|
510
|
-
let subActionSuccess = false;
|
|
511
|
-
let subActionCommand: string | undefined;
|
|
512
|
-
let subActionError: string | undefined;
|
|
513
|
-
let subActionRetries = 0;
|
|
514
|
-
let usedVisionMode = false;
|
|
515
|
-
|
|
516
|
-
// Build context about what's been done so far in this step
|
|
517
|
-
const stepContext = step.subActions && step.subActions.length > 0
|
|
518
|
-
? `\nSub-actions completed so far for this step:\n${step.subActions.map((sa, idx) => ` ${idx + 1}. ${sa.command} - ${sa.success ? 'SUCCESS' : 'FAILED'}`).join('\n')}`
|
|
519
|
-
: '';
|
|
520
|
-
|
|
521
|
-
for (let attempt = 0; attempt <= MAX_RETRIES_PER_STEP; attempt++) {
|
|
522
|
-
// Check if we've exceeded failed attempts budget BEFORE attempting
|
|
523
|
-
if (totalFailedAttemptsForStep >= MAX_FAILED_ATTEMPTS_PER_STEP) {
|
|
524
|
-
this.log(` ⚠️ Exceeded failed attempts budget (${MAX_FAILED_ATTEMPTS_PER_STEP}) for this step`);
|
|
525
|
-
stepComplete = true;
|
|
526
|
-
stepSuccess = false;
|
|
527
|
-
stepError = `Exceeded maximum failed attempts (${MAX_FAILED_ATTEMPTS_PER_STEP}) for step`;
|
|
528
|
-
break;
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
let currentAttemptCommand: string | undefined;
|
|
532
|
-
let currentAttemptSuccess = false;
|
|
533
|
-
let currentAttemptError: string | undefined;
|
|
534
|
-
const attemptTimestamp = Date.now();
|
|
535
|
-
|
|
536
|
-
try {
|
|
537
|
-
this.log(`Step ${step.stepNumber} - Sub-action ${subActionCount + 1}, Attempt ${attempt + 1}: ${step.description}`);
|
|
538
|
-
|
|
539
|
-
// Get current page state - handle navigation in progress
|
|
540
|
-
let domSnapshot;
|
|
541
|
-
let pageInfo;
|
|
542
|
-
try {
|
|
543
|
-
domSnapshot = {
|
|
544
|
-
url: page.url(),
|
|
545
|
-
title: await page.title(),
|
|
546
|
-
accessibilityTree: await page.accessibility.snapshot()
|
|
547
|
-
};
|
|
548
|
-
pageInfo = await getEnhancedPageInfo(domSnapshot);
|
|
549
|
-
} catch (contextError: any) {
|
|
550
|
-
// If execution context was destroyed (navigation in progress), wait and retry
|
|
551
|
-
if (contextError.message && contextError.message.includes('Execution context was destroyed')) {
|
|
552
|
-
this.log(` ⏳ Navigation in progress, waiting for page to load...`);
|
|
553
|
-
await page.waitForLoadState('domcontentloaded', { timeout: 5000 }).catch(() => {});
|
|
554
|
-
// Retry page state capture
|
|
555
|
-
domSnapshot = {
|
|
556
|
-
url: page.url(),
|
|
557
|
-
title: await page.title(),
|
|
558
|
-
accessibilityTree: await page.accessibility.snapshot()
|
|
559
|
-
};
|
|
560
|
-
pageInfo = await getEnhancedPageInfo(domSnapshot);
|
|
561
|
-
} else {
|
|
562
|
-
throw contextError; // Re-throw if it's not a navigation issue
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
// Vision trigger: Liberal usage since gpt-5-mini vision is cost-effective
|
|
567
|
-
const modelToUse = job.model || DEFAULT_MODEL;
|
|
568
|
-
let command: string | null;
|
|
569
|
-
|
|
570
|
-
// Enhanced logging for vision trigger logic
|
|
571
|
-
this.log(` 🔍 Vision trigger check: subAction=${subActionCount + 1}, attempt=${attempt}, totalFailed=${totalFailedAttemptsForStep}, usedVision=${usedVisionMode}`);
|
|
572
|
-
|
|
573
|
-
// Liberal vision strategy (gpt-5-mini is cost-effective):
|
|
574
|
-
// 1. After ANY failure (1+) → use vision
|
|
575
|
-
// 2. Complex steps → use vision from attempt 1
|
|
576
|
-
// 3. No LLM assessment gate → go directly to vision
|
|
577
|
-
const hasFailure = totalFailedAttemptsForStep >= 1 && lastError;
|
|
578
|
-
const shouldUseProactiveVision = isComplexStep && attempt === 0; // First attempt for complex steps
|
|
579
|
-
const shouldUseVision = (hasFailure || shouldUseProactiveVision) && !usedVisionMode;
|
|
580
|
-
|
|
581
|
-
if (shouldUseVision) {
|
|
582
|
-
if (shouldUseProactiveVision) {
|
|
583
|
-
this.log(` 🎯 PROACTIVE VISION: Complex step detected, using vision from first attempt`);
|
|
584
|
-
} else {
|
|
585
|
-
this.log(` 🎯 VISION TRIGGER: ${totalFailedAttemptsForStep} failure(s) detected, using vision (no LLM gate)`);
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
// Two-step supervisor pattern:
|
|
589
|
-
// 1. Supervisor analyzes screenshot and provides instructions
|
|
590
|
-
// 2. Worker generates command based on those instructions
|
|
591
|
-
|
|
592
|
-
this.log(` 📸 Taking screenshot for supervisor analysis...`);
|
|
593
|
-
|
|
594
|
-
// Capture optimized screenshot using utility method
|
|
595
|
-
const imageDataUrl = await captureOptimizedScreenshot(
|
|
596
|
-
page,
|
|
597
|
-
{ timeout: 10000 }, // Uses default quality 60
|
|
598
|
-
(msg) => this.log(msg)
|
|
599
|
-
);
|
|
600
|
-
|
|
601
|
-
this.log(` 👔 STEP 1: Supervisor analyzing screenshot (${VISION_MODEL})...`);
|
|
602
|
-
const supervisorDiagnostics = await this.llmFacade.getVisionDiagnostics(
|
|
603
|
-
step.description + stepContext,
|
|
604
|
-
pageInfo,
|
|
605
|
-
previousSteps,
|
|
606
|
-
lastError,
|
|
607
|
-
imageDataUrl,
|
|
608
|
-
VISION_MODEL
|
|
609
|
-
);
|
|
610
|
-
|
|
611
|
-
// DEBUG: Log vision diagnostics
|
|
612
|
-
this.log(` 📸 Visual insights: ${supervisorDiagnostics.visualAnalysis}`);
|
|
613
|
-
this.log(` 🔍 Root cause: ${supervisorDiagnostics.rootCause}`);
|
|
614
|
-
this.log(` 💡 Recommended approach: ${supervisorDiagnostics.recommendedApproach}`);
|
|
615
|
-
if (supervisorDiagnostics.elementsFound.length > 0) {
|
|
616
|
-
this.log(` ✅ Elements found: ${supervisorDiagnostics.elementsFound.join(', ')}`);
|
|
617
|
-
}
|
|
618
|
-
if (supervisorDiagnostics.elementsNotFound.length > 0) {
|
|
619
|
-
this.log(` ❌ Elements not found: ${supervisorDiagnostics.elementsNotFound.join(', ')}`);
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
this.log(` 🔨 STEP 2: Worker generating command from supervisor instructions (${DEFAULT_MODEL})...`);
|
|
623
|
-
command = await this.llmFacade.generateCommandFromSupervisorInstructions(
|
|
624
|
-
step.description + stepContext,
|
|
625
|
-
supervisorDiagnostics,
|
|
626
|
-
pageInfo,
|
|
627
|
-
modelToUse // Cheaper model for command generation
|
|
628
|
-
);
|
|
629
|
-
usedVisionMode = true;
|
|
630
|
-
} else {
|
|
631
|
-
// Not using vision - use regular DOM-based approach
|
|
632
|
-
if (usedVisionMode) {
|
|
633
|
-
this.log(` 📝 Vision already used - using DOM-based approach`);
|
|
634
|
-
} else if (isComplexStep) {
|
|
635
|
-
this.log(` 📝 Complex step, but first attempt - using DOM-based approach (vision on retry)`);
|
|
636
|
-
} else {
|
|
637
|
-
this.log(` 📝 Using DOM-based approach (${totalFailedAttemptsForStep} failures so far)`);
|
|
638
|
-
}
|
|
639
|
-
const stepDescriptionWithContext = step.description + stepContext;
|
|
640
|
-
command = await this.llmFacade.generatePlaywrightCommand(
|
|
641
|
-
stepDescriptionWithContext,
|
|
642
|
-
pageInfo,
|
|
643
|
-
previousSteps,
|
|
644
|
-
lastError,
|
|
645
|
-
step,
|
|
646
|
-
modelToUse
|
|
647
|
-
);
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
if (!command) {
|
|
651
|
-
throw new Error('LLM failed to generate a Playwright command.');
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
currentAttemptCommand = command;
|
|
655
|
-
this.log(` Command: ${command}`);
|
|
656
|
-
|
|
657
|
-
// Execute the command
|
|
658
|
-
await this.executePlaywrightCommand(page, browser, context, command);
|
|
659
|
-
|
|
660
|
-
// Success
|
|
661
|
-
subActionSuccess = true;
|
|
662
|
-
currentAttemptSuccess = true;
|
|
663
|
-
subActionCommand = command;
|
|
664
|
-
step.playwrightCommands!.push(command);
|
|
665
|
-
this.log(` ✅ SUCCESS: ${command}${usedVisionMode ? ' (vision-aided)' : ''}`);
|
|
666
|
-
|
|
667
|
-
// Wait a bit for any navigation that might have been triggered
|
|
668
|
-
// This prevents "Execution context destroyed" errors when checking goal completion
|
|
669
|
-
await page.waitForLoadState('domcontentloaded', { timeout: 3000 }).catch(() => {
|
|
670
|
-
// Ignore timeout - page might not be navigating
|
|
671
|
-
});
|
|
672
|
-
|
|
673
|
-
break; // Sub-action successful, check if step is complete
|
|
674
|
-
} catch (error: any) {
|
|
675
|
-
subActionError = error instanceof Error ? error.message : String(error);
|
|
676
|
-
currentAttemptError = subActionError;
|
|
677
|
-
|
|
678
|
-
// Get current URL for context (especially useful for navigation failures)
|
|
679
|
-
let currentUrl = 'unknown';
|
|
680
|
-
try {
|
|
681
|
-
currentUrl = page.url();
|
|
682
|
-
} catch (e) {
|
|
683
|
-
// Ignore if we can't get URL
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
// Enhanced error message with current URL
|
|
687
|
-
const errorWithContext = `${subActionError} | Current URL: ${currentUrl}`;
|
|
688
|
-
|
|
689
|
-
this.logError(` ❌ FAILED (attempt ${attempt + 1}): ${subActionError}`);
|
|
690
|
-
this.logError(` Current URL: ${currentUrl}`);
|
|
691
|
-
this.logError(` Command attempted: ${currentAttemptCommand || 'N/A'}`);
|
|
692
|
-
subActionRetries++;
|
|
693
|
-
totalFailedAttemptsForStep++; // Increment failed attempts counter
|
|
694
|
-
|
|
695
|
-
// Only update lastError if this is the final attempt
|
|
696
|
-
if (attempt === MAX_RETRIES_PER_STEP) {
|
|
697
|
-
lastError = errorWithContext; // Include URL in error context for LLM
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
// If this is the last attempt, mark sub-action as failed
|
|
701
|
-
if (attempt === MAX_RETRIES_PER_STEP) {
|
|
702
|
-
subActionSuccess = false;
|
|
703
|
-
subActionCommand = currentAttemptCommand;
|
|
704
|
-
this.logError(` 🚫 SUB-ACTION FAILED after ${MAX_RETRIES_PER_STEP + 1} attempts (including vision mode if used)`);
|
|
705
|
-
break; // Exit retry loop
|
|
706
|
-
}
|
|
707
|
-
} finally {
|
|
708
|
-
if (!step.attempts) {
|
|
709
|
-
step.attempts = [];
|
|
710
|
-
}
|
|
711
|
-
step.attempts.push({
|
|
712
|
-
attemptNumber: attempt + 1,
|
|
713
|
-
command: currentAttemptCommand,
|
|
714
|
-
success: currentAttemptSuccess,
|
|
715
|
-
error: currentAttemptError,
|
|
716
|
-
timestamp: attemptTimestamp
|
|
717
|
-
});
|
|
718
|
-
}
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
// Record the sub-action
|
|
722
|
-
if (subActionCommand) {
|
|
723
|
-
step.subActions!.push({
|
|
724
|
-
command: subActionCommand,
|
|
725
|
-
success: subActionSuccess,
|
|
726
|
-
error: subActionError,
|
|
727
|
-
retryCount: subActionRetries
|
|
728
|
-
});
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
subActionCount++;
|
|
732
|
-
|
|
733
|
-
// Determine if step (goal) is complete
|
|
734
|
-
if (subActionSuccess) {
|
|
735
|
-
// After each successful sub-action, ask LLM if goal is complete
|
|
736
|
-
if (subActionCount >= MAX_SUBACTIONS_PER_STEP) {
|
|
737
|
-
// Safety limit - avoid infinite loops
|
|
738
|
-
stepComplete = true;
|
|
739
|
-
stepSuccess = true;
|
|
740
|
-
this.log(` ⚠️ Reached max sub-actions limit (${MAX_SUBACTIONS_PER_STEP}) with ${totalFailedAttemptsForStep} failed attempts, considering step complete`);
|
|
741
|
-
} else {
|
|
742
|
-
// Ask LLM if goal is complete
|
|
743
|
-
try {
|
|
744
|
-
// Capture page state - if navigation is still happening, retry once
|
|
745
|
-
let domSnapshot;
|
|
746
|
-
let pageInfo;
|
|
747
|
-
try {
|
|
748
|
-
domSnapshot = {
|
|
749
|
-
url: page.url(),
|
|
750
|
-
title: await page.title(),
|
|
751
|
-
accessibilityTree: await page.accessibility.snapshot()
|
|
752
|
-
};
|
|
753
|
-
pageInfo = await getEnhancedPageInfo(domSnapshot);
|
|
754
|
-
} catch (contextError: any) {
|
|
755
|
-
// If execution context was destroyed (navigation in progress), wait and retry
|
|
756
|
-
if (contextError.message && contextError.message.includes('Execution context was destroyed')) {
|
|
757
|
-
this.log(` ⏳ Navigation detected, waiting for page to load...`);
|
|
758
|
-
await page.waitForLoadState('domcontentloaded', { timeout: 5000 }).catch(() => {});
|
|
759
|
-
// Retry page state capture
|
|
760
|
-
domSnapshot = {
|
|
761
|
-
url: page.url(),
|
|
762
|
-
title: await page.title(),
|
|
763
|
-
accessibilityTree: await page.accessibility.snapshot()
|
|
764
|
-
};
|
|
765
|
-
pageInfo = await getEnhancedPageInfo(domSnapshot);
|
|
766
|
-
} else {
|
|
767
|
-
throw contextError; // Re-throw if it's not a navigation issue
|
|
768
|
-
}
|
|
769
|
-
}
|
|
770
|
-
|
|
771
|
-
// Vision-backed goal completion for complex/verification steps
|
|
772
|
-
const shouldUseVisionForCompletion = isComplexStep && subActionCount >= 1; // At least one action done
|
|
773
|
-
let completionCheck;
|
|
774
|
-
|
|
775
|
-
if (shouldUseVisionForCompletion) {
|
|
776
|
-
this.log(` 🎯 Vision-backed goal completion check (complex step)`);
|
|
777
|
-
|
|
778
|
-
// Capture screenshot for visual verification
|
|
779
|
-
const imageDataUrl = await captureOptimizedScreenshot(
|
|
780
|
-
page,
|
|
781
|
-
{ timeout: 10000 },
|
|
782
|
-
(msg) => this.log(msg)
|
|
783
|
-
);
|
|
784
|
-
|
|
785
|
-
// Use vision model to check goal completion with visual context
|
|
786
|
-
completionCheck = await this.llmFacade.checkGoalCompletionWithVision(
|
|
787
|
-
step.description,
|
|
788
|
-
step.playwrightCommands || [],
|
|
789
|
-
pageInfo,
|
|
790
|
-
imageDataUrl,
|
|
791
|
-
VISION_MODEL
|
|
792
|
-
);
|
|
793
|
-
} else {
|
|
794
|
-
// Regular DOM-based goal completion check
|
|
795
|
-
completionCheck = await this.llmFacade.checkGoalCompletion(
|
|
796
|
-
step.description,
|
|
797
|
-
step.playwrightCommands || [],
|
|
798
|
-
pageInfo,
|
|
799
|
-
job.model || DEFAULT_MODEL
|
|
800
|
-
);
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
this.log(` 🎯 Goal completion check: ${completionCheck.isComplete ? 'COMPLETE' : 'INCOMPLETE'} - ${completionCheck.reason}`);
|
|
804
|
-
|
|
805
|
-
if (completionCheck.isComplete) {
|
|
806
|
-
stepComplete = true;
|
|
807
|
-
stepSuccess = true;
|
|
808
|
-
} else {
|
|
809
|
-
// Continue with next sub-action
|
|
810
|
-
if (completionCheck.nextSubGoal) {
|
|
811
|
-
this.log(` 📍 Next sub-goal: ${completionCheck.nextSubGoal}`);
|
|
812
|
-
}
|
|
813
|
-
// Continue looping to generate next command
|
|
814
|
-
}
|
|
815
|
-
} catch (error) {
|
|
816
|
-
this.logError(`Error checking goal completion: ${error}`);
|
|
817
|
-
// Fallback: consider complete after 1 successful sub-action if we can't check
|
|
818
|
-
stepComplete = true;
|
|
819
|
-
stepSuccess = true;
|
|
820
|
-
}
|
|
821
|
-
}
|
|
822
|
-
} else {
|
|
823
|
-
// Sub-action failed
|
|
824
|
-
stepComplete = true; // Move on after failure
|
|
825
|
-
stepSuccess = false;
|
|
826
|
-
stepError = subActionError;
|
|
827
|
-
overallSuccess = false;
|
|
828
|
-
}
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
// Set the step's final command (last successful or aggregate)
|
|
832
|
-
if (step.playwrightCommands && step.playwrightCommands.length > 0) {
|
|
833
|
-
step.playwrightCommand = step.playwrightCommands[step.playwrightCommands.length - 1];
|
|
834
|
-
}
|
|
835
|
-
|
|
836
|
-
step.success = stepSuccess;
|
|
837
|
-
step.error = stepError;
|
|
838
|
-
previousSteps.push(step);
|
|
839
|
-
|
|
840
|
-
// Update consecutive failure counter
|
|
841
|
-
if (stepSuccess) {
|
|
842
|
-
consecutiveFailures = 0; // Reset on success
|
|
843
|
-
} else {
|
|
844
|
-
consecutiveFailures++;
|
|
845
|
-
this.log(`⚠️ Consecutive failures: ${consecutiveFailures}/${MAX_CONSECUTIVE_FAILURES}`);
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
// Emit step result log events
|
|
849
|
-
this.emit('log', job.id, `### Step ${step.stepNumber}: ${step.description}\n`);
|
|
850
|
-
this.emit('log', job.id, `Status: ${stepSuccess ? '✅ SUCCESS' : '❌ FAILED'}\n`);
|
|
851
|
-
this.emit('log', job.id, `Sub-actions: ${subActionCount}\n`);
|
|
852
|
-
this.emit('log', job.id, `Failed attempts: ${totalFailedAttemptsForStep}\n`);
|
|
853
|
-
|
|
854
|
-
if (step.playwrightCommands && step.playwrightCommands.length > 0) {
|
|
855
|
-
this.emit('log', job.id, `Commands:\n`);
|
|
856
|
-
step.playwrightCommands.forEach((cmd, idx) => {
|
|
857
|
-
this.emit('log', job.id, ` ${idx + 1}. ${cmd}\n`);
|
|
858
|
-
});
|
|
859
|
-
}
|
|
860
|
-
|
|
861
|
-
if (stepError) {
|
|
862
|
-
this.emit('log', job.id, `Error: ${stepError}\n`);
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
if (step.attempts && step.attempts.length > 0) {
|
|
866
|
-
this.emit('log', job.id, `Total attempts: ${step.attempts.length}\n`);
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
this.emit('log', job.id, `\n`);
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
} // End of else block (legacy mode)
|
|
873
|
-
|
|
874
|
-
// Generate test name if not provided
|
|
875
|
-
const testName = job.testName || await this.llmFacade.generateTestName(job.scenario, job.model);
|
|
876
|
-
|
|
877
|
-
// Generate hashtags for semantic grouping
|
|
878
|
-
const hashtags = await this.llmFacade.generateHashtags(job.scenario, job.model);
|
|
879
|
-
|
|
880
|
-
// Generate preferred filename
|
|
881
|
-
let preferredFileName: string;
|
|
882
|
-
if (testName && testName !== 'test') {
|
|
883
|
-
// Use the generated test name
|
|
884
|
-
const sanitizedName = testName
|
|
885
|
-
.replace(/[^a-zA-Z0-9\s-_]/g, '') // Remove special characters except spaces, hyphens, underscores
|
|
886
|
-
.replace(/\s+/g, '_') // Replace spaces with underscores
|
|
887
|
-
.replace(/_+/g, '_') // Replace multiple underscores with single underscore
|
|
888
|
-
.replace(/^_|_$/g, '') // Remove leading/trailing underscores
|
|
889
|
-
.toLowerCase();
|
|
890
|
-
preferredFileName = `${sanitizedName}.smart.spec.js`;
|
|
891
|
-
} else {
|
|
892
|
-
// Use scenario file name if no meaningful test name was generated
|
|
893
|
-
const scenarioFileName = job.scenarioFileName || 'scenario';
|
|
894
|
-
const baseName = scenarioFileName.replace(/\.[^/.]+$/, ''); // Remove extension
|
|
895
|
-
const sanitizedName = baseName
|
|
896
|
-
.replace(/[^a-zA-Z0-9\s-_]/g, '') // Remove special characters except spaces, hyphens, underscores
|
|
897
|
-
.replace(/\s+/g, '_') // Replace spaces with underscores
|
|
898
|
-
.replace(/_+/g, '_') // Replace multiple underscores with single underscore
|
|
899
|
-
.replace(/^_|_$/g, '') // Remove leading/trailing underscores
|
|
900
|
-
.toLowerCase();
|
|
901
|
-
preferredFileName = `${sanitizedName}.smart.spec.js`;
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
// Generate clean script with TestChimp comment and code
|
|
905
|
-
this.log(`[ScenarioWorker] Generating script from ${steps.length} steps`);
|
|
906
|
-
steps.forEach((s, i) => {
|
|
907
|
-
this.log(`[ScenarioWorker] Step ${i+1}: ${s.description}`);
|
|
908
|
-
this.log(`[ScenarioWorker] Commands: ${s.playwrightCommands?.length || 0}`);
|
|
909
|
-
if (s.playwrightCommands && s.playwrightCommands.length > 0) {
|
|
910
|
-
this.log(`[ScenarioWorker] First command: ${s.playwrightCommands[0]}`);
|
|
911
|
-
}
|
|
912
|
-
});
|
|
913
|
-
|
|
914
|
-
generatedScript = generateTestScript(testName, steps, undefined, hashtags);
|
|
915
|
-
this.log(`[ScenarioWorker] Generated script length: ${generatedScript.length}`);
|
|
916
|
-
this.log(`[ScenarioWorker] Script starts with: ${generatedScript.substring(0, 150)}...`);
|
|
917
|
-
|
|
918
|
-
// Perform final cleanup pass to remove redundancies and make minor adjustments
|
|
919
|
-
this.log(`[ScenarioWorker] Performing final script cleanup...`);
|
|
920
|
-
try {
|
|
921
|
-
const cleanupResult = await this.llmFacade.cleanupScript(generatedScript, job.model);
|
|
922
|
-
|
|
923
|
-
if (cleanupResult.changes && cleanupResult.changes.length > 0) {
|
|
924
|
-
this.log(`[ScenarioWorker] Cleanup made ${cleanupResult.changes.length} improvement(s):`);
|
|
925
|
-
cleanupResult.changes.forEach((change, i) => {
|
|
926
|
-
this.log(`[ScenarioWorker] ${i + 1}. ${change}`);
|
|
927
|
-
});
|
|
928
|
-
generatedScript = cleanupResult.script;
|
|
929
|
-
} else if (cleanupResult.skipped) {
|
|
930
|
-
this.log(`[ScenarioWorker] Cleanup skipped: ${cleanupResult.skipped}`);
|
|
931
|
-
} else {
|
|
932
|
-
this.log(`[ScenarioWorker] Cleanup completed - no changes needed`);
|
|
933
|
-
}
|
|
934
|
-
} catch (error: any) {
|
|
935
|
-
this.log(`[ScenarioWorker] Cleanup failed, using original script: ${error.message}`);
|
|
936
|
-
// Continue with original script on error
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
// Generate detailed execution log
|
|
940
|
-
const logLines: string[] = [];
|
|
941
|
-
logLines.push(`# Scenario Execution Log`);
|
|
942
|
-
logLines.push(`Job ID: ${job.id}`);
|
|
943
|
-
logLines.push(`Scenario: ${job.scenario}`);
|
|
944
|
-
logLines.push(`Start Time: ${new Date(startTime).toISOString()}`);
|
|
945
|
-
logLines.push(`End Time: ${new Date().toISOString()}`);
|
|
946
|
-
logLines.push(`Total Execution Time: ${Date.now() - startTime}ms`);
|
|
947
|
-
logLines.push(`Overall Success: ${overallSuccess ? 'YES' : 'NO'}`);
|
|
948
|
-
|
|
949
|
-
// Add early termination info if applicable
|
|
950
|
-
const skippedSteps = steps.filter(s => s.error?.includes('Skipped due to'));
|
|
951
|
-
if (skippedSteps.length > 0) {
|
|
952
|
-
logLines.push(`Early Termination: YES (${consecutiveFailures} consecutive failures)`);
|
|
953
|
-
logLines.push(`Steps Skipped: ${skippedSteps.length}`);
|
|
954
|
-
}
|
|
955
|
-
|
|
956
|
-
logLines.push(``);
|
|
957
|
-
|
|
958
|
-
for (const step of steps) {
|
|
959
|
-
logLines.push(`## Step ${step.stepNumber}: ${step.description}`);
|
|
960
|
-
logLines.push(`Status: ${step.success ? 'SUCCESS' : 'FAILED'}`);
|
|
961
|
-
logLines.push(`Retry Count: ${step.retryCount || 0}`);
|
|
962
|
-
|
|
963
|
-
if (step.playwrightCommand) {
|
|
964
|
-
logLines.push(`Final Command: ${step.playwrightCommand}`);
|
|
965
|
-
}
|
|
966
|
-
|
|
967
|
-
if (step.error) {
|
|
968
|
-
logLines.push(`Final Error: ${step.error}`);
|
|
969
|
-
}
|
|
970
|
-
|
|
971
|
-
if (step.attempts && step.attempts.length > 0) {
|
|
972
|
-
logLines.push(`### Attempts:`);
|
|
973
|
-
for (const attempt of step.attempts) {
|
|
974
|
-
logLines.push(`- Attempt ${attempt.attemptNumber}:`);
|
|
975
|
-
logLines.push(` Command: ${attempt.command || 'N/A'}`);
|
|
976
|
-
logLines.push(` Success: ${attempt.success ? 'YES' : 'NO'}`);
|
|
977
|
-
if (attempt.error) {
|
|
978
|
-
logLines.push(` Error: ${attempt.error}`);
|
|
979
|
-
}
|
|
980
|
-
logLines.push(` Timestamp: ${new Date(attempt.timestamp).toISOString()}`);
|
|
981
|
-
}
|
|
982
|
-
}
|
|
983
|
-
|
|
984
|
-
logLines.push(``);
|
|
985
|
-
}
|
|
986
|
-
|
|
987
|
-
const executionLog = logLines.join('\n');
|
|
988
|
-
|
|
989
|
-
// Report job completion
|
|
990
|
-
await this.reportJobProgress({
|
|
991
|
-
jobId: job.id,
|
|
992
|
-
status: overallSuccess ? 'completed' : 'failed',
|
|
993
|
-
testName,
|
|
994
|
-
script: generatedScript,
|
|
995
|
-
error: overallSuccess ? undefined : 'Some steps failed during execution'
|
|
996
|
-
});
|
|
997
|
-
|
|
998
|
-
return {
|
|
999
|
-
success: overallSuccess,
|
|
1000
|
-
steps,
|
|
1001
|
-
generatedScript,
|
|
1002
|
-
executionLog,
|
|
1003
|
-
executionTime: Date.now() - startTime,
|
|
1004
|
-
testName,
|
|
1005
|
-
preferredFileName
|
|
1006
|
-
};
|
|
1007
|
-
|
|
1008
|
-
} catch (error: any) {
|
|
1009
|
-
overallSuccess = false;
|
|
1010
|
-
this.logError(`Overall scenario processing error: ${error}`);
|
|
1011
|
-
|
|
1012
|
-
// Report job failure
|
|
1013
|
-
await this.reportJobProgress({
|
|
1014
|
-
jobId: job.id,
|
|
1015
|
-
status: 'failed',
|
|
1016
|
-
testName: job.testName || 'test',
|
|
1017
|
-
script: generatedScript,
|
|
1018
|
-
error: error instanceof Error ? error.message : 'Unknown error during scenario processing'
|
|
1019
|
-
});
|
|
1020
|
-
|
|
1021
|
-
return {
|
|
1022
|
-
success: false,
|
|
1023
|
-
steps,
|
|
1024
|
-
generatedScript,
|
|
1025
|
-
executionLog: `# Scenario Execution Log\nJob ID: ${job.id}\nScenario: ${job.scenario}\nError: ${error instanceof Error ? error.message : 'Unknown error during scenario processing'}`,
|
|
1026
|
-
executionTime: Date.now() - startTime,
|
|
1027
|
-
testName: job.testName || 'test',
|
|
1028
|
-
preferredFileName: 'test.spec.js',
|
|
1029
|
-
error: error instanceof Error ? error.message : 'Unknown error during scenario processing'
|
|
1030
|
-
};
|
|
1031
|
-
} finally {
|
|
1032
|
-
// LIFECYCLE: Call afterEndTest if provided
|
|
1033
|
-
if (browser && this.progressReporter?.afterEndTest) {
|
|
1034
|
-
try {
|
|
1035
|
-
await this.progressReporter.afterEndTest(
|
|
1036
|
-
overallSuccess ? 'passed' : 'failed',
|
|
1037
|
-
overallSuccess ? undefined : 'Test execution had failures',
|
|
1038
|
-
page
|
|
1039
|
-
);
|
|
1040
|
-
} catch (callbackError) {
|
|
1041
|
-
this.log(`afterEndTest callback failed: ${callbackError}`);
|
|
1042
|
-
}
|
|
1043
|
-
}
|
|
1044
|
-
|
|
1045
|
-
// Only close browser if we created it (not provided by caller)
|
|
1046
|
-
const usingExternalBrowser = !!(job.existingBrowser && job.existingContext && job.existingPage);
|
|
1047
|
-
if (browser && !usingExternalBrowser) {
|
|
1048
|
-
await browser.close();
|
|
1049
|
-
}
|
|
1050
|
-
}
|
|
1051
|
-
}
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
private async executePlaywrightCommand(
|
|
1059
|
-
page: any,
|
|
1060
|
-
browser: any,
|
|
1061
|
-
context: any,
|
|
1062
|
-
command: string
|
|
1063
|
-
): Promise<void> {
|
|
1064
|
-
// Detect if command contains navigation or load state operations that need longer timeout
|
|
1065
|
-
const needsLongerTimeout = command.includes('waitForLoadState') ||
|
|
1066
|
-
command.includes('goto(') ||
|
|
1067
|
-
command.includes('waitForURL') ||
|
|
1068
|
-
command.includes('waitForNavigation');
|
|
1069
|
-
|
|
1070
|
-
// Use appropriate timeout based on operation type
|
|
1071
|
-
const timeout = needsLongerTimeout ? 30000 : 5000;
|
|
1072
|
-
page.setDefaultTimeout(timeout);
|
|
1073
|
-
|
|
1074
|
-
try {
|
|
1075
|
-
// Execute command directly without validation
|
|
1076
|
-
const commandFunction = new Function('page', 'browser', 'context', 'expect', `
|
|
1077
|
-
return (async () => {
|
|
1078
|
-
${command}
|
|
1079
|
-
})();
|
|
1080
|
-
`);
|
|
1081
|
-
|
|
1082
|
-
// Dynamically import expect
|
|
1083
|
-
const { expect } = require('@playwright/test');
|
|
1084
|
-
await commandFunction(page, browser, context, expect);
|
|
1085
|
-
|
|
1086
|
-
} finally {
|
|
1087
|
-
// Reset to default timeout for element operations
|
|
1088
|
-
page.setDefaultTimeout(5000);
|
|
1089
|
-
}
|
|
1090
|
-
}
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
/**
|
|
1095
|
-
* Execute exploration mode using orchestrator
|
|
1096
|
-
*/
|
|
1097
|
-
async executeExploration(page: any, explorationConfig: any, jobId: string): Promise<any> {
|
|
1098
|
-
if (!this.useOrchestrator || !this.orchestratorAgent) {
|
|
1099
|
-
throw new Error('Orchestrator not available - exploration mode requires orchestrator');
|
|
1100
|
-
}
|
|
1101
|
-
|
|
1102
|
-
// Execute exploration via orchestrator
|
|
1103
|
-
return this.orchestratorAgent.executeExploration(page, explorationConfig, jobId);
|
|
1104
|
-
}
|
|
1105
|
-
|
|
1106
|
-
async cleanup(): Promise<void> {
|
|
1107
|
-
this.initialized = false;
|
|
1108
|
-
this.sessionId = null;
|
|
1109
|
-
}
|
|
1110
|
-
}
|