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.
Files changed (81) hide show
  1. package/dist/orchestrator/orchestrator-agent.d.ts.map +1 -1
  2. package/dist/orchestrator/orchestrator-agent.js +7 -4
  3. package/dist/orchestrator/orchestrator-agent.js.map +1 -1
  4. package/dist/orchestrator/orchestrator-prompts.d.ts.map +1 -1
  5. package/dist/orchestrator/orchestrator-prompts.js +73 -15
  6. package/dist/orchestrator/orchestrator-prompts.js.map +1 -1
  7. package/dist/orchestrator/page-som-handler.d.ts +1 -2
  8. package/dist/orchestrator/page-som-handler.d.ts.map +1 -1
  9. package/dist/orchestrator/page-som-handler.js +51 -25
  10. package/dist/orchestrator/page-som-handler.js.map +1 -1
  11. package/package.json +6 -1
  12. package/plandocs/BEFORE_AFTER_VERIFICATION.md +0 -148
  13. package/plandocs/COORDINATE_MODE_DIAGNOSIS.md +0 -144
  14. package/plandocs/CREDIT_CALLBACK_ARCHITECTURE.md +0 -253
  15. package/plandocs/HUMAN_LIKE_IMPROVEMENTS.md +0 -642
  16. package/plandocs/IMPLEMENTATION_STATUS.md +0 -108
  17. package/plandocs/INTEGRATION_COMPLETE.md +0 -322
  18. package/plandocs/MULTI_AGENT_ARCHITECTURE_REVIEW.md +0 -844
  19. package/plandocs/ORCHESTRATOR_MVP_SUMMARY.md +0 -539
  20. package/plandocs/PHASE1_ABSTRACTION_COMPLETE.md +0 -241
  21. package/plandocs/PHASE1_FINAL_STATUS.md +0 -210
  22. package/plandocs/PHASE_1_COMPLETE.md +0 -165
  23. package/plandocs/PHASE_1_SUMMARY.md +0 -184
  24. package/plandocs/PLANNING_SESSION_SUMMARY.md +0 -372
  25. package/plandocs/PROMPT_OPTIMIZATION_ANALYSIS.md +0 -120
  26. package/plandocs/PROMPT_SANITY_CHECK.md +0 -120
  27. package/plandocs/SCRIPT_CLEANUP_FEATURE.md +0 -201
  28. package/plandocs/SCRIPT_GENERATION_ARCHITECTURE.md +0 -364
  29. package/plandocs/SELECTOR_IMPROVEMENTS.md +0 -139
  30. package/plandocs/SESSION_SUMMARY_v0.0.33.md +0 -151
  31. package/plandocs/TROUBLESHOOTING_SESSION.md +0 -72
  32. package/plandocs/VISION_DIAGNOSTICS_IMPROVEMENTS.md +0 -336
  33. package/plandocs/VISUAL_AGENT_EVOLUTION_PLAN.md +0 -396
  34. package/plandocs/WHATS_NEW_v0.0.33.md +0 -183
  35. package/plandocs/exploratory-mode-support-v2.plan.md +0 -953
  36. package/plandocs/exploratory-mode-support.plan.md +0 -928
  37. package/plandocs/journey-id-tracking-addendum.md +0 -227
  38. package/releasenotes/RELEASE_0.0.26.md +0 -165
  39. package/releasenotes/RELEASE_0.0.27.md +0 -236
  40. package/releasenotes/RELEASE_0.0.28.md +0 -286
  41. package/src/auth-config.ts +0 -84
  42. package/src/credit-usage-service.ts +0 -188
  43. package/src/env-loader.ts +0 -103
  44. package/src/execution-service.ts +0 -996
  45. package/src/file-handler.ts +0 -104
  46. package/src/index.ts +0 -432
  47. package/src/llm-facade.ts +0 -821
  48. package/src/llm-provider.ts +0 -53
  49. package/src/model-constants.ts +0 -35
  50. package/src/orchestrator/decision-parser.ts +0 -139
  51. package/src/orchestrator/index.ts +0 -58
  52. package/src/orchestrator/orchestrator-agent.ts +0 -1282
  53. package/src/orchestrator/orchestrator-prompts.ts +0 -786
  54. package/src/orchestrator/page-som-handler.ts +0 -1565
  55. package/src/orchestrator/som-types.ts +0 -188
  56. package/src/orchestrator/tool-registry.ts +0 -184
  57. package/src/orchestrator/tools/check-page-ready.ts +0 -75
  58. package/src/orchestrator/tools/extract-data.ts +0 -92
  59. package/src/orchestrator/tools/index.ts +0 -15
  60. package/src/orchestrator/tools/inspect-page.ts +0 -42
  61. package/src/orchestrator/tools/recall-history.ts +0 -72
  62. package/src/orchestrator/tools/refresh-som-markers.ts +0 -69
  63. package/src/orchestrator/tools/take-screenshot.ts +0 -128
  64. package/src/orchestrator/tools/verify-action-result.ts +0 -159
  65. package/src/orchestrator/tools/view-previous-screenshot.ts +0 -103
  66. package/src/orchestrator/types.ts +0 -291
  67. package/src/playwright-mcp-service.ts +0 -224
  68. package/src/progress-reporter.ts +0 -144
  69. package/src/prompts.ts +0 -842
  70. package/src/providers/backend-proxy-llm-provider.ts +0 -91
  71. package/src/providers/local-llm-provider.ts +0 -38
  72. package/src/scenario-service.ts +0 -252
  73. package/src/scenario-worker-class.ts +0 -1110
  74. package/src/script-utils.ts +0 -203
  75. package/src/types.ts +0 -239
  76. package/src/utils/browser-utils.ts +0 -348
  77. package/src/utils/coordinate-converter.ts +0 -162
  78. package/src/utils/page-info-retry.ts +0 -65
  79. package/src/utils/page-info-utils.ts +0 -285
  80. package/testchimp-runner-core-0.0.35.tgz +0 -0
  81. 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
- }