testchimp-runner-core 0.0.34 → 0.0.36

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