testchimp-runner-core 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth-config.d.ts +33 -0
- package/dist/auth-config.d.ts.map +1 -0
- package/dist/auth-config.js +69 -0
- package/dist/auth-config.js.map +1 -0
- package/dist/env-loader.d.ts +20 -0
- package/dist/env-loader.d.ts.map +1 -0
- package/dist/env-loader.js +83 -0
- package/dist/env-loader.js.map +1 -0
- package/dist/execution-service.d.ts +61 -0
- package/dist/execution-service.d.ts.map +1 -0
- package/dist/execution-service.js +822 -0
- package/dist/execution-service.js.map +1 -0
- package/dist/file-handler.d.ts +59 -0
- package/dist/file-handler.d.ts.map +1 -0
- package/dist/file-handler.js +75 -0
- package/dist/file-handler.js.map +1 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +196 -0
- package/dist/index.js.map +1 -0
- package/dist/llm-facade.d.ts +101 -0
- package/dist/llm-facade.d.ts.map +1 -0
- package/dist/llm-facade.js +289 -0
- package/dist/llm-facade.js.map +1 -0
- package/dist/playwright-mcp-service.d.ts +42 -0
- package/dist/playwright-mcp-service.d.ts.map +1 -0
- package/dist/playwright-mcp-service.js +167 -0
- package/dist/playwright-mcp-service.js.map +1 -0
- package/dist/prompts.d.ts +34 -0
- package/dist/prompts.d.ts.map +1 -0
- package/dist/prompts.js +237 -0
- package/dist/prompts.js.map +1 -0
- package/dist/scenario-service.d.ts +25 -0
- package/dist/scenario-service.d.ts.map +1 -0
- package/dist/scenario-service.js +119 -0
- package/dist/scenario-service.js.map +1 -0
- package/dist/scenario-worker-class.d.ts +30 -0
- package/dist/scenario-worker-class.d.ts.map +1 -0
- package/dist/scenario-worker-class.js +263 -0
- package/dist/scenario-worker-class.js.map +1 -0
- package/dist/script-utils.d.ts +44 -0
- package/dist/script-utils.d.ts.map +1 -0
- package/dist/script-utils.js +100 -0
- package/dist/script-utils.js.map +1 -0
- package/dist/types.d.ts +171 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +28 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/browser-utils.d.ts +13 -0
- package/dist/utils/browser-utils.d.ts.map +1 -0
- package/dist/utils/browser-utils.js +269 -0
- package/dist/utils/browser-utils.js.map +1 -0
- package/dist/utils/page-info-utils.d.ts +16 -0
- package/dist/utils/page-info-utils.d.ts.map +1 -0
- package/dist/utils/page-info-utils.js +77 -0
- package/dist/utils/page-info-utils.js.map +1 -0
- package/env.prod +1 -0
- package/env.staging +1 -0
- package/package.json +38 -0
- package/src/auth-config.ts +84 -0
- package/src/env-loader.ts +91 -0
- package/src/execution-service.ts +999 -0
- package/src/file-handler.ts +104 -0
- package/src/index.ts +205 -0
- package/src/llm-facade.ts +413 -0
- package/src/playwright-mcp-service.ts +203 -0
- package/src/prompts.ts +247 -0
- package/src/scenario-service.ts +138 -0
- package/src/scenario-worker-class.ts +330 -0
- package/src/script-utils.ts +109 -0
- package/src/types.ts +202 -0
- package/src/utils/browser-utils.ts +272 -0
- package/src/utils/page-info-utils.ts +93 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,999 @@
|
|
|
1
|
+
import { PlaywrightMCPService as PlaywrightService } from './playwright-mcp-service';
|
|
2
|
+
import {
|
|
3
|
+
PlaywrightExecutionRequest,
|
|
4
|
+
PlaywrightExecutionResponse,
|
|
5
|
+
ScriptResult,
|
|
6
|
+
ScriptExecutionRequest,
|
|
7
|
+
ScriptExecutionResponse,
|
|
8
|
+
ScriptStep,
|
|
9
|
+
ExecutionMode,
|
|
10
|
+
StepOperation,
|
|
11
|
+
StepRepairAction
|
|
12
|
+
} from './types';
|
|
13
|
+
import { RepairSuggestionResponse, RepairConfidenceResponse } from './llm-facade';
|
|
14
|
+
import { Browser, BrowserContext, Page } from 'playwright';
|
|
15
|
+
import { expect } from '@playwright/test';
|
|
16
|
+
import { getEnhancedPageInfo, PageInfo } from './utils/page-info-utils';
|
|
17
|
+
import { initializeBrowser } from './utils/browser-utils';
|
|
18
|
+
import { LLMFacade } from './llm-facade';
|
|
19
|
+
import { AuthConfig } from './auth-config';
|
|
20
|
+
import { addTestChimpComment } from './script-utils';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Service for orchestrating Playwright script execution
|
|
24
|
+
*/
|
|
25
|
+
export class ExecutionService {
|
|
26
|
+
private playwrightService: PlaywrightService;
|
|
27
|
+
private llmFacade: LLMFacade;
|
|
28
|
+
|
|
29
|
+
constructor(authConfig?: AuthConfig) {
|
|
30
|
+
this.playwrightService = new PlaywrightService();
|
|
31
|
+
this.llmFacade = new LLMFacade(authConfig);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Initialize the execution service
|
|
36
|
+
*/
|
|
37
|
+
async initialize(): Promise<void> {
|
|
38
|
+
await this.playwrightService.initialize();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Execute a script with optional AI repair capabilities
|
|
44
|
+
*/
|
|
45
|
+
async executeScript(request: ScriptExecutionRequest): Promise<ScriptExecutionResponse> {
|
|
46
|
+
const startTime = Date.now();
|
|
47
|
+
const model = request.model || 'gpt-4.1-mini';
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
if (request.mode === ExecutionMode.RUN_EXACTLY) {
|
|
51
|
+
return await this.runExactly(request, startTime);
|
|
52
|
+
} else {
|
|
53
|
+
return await this.runWithAIRepair(request, startTime, model);
|
|
54
|
+
}
|
|
55
|
+
} catch (error) {
|
|
56
|
+
return {
|
|
57
|
+
run_status: 'failed',
|
|
58
|
+
executionTime: Date.now() - startTime,
|
|
59
|
+
error: error instanceof Error ? error.message : 'Unknown error'
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Execute a complete Playwright test suite as a single job
|
|
66
|
+
*/
|
|
67
|
+
async executeTestSuite(request: PlaywrightExecutionRequest): Promise<PlaywrightExecutionResponse> {
|
|
68
|
+
try {
|
|
69
|
+
// Parse Playwright configuration
|
|
70
|
+
const config = this.parsePlaywrightConfig(request.playwrightConfig);
|
|
71
|
+
|
|
72
|
+
// Execute the entire job (prescript + script + postscript) as one unit
|
|
73
|
+
const jobResult = await this.playwrightService.executeJob(
|
|
74
|
+
request.prescript,
|
|
75
|
+
request.script,
|
|
76
|
+
request.postscript,
|
|
77
|
+
config
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
success: jobResult.success,
|
|
82
|
+
results: jobResult.results,
|
|
83
|
+
executionTime: jobResult.executionTime,
|
|
84
|
+
error: jobResult.error
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
} catch (error) {
|
|
88
|
+
return {
|
|
89
|
+
success: false,
|
|
90
|
+
results: {
|
|
91
|
+
script: { success: false, output: '', error: '', executionTime: 0 }
|
|
92
|
+
},
|
|
93
|
+
executionTime: 0,
|
|
94
|
+
error: error instanceof Error ? error.message : 'Unknown error occurred'
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Parse Playwright configuration from string
|
|
101
|
+
*/
|
|
102
|
+
private parsePlaywrightConfig(configString: string): any {
|
|
103
|
+
try {
|
|
104
|
+
// Try to parse as JSON first
|
|
105
|
+
const config = JSON.parse(configString);
|
|
106
|
+
return {
|
|
107
|
+
browserType: config.browserType || 'chromium',
|
|
108
|
+
headless: config.headless === true,
|
|
109
|
+
viewport: config.viewport || { width: 1280, height: 720 },
|
|
110
|
+
options: config.options || {}
|
|
111
|
+
};
|
|
112
|
+
} catch {
|
|
113
|
+
// If not JSON, try to extract basic config from JavaScript
|
|
114
|
+
try {
|
|
115
|
+
// Simple regex-based extraction for common config patterns
|
|
116
|
+
const headlessMatch = configString.match(/headless:\s*(true|false)/);
|
|
117
|
+
const viewportMatch = configString.match(/viewport:\s*\{\s*width:\s*(\d+),\s*height:\s*(\d+)\s*\}/);
|
|
118
|
+
const browserMatch = configString.match(/browserType:\s*['"`](chromium|firefox|webkit)['"`]/);
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
browserType: browserMatch ? browserMatch[1] : 'chromium',
|
|
122
|
+
headless: headlessMatch ? headlessMatch[1] === 'true' : true,
|
|
123
|
+
viewport: viewportMatch ?
|
|
124
|
+
{ width: parseInt(viewportMatch[1]), height: parseInt(viewportMatch[2]) } :
|
|
125
|
+
{ width: 1280, height: 720 },
|
|
126
|
+
options: {}
|
|
127
|
+
};
|
|
128
|
+
} catch {
|
|
129
|
+
// Return default config if parsing fails
|
|
130
|
+
return {
|
|
131
|
+
browserType: 'chromium',
|
|
132
|
+
headless: false,
|
|
133
|
+
viewport: { width: 1280, height: 720 },
|
|
134
|
+
options: {}
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Close the execution service
|
|
142
|
+
*/
|
|
143
|
+
async close(): Promise<void> {
|
|
144
|
+
await this.playwrightService.close();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Check if the service is ready
|
|
149
|
+
*/
|
|
150
|
+
isReady(): boolean {
|
|
151
|
+
return this.playwrightService.isReady();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private async runExactly(request: ScriptExecutionRequest, startTime: number, model?: string): Promise<ScriptExecutionResponse> {
|
|
155
|
+
const deflakeRunCount = request.deflake_run_count !== undefined ? request.deflake_run_count : 1;
|
|
156
|
+
const totalAttempts = deflakeRunCount + 1; // Original run + deflake attempts
|
|
157
|
+
let lastError: Error | null = null;
|
|
158
|
+
|
|
159
|
+
console.log(`runExactly: deflake_run_count = ${request.deflake_run_count}, totalAttempts = ${totalAttempts}`);
|
|
160
|
+
|
|
161
|
+
// Script content should be provided by the caller (TestChimpService)
|
|
162
|
+
// The TestChimpService handles file reading through the appropriate FileHandler
|
|
163
|
+
if (!request.script) {
|
|
164
|
+
throw new Error('Script content is required for execution. The TestChimpService should read the file and provide script content.');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
for (let attempt = 1; attempt <= totalAttempts; attempt++) {
|
|
168
|
+
console.log(`Attempting deflake run ${attempt}/${totalAttempts}`);
|
|
169
|
+
const { browser, context, page } = await this.initializeBrowser(request.playwrightConfig, request.headless, request.playwrightConfigFilePath);
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
// Execute the script as-is
|
|
173
|
+
await this.executeScriptContent(request.script, page);
|
|
174
|
+
|
|
175
|
+
await browser.close();
|
|
176
|
+
|
|
177
|
+
// Success! Return immediately
|
|
178
|
+
return {
|
|
179
|
+
run_status: 'success',
|
|
180
|
+
num_deflake_runs: attempt - 1, // Count only deflaking runs (exclude original run)
|
|
181
|
+
executionTime: Date.now() - startTime
|
|
182
|
+
};
|
|
183
|
+
} catch (error) {
|
|
184
|
+
lastError = error instanceof Error ? error : new Error('Script execution failed');
|
|
185
|
+
console.log(`Initial run failed: ${lastError.message}`);
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
await browser.close();
|
|
189
|
+
} catch (closeError) {
|
|
190
|
+
// Browser might already be closed
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// If this is not the last attempt, continue to next attempt
|
|
194
|
+
if (attempt < totalAttempts) {
|
|
195
|
+
console.log(`Deflaking attempt ${attempt} failed, trying again... (${attempt + 1}/${totalAttempts})`);
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// All attempts failed
|
|
202
|
+
return {
|
|
203
|
+
run_status: 'failed',
|
|
204
|
+
num_deflake_runs: deflakeRunCount, // Count only deflaking runs (exclude original run)
|
|
205
|
+
executionTime: Date.now() - startTime,
|
|
206
|
+
error: lastError?.message || 'All deflaking attempts failed'
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
private async runWithAIRepair(request: ScriptExecutionRequest, startTime: number, model: string): Promise<ScriptExecutionResponse> {
|
|
211
|
+
const repairFlexibility = request.repair_flexibility || 3;
|
|
212
|
+
|
|
213
|
+
// Script content should be provided by the caller (TestChimpService)
|
|
214
|
+
// The TestChimpService handles file reading through the appropriate FileHandler
|
|
215
|
+
if (!request.script) {
|
|
216
|
+
throw new Error('Script content is required for AI repair. The TestChimpService should read the file and provide script content.');
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// First, try runExactly (which includes deflaking if configured)
|
|
220
|
+
console.log('Attempting runExactly first (with deflaking if configured)...');
|
|
221
|
+
const runExactlyResult = await this.runExactly(request, startTime, model);
|
|
222
|
+
|
|
223
|
+
// If runExactly succeeded, return that result
|
|
224
|
+
if (runExactlyResult.run_status === 'success') {
|
|
225
|
+
return runExactlyResult;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// runExactly failed, start AI repair
|
|
229
|
+
console.log('runExactly failed, starting AI repair process...');
|
|
230
|
+
|
|
231
|
+
try {
|
|
232
|
+
|
|
233
|
+
// Start browser initialization and script parsing in parallel for faster startup
|
|
234
|
+
console.log('Initializing repair browser and parsing script...');
|
|
235
|
+
const [steps, { browser: repairBrowser, context: repairContext, page: repairPage }] = await Promise.all([
|
|
236
|
+
this.parseScriptIntoSteps(request.script, model),
|
|
237
|
+
this.initializeBrowser(request.playwrightConfig, request.headless, request.playwrightConfigFilePath) // Use request.headless (defaults to false/headed)
|
|
238
|
+
]);
|
|
239
|
+
|
|
240
|
+
console.log('Starting AI repair with parsed steps...');
|
|
241
|
+
const updatedSteps = await this.repairStepsWithAI(steps, repairPage, repairFlexibility, model);
|
|
242
|
+
|
|
243
|
+
// Always generate the updated script
|
|
244
|
+
const updatedScript = this.generateUpdatedScript(updatedSteps);
|
|
245
|
+
|
|
246
|
+
// Check if repair was successful by seeing if we completed all steps
|
|
247
|
+
const allStepsSuccessful = updatedSteps.length > 0 && updatedSteps.every(step => step.success);
|
|
248
|
+
|
|
249
|
+
// Check if we have any successful repairs (partial success)
|
|
250
|
+
const hasSuccessfulRepairs = updatedSteps.some(step => step.success);
|
|
251
|
+
|
|
252
|
+
// Debug: Log step success status
|
|
253
|
+
console.log('Step success status:', updatedSteps.map((step, index) => `Step ${index + 1}: ${step.success ? 'SUCCESS' : 'FAILED'}`));
|
|
254
|
+
console.log('All steps successful:', allStepsSuccessful);
|
|
255
|
+
console.log('Has successful repairs:', hasSuccessfulRepairs);
|
|
256
|
+
|
|
257
|
+
// Debug: Log individual step details
|
|
258
|
+
updatedSteps.forEach((step, index) => {
|
|
259
|
+
console.log(`Step ${index + 1} details: success=${step.success}, description="${step.description}"`);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
// Update file if we have any successful repairs (partial or complete)
|
|
263
|
+
if (hasSuccessfulRepairs) {
|
|
264
|
+
const confidenceResponse = await this.llmFacade.assessRepairConfidence(request.script!, updatedScript, model);
|
|
265
|
+
const finalScript = await this.llmFacade.generateFinalScript(request.script!, updatedScript, confidenceResponse.advice, model);
|
|
266
|
+
|
|
267
|
+
// Ensure the final script has the correct TestChimp comment format with repair advice
|
|
268
|
+
const scriptWithRepairAdvice = addTestChimpComment(finalScript, confidenceResponse.advice);
|
|
269
|
+
|
|
270
|
+
await repairBrowser.close();
|
|
271
|
+
|
|
272
|
+
return {
|
|
273
|
+
run_status: 'failed', // Original script failed
|
|
274
|
+
repair_status: allStepsSuccessful ? 'success' : 'partial', // Complete or partial repair success
|
|
275
|
+
repair_confidence: confidenceResponse.confidence,
|
|
276
|
+
repair_advice: confidenceResponse.advice,
|
|
277
|
+
updated_script: scriptWithRepairAdvice, // Return the drop-in replacement script with proper TestChimp comment
|
|
278
|
+
num_deflake_runs: runExactlyResult.num_deflake_runs, // All deflaking attempts failed
|
|
279
|
+
executionTime: Date.now() - startTime
|
|
280
|
+
};
|
|
281
|
+
} else {
|
|
282
|
+
// No successful repairs at all
|
|
283
|
+
await repairBrowser.close();
|
|
284
|
+
|
|
285
|
+
return {
|
|
286
|
+
run_status: 'failed', // Original script failed
|
|
287
|
+
repair_status: 'failed',
|
|
288
|
+
repair_confidence: 0,
|
|
289
|
+
repair_advice: 'AI repair could not fix any steps',
|
|
290
|
+
updated_script: request.script!, // Return original script since no repairs were successful
|
|
291
|
+
num_deflake_runs: runExactlyResult.num_deflake_runs, // All deflaking attempts failed
|
|
292
|
+
executionTime: Date.now() - startTime,
|
|
293
|
+
error: 'AI repair could not fix any steps'
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
} catch (error) {
|
|
297
|
+
return {
|
|
298
|
+
run_status: 'failed',
|
|
299
|
+
repair_status: 'failed',
|
|
300
|
+
num_deflake_runs: runExactlyResult.num_deflake_runs,
|
|
301
|
+
executionTime: Date.now() - startTime,
|
|
302
|
+
error: error instanceof Error ? error.message : 'Script execution failed'
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
private async parseScriptIntoSteps(script: string, model: string): Promise<(ScriptStep & { success?: boolean; error?: string })[]> {
|
|
308
|
+
// First try LLM-based parsing
|
|
309
|
+
try {
|
|
310
|
+
console.log('Attempting LLM-based script parsing...');
|
|
311
|
+
const result = await this.llmFacade.parseScriptIntoSteps(script, model);
|
|
312
|
+
console.log('LLM parsing successful, got', result.length, 'steps');
|
|
313
|
+
return result;
|
|
314
|
+
} catch (error) {
|
|
315
|
+
console.log('LLM parsing failed, falling back to code parsing:', error);
|
|
316
|
+
const fallbackResult = this.parseScriptIntoStepsFallback(script);
|
|
317
|
+
console.log('Fallback parsing successful, got', fallbackResult.length, 'steps');
|
|
318
|
+
return fallbackResult;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
private parseScriptIntoStepsFallback(script: string): (ScriptStep & { success?: boolean; error?: string })[] {
|
|
324
|
+
const lines = script.split('\n');
|
|
325
|
+
const steps: (ScriptStep & { success?: boolean; error?: string })[] = [];
|
|
326
|
+
let currentStep: ScriptStep | null = null;
|
|
327
|
+
let currentCode: string[] = [];
|
|
328
|
+
|
|
329
|
+
for (const line of lines) {
|
|
330
|
+
const trimmedLine = line.trim();
|
|
331
|
+
|
|
332
|
+
// Check for step comment
|
|
333
|
+
if (trimmedLine.startsWith('// Step ')) {
|
|
334
|
+
// Save previous step if exists and has code
|
|
335
|
+
if (currentStep) {
|
|
336
|
+
const code = currentCode.join('\n').trim();
|
|
337
|
+
const cleanedCode = this.cleanStepCode(code);
|
|
338
|
+
if (cleanedCode) {
|
|
339
|
+
currentStep.code = cleanedCode;
|
|
340
|
+
steps.push(currentStep);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Start new step
|
|
345
|
+
const description = trimmedLine.replace(/^\/\/\s*Step\s*\d+:\s*/, '').replace(/\s*\[FAILED\]\s*$/, '').trim();
|
|
346
|
+
currentStep = { description, code: '' };
|
|
347
|
+
currentCode = [];
|
|
348
|
+
} else if (trimmedLine && !trimmedLine.startsWith('import') && !trimmedLine.startsWith('test(') && !trimmedLine.startsWith('});')) {
|
|
349
|
+
// Add code line to current step
|
|
350
|
+
if (currentStep) {
|
|
351
|
+
currentCode.push(line);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Add the last step if it has code
|
|
357
|
+
if (currentStep) {
|
|
358
|
+
const code = currentCode.join('\n').trim();
|
|
359
|
+
const cleanedCode = this.cleanStepCode(code);
|
|
360
|
+
if (cleanedCode) {
|
|
361
|
+
currentStep.code = cleanedCode;
|
|
362
|
+
steps.push(currentStep);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return steps;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
private async repairStepsWithAI(
|
|
370
|
+
steps: (ScriptStep & { success?: boolean; error?: string })[],
|
|
371
|
+
page: Page,
|
|
372
|
+
repairFlexibility: number,
|
|
373
|
+
model: string
|
|
374
|
+
): Promise<(ScriptStep & { success?: boolean; error?: string })[]> {
|
|
375
|
+
let updatedSteps = [...steps];
|
|
376
|
+
const maxTries = 3;
|
|
377
|
+
const recentRepairs: Array<{
|
|
378
|
+
stepNumber: number;
|
|
379
|
+
operation: string;
|
|
380
|
+
originalDescription?: string;
|
|
381
|
+
newDescription?: string;
|
|
382
|
+
originalCode?: string;
|
|
383
|
+
newCode?: string;
|
|
384
|
+
}> = [];
|
|
385
|
+
|
|
386
|
+
// Create a shared execution context that accumulates all executed code for variable tracking
|
|
387
|
+
let executionContext = '';
|
|
388
|
+
const contextVariables = new Map<string, any>();
|
|
389
|
+
|
|
390
|
+
let i = 0;
|
|
391
|
+
while (i < updatedSteps.length) {
|
|
392
|
+
const step = updatedSteps[i];
|
|
393
|
+
console.log(`Loop iteration: i=${i}, step description="${step.description}", total steps=${updatedSteps.length}`);
|
|
394
|
+
|
|
395
|
+
try {
|
|
396
|
+
// Try to execute the step directly without context replay
|
|
397
|
+
console.log(`Attempting Step ${i + 1}: ${step.description}`);
|
|
398
|
+
console.log(` Code: ${step.code}`);
|
|
399
|
+
await this.executeStepCode(step.code, page);
|
|
400
|
+
step.success = true;
|
|
401
|
+
console.log(`Step ${i + 1} executed successfully: ${step.description}`);
|
|
402
|
+
console.log(`Step ${i + 1} success status set to: ${step.success}`);
|
|
403
|
+
|
|
404
|
+
// Add this step's code to the execution context for future steps (for variable tracking)
|
|
405
|
+
executionContext += step.code + '\n';
|
|
406
|
+
i++; // Move to next step
|
|
407
|
+
} catch (error) {
|
|
408
|
+
console.log(`Step ${i + 1} failed: ${step.description}`);
|
|
409
|
+
console.log(` Failed code: ${step.code}`);
|
|
410
|
+
console.log(` Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
411
|
+
if (error instanceof Error && error.stack) {
|
|
412
|
+
console.log(` Stack trace: ${error.stack}`);
|
|
413
|
+
}
|
|
414
|
+
step.success = false;
|
|
415
|
+
step.error = this.safeSerializeError(error);
|
|
416
|
+
|
|
417
|
+
// Try multiple repair attempts
|
|
418
|
+
const repairHistory: Array<{
|
|
419
|
+
attempt: number;
|
|
420
|
+
action: StepRepairAction;
|
|
421
|
+
error: string;
|
|
422
|
+
pageInfo: PageInfo;
|
|
423
|
+
}> = [];
|
|
424
|
+
|
|
425
|
+
let repairSuccess = false;
|
|
426
|
+
const originalDescription = step.description;
|
|
427
|
+
const originalCode = step.code;
|
|
428
|
+
|
|
429
|
+
for (let attempt = 1; attempt <= maxTries; attempt++) {
|
|
430
|
+
console.log(`Step ${i + 1} repair attempt ${attempt}/${maxTries}`);
|
|
431
|
+
|
|
432
|
+
// Get current page state for AI repair
|
|
433
|
+
const pageInfo = await this.getEnhancedPageInfo(page);
|
|
434
|
+
|
|
435
|
+
// Build failure history for LLM context
|
|
436
|
+
const failureHistory = this.buildFailureHistory(repairHistory, step, error);
|
|
437
|
+
|
|
438
|
+
// Build recent repairs context for LLM
|
|
439
|
+
const recentRepairsContext = this.buildRecentRepairsContext(recentRepairs);
|
|
440
|
+
|
|
441
|
+
// Ask AI for repair suggestion with failure history and recent repairs
|
|
442
|
+
const repairSuggestion = await this.llmFacade.getRepairSuggestion(
|
|
443
|
+
step.description,
|
|
444
|
+
step.code,
|
|
445
|
+
step.error || 'Unknown error',
|
|
446
|
+
pageInfo,
|
|
447
|
+
failureHistory,
|
|
448
|
+
recentRepairsContext,
|
|
449
|
+
model
|
|
450
|
+
);
|
|
451
|
+
|
|
452
|
+
if (!repairSuggestion.shouldContinue) {
|
|
453
|
+
console.log(`AI decided to stop repair at attempt ${attempt}: ${repairSuggestion.reason}`);
|
|
454
|
+
break;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Apply the repair action
|
|
458
|
+
try {
|
|
459
|
+
// Set the step index and insertAfterIndex on the client side based on current step being processed
|
|
460
|
+
const repairAction = {
|
|
461
|
+
...repairSuggestion.action,
|
|
462
|
+
stepIndex: i, // Client-side step index management
|
|
463
|
+
insertAfterIndex: repairSuggestion.action.operation === StepOperation.INSERT ? i - 1 : undefined // For INSERT, insert before current step
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
console.log(`🔧 Applying repair action:`, {
|
|
467
|
+
operation: repairAction.operation,
|
|
468
|
+
stepIndex: repairAction.stepIndex,
|
|
469
|
+
insertAfterIndex: repairAction.insertAfterIndex,
|
|
470
|
+
newStepDescription: repairAction.newStep?.description,
|
|
471
|
+
newStepCode: repairAction.newStep?.code
|
|
472
|
+
});
|
|
473
|
+
console.log(`🔧 Steps array before repair:`, updatedSteps.map((s, idx) => `${idx}: "${s.description}" (success: ${s.success})`));
|
|
474
|
+
|
|
475
|
+
const result = await this.applyRepairActionInContext(repairAction, updatedSteps, i, page, executionContext, contextVariables);
|
|
476
|
+
|
|
477
|
+
if (result.success) {
|
|
478
|
+
repairSuccess = true;
|
|
479
|
+
console.log(`🔧 Steps array after repair:`, updatedSteps.map((s, idx) => `${idx}: "${s.description}" (success: ${s.success})`));
|
|
480
|
+
|
|
481
|
+
// Mark the appropriate step(s) as successful based on operation type
|
|
482
|
+
if (repairAction.operation === StepOperation.MODIFY) {
|
|
483
|
+
// For MODIFY: mark the modified step as successful
|
|
484
|
+
step.success = true;
|
|
485
|
+
step.error = undefined;
|
|
486
|
+
updatedSteps[i].success = true;
|
|
487
|
+
updatedSteps[i].error = undefined;
|
|
488
|
+
console.log(`Step ${i + 1} marked as successful after MODIFY repair`);
|
|
489
|
+
} else if (repairAction.operation === StepOperation.INSERT) {
|
|
490
|
+
// For INSERT: mark the newly inserted step as successful
|
|
491
|
+
const insertIndex = repairAction.insertAfterIndex !== undefined ? repairAction.insertAfterIndex + 1 : i + 1;
|
|
492
|
+
if (updatedSteps[insertIndex]) {
|
|
493
|
+
updatedSteps[insertIndex].success = true;
|
|
494
|
+
updatedSteps[insertIndex].error = undefined;
|
|
495
|
+
}
|
|
496
|
+
} else if (repairAction.operation === StepOperation.REMOVE) {
|
|
497
|
+
// For REMOVE: no step to mark as successful since we removed it
|
|
498
|
+
// The step is already removed from the array
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const commandInfo = repairAction.operation === StepOperation.MODIFY ?
|
|
502
|
+
`MODIFY: "${repairAction.newStep?.code || 'N/A'}"` :
|
|
503
|
+
repairAction.operation === StepOperation.INSERT ?
|
|
504
|
+
`INSERT: "${repairAction.newStep?.code || 'N/A'}"` :
|
|
505
|
+
repairAction.operation === StepOperation.REMOVE ?
|
|
506
|
+
`REMOVE: step at index ${repairAction.stepIndex}` :
|
|
507
|
+
repairAction.operation;
|
|
508
|
+
console.log(`Step ${i + 1} repair action ${commandInfo} executed successfully on attempt ${attempt}`);
|
|
509
|
+
|
|
510
|
+
// Update execution context based on the repair action
|
|
511
|
+
if (repairAction.operation === StepOperation.MODIFY && repairAction.newStep) {
|
|
512
|
+
// Update the step in the execution context for variable tracking
|
|
513
|
+
executionContext = executionContext.replace(originalCode, repairAction.newStep.code);
|
|
514
|
+
} else if (repairAction.operation === StepOperation.INSERT && repairAction.newStep) {
|
|
515
|
+
// Insert the new step code into execution context for variable tracking
|
|
516
|
+
executionContext += repairAction.newStep.code + '\n';
|
|
517
|
+
} else if (repairAction.operation === StepOperation.REMOVE) {
|
|
518
|
+
// Remove the step code from execution context for variable tracking
|
|
519
|
+
executionContext = executionContext.replace(originalCode, '');
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Record this successful repair
|
|
523
|
+
recentRepairs.push({
|
|
524
|
+
stepNumber: i + 1,
|
|
525
|
+
operation: repairAction.operation,
|
|
526
|
+
originalDescription: repairAction.operation === StepOperation.REMOVE ? originalDescription : undefined,
|
|
527
|
+
newDescription: repairAction.newStep?.description,
|
|
528
|
+
originalCode: repairAction.operation === StepOperation.REMOVE ? originalCode : undefined,
|
|
529
|
+
newCode: repairAction.newStep?.code
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
// Keep only the last 3 repairs for context
|
|
533
|
+
if (recentRepairs.length > 3) {
|
|
534
|
+
recentRepairs.shift();
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// Update step index based on operation
|
|
538
|
+
if (repairAction.operation === StepOperation.INSERT) {
|
|
539
|
+
// For INSERT: inserted step is already executed
|
|
540
|
+
console.log(`INSERT operation: current i=${i}, insertAfterIndex=${repairAction.insertAfterIndex}`);
|
|
541
|
+
console.log(`INSERT: Steps array length before: ${updatedSteps.length}`);
|
|
542
|
+
console.log(`INSERT: Steps before operation:`, updatedSteps.map((s, idx) => `${idx}: "${s.description}" (success: ${s.success})`));
|
|
543
|
+
|
|
544
|
+
if (repairAction.insertAfterIndex !== undefined && repairAction.insertAfterIndex < i) {
|
|
545
|
+
// If inserting before current position, current step moved down by 1
|
|
546
|
+
console.log(`INSERT before current position: incrementing i from ${i} to ${i + 1}`);
|
|
547
|
+
i++; // Move to the original step that was pushed to the next position
|
|
548
|
+
} else {
|
|
549
|
+
// If inserting at or after current position, stay at current step
|
|
550
|
+
console.log(`INSERT at/after current position: keeping i at ${i}`);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
console.log(`INSERT: Steps array length after: ${updatedSteps.length}`);
|
|
554
|
+
console.log(`INSERT: Steps after operation:`, updatedSteps.map((s, idx) => `${idx}: "${s.description}" (success: ${s.success})`));
|
|
555
|
+
} else if (repairAction.operation === StepOperation.REMOVE) {
|
|
556
|
+
// For REMOVE: stay at same index since the next step moved to current position
|
|
557
|
+
// Don't increment i because the array shifted left
|
|
558
|
+
} else {
|
|
559
|
+
// For MODIFY: move to next step since modified step was executed
|
|
560
|
+
i++; // Move to next step for MODIFY
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// Add the repaired step's code to execution context for variable tracking
|
|
564
|
+
executionContext += step.code + '\n';
|
|
565
|
+
|
|
566
|
+
break;
|
|
567
|
+
} else {
|
|
568
|
+
throw new Error(result.error || 'Repair action failed');
|
|
569
|
+
}
|
|
570
|
+
} catch (repairError) {
|
|
571
|
+
const repairErrorMessage = repairError instanceof Error ? repairError.message : 'Repair failed';
|
|
572
|
+
const commandInfo = repairSuggestion.action.operation === StepOperation.MODIFY ?
|
|
573
|
+
`MODIFY: "${repairSuggestion.action.newStep?.code || 'N/A'}"` :
|
|
574
|
+
repairSuggestion.action.operation === StepOperation.INSERT ?
|
|
575
|
+
`INSERT: "${repairSuggestion.action.newStep?.code || 'N/A'}"` :
|
|
576
|
+
repairSuggestion.action.operation === StepOperation.REMOVE ?
|
|
577
|
+
`REMOVE: step at index ${repairSuggestion.action.stepIndex}` :
|
|
578
|
+
repairSuggestion.action.operation;
|
|
579
|
+
console.log(`Step ${i + 1} repair attempt ${attempt} failed (${commandInfo}): ${repairErrorMessage}`);
|
|
580
|
+
if (repairError instanceof Error && repairError.stack) {
|
|
581
|
+
console.log(` Repair stack trace: ${repairError.stack}`);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// Record this attempt in history
|
|
585
|
+
repairHistory.push({
|
|
586
|
+
attempt,
|
|
587
|
+
action: repairSuggestion.action,
|
|
588
|
+
error: repairErrorMessage,
|
|
589
|
+
pageInfo
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
step.error = repairErrorMessage;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
if (!repairSuccess) {
|
|
597
|
+
console.log(`Step ${i + 1} failed after ${maxTries} repair attempts`);
|
|
598
|
+
break;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
return updatedSteps;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
private async executeStepCode(code: string, page: Page): Promise<void> {
|
|
607
|
+
// Set timeout for individual step execution
|
|
608
|
+
page.setDefaultTimeout(5000); // 5 seconds for individual commands
|
|
609
|
+
|
|
610
|
+
try {
|
|
611
|
+
// Clean and validate the code before execution
|
|
612
|
+
const cleanedCode = this.cleanStepCode(code);
|
|
613
|
+
|
|
614
|
+
if (!cleanedCode || cleanedCode.trim().length === 0) {
|
|
615
|
+
throw new Error('Step code is empty or contains only comments');
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// Create an async function that has access to page, expect, and other Playwright globals
|
|
619
|
+
const executeCode = new Function('page', 'expect', `return (async () => { ${cleanedCode} })()`);
|
|
620
|
+
const result = executeCode(page, expect);
|
|
621
|
+
await result;
|
|
622
|
+
} finally {
|
|
623
|
+
// Restore to reasonable default timeout
|
|
624
|
+
page.setDefaultTimeout(10000);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* Validate step code has executable content (preserves comments)
|
|
630
|
+
*/
|
|
631
|
+
private cleanStepCode(code: string): string {
|
|
632
|
+
if (!code || code.trim().length === 0) {
|
|
633
|
+
return '';
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// Check if there are any executable statements (including those with comments)
|
|
637
|
+
const hasExecutableCode = /[a-zA-Z_$][a-zA-Z0-9_$]*\s*\(|await\s+|return\s+|if\s*\(|for\s*\(|while\s*\(|switch\s*\(|try\s*\{|catch\s*\(/.test(code);
|
|
638
|
+
|
|
639
|
+
if (!hasExecutableCode) {
|
|
640
|
+
return '';
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
return code; // Return the original code without removing comments
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
private async executeStepInContext(
|
|
647
|
+
code: string,
|
|
648
|
+
page: Page,
|
|
649
|
+
executionContext: string,
|
|
650
|
+
contextVariables: Map<string, any>
|
|
651
|
+
): Promise<void> {
|
|
652
|
+
// Set timeout for individual step execution
|
|
653
|
+
page.setDefaultTimeout(5000); // 5 seconds for individual commands
|
|
654
|
+
|
|
655
|
+
try {
|
|
656
|
+
// Execute only the current step code, but make context variables available
|
|
657
|
+
const fullCode = code;
|
|
658
|
+
|
|
659
|
+
// Create a function that has access to page, expect, and the context variables
|
|
660
|
+
const executeCode = new Function(
|
|
661
|
+
'page',
|
|
662
|
+
'expect',
|
|
663
|
+
'contextVariables',
|
|
664
|
+
`return (async () => {
|
|
665
|
+
// Make context variables available in the execution scope
|
|
666
|
+
for (const [key, value] of contextVariables) {
|
|
667
|
+
globalThis[key] = value;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
${fullCode}
|
|
671
|
+
|
|
672
|
+
// Capture any new variables that might have been created
|
|
673
|
+
const newVars = {};
|
|
674
|
+
for (const key in globalThis) {
|
|
675
|
+
if (!contextVariables.has(key) && typeof globalThis[key] !== 'function' && key !== 'page' && key !== 'expect') {
|
|
676
|
+
newVars[key] = globalThis[key];
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
return newVars;
|
|
680
|
+
})()`
|
|
681
|
+
);
|
|
682
|
+
|
|
683
|
+
const newVars = await executeCode(page, expect, contextVariables);
|
|
684
|
+
|
|
685
|
+
// Update the context variables with any new variables created
|
|
686
|
+
for (const [key, value] of Object.entries(newVars)) {
|
|
687
|
+
contextVariables.set(key, value);
|
|
688
|
+
}
|
|
689
|
+
} finally {
|
|
690
|
+
// Restore to reasonable default timeout
|
|
691
|
+
page.setDefaultTimeout(5000);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
private async executeScriptContent(script: string, page: Page): Promise<void> {
|
|
696
|
+
// Extract the test function content
|
|
697
|
+
const testMatch = script.match(/test\([^,]+,\s*async\s*\(\s*\{\s*page[^}]*\}\s*\)\s*=>\s*\{([\s\S]*)\}\s*\);/);
|
|
698
|
+
if (!testMatch) {
|
|
699
|
+
throw new Error('Could not extract test function from script');
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
const testBody = testMatch[1];
|
|
703
|
+
// Execute the entire test body as one async function
|
|
704
|
+
const executeTest = new Function('page', 'expect', `return (async () => { ${testBody} })()`);
|
|
705
|
+
await executeTest(page, expect);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
private async getEnhancedPageInfo(page: Page): Promise<PageInfo> {
|
|
709
|
+
try {
|
|
710
|
+
return await getEnhancedPageInfo(page);
|
|
711
|
+
} catch (error) {
|
|
712
|
+
return {
|
|
713
|
+
url: page.url(),
|
|
714
|
+
title: 'Unknown',
|
|
715
|
+
elements: 'Unable to extract',
|
|
716
|
+
formFields: 'Unable to extract',
|
|
717
|
+
interactiveElements: 'Unable to extract',
|
|
718
|
+
pageStructure: 'Unable to extract'
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
private buildFailureHistory(
|
|
724
|
+
repairHistory: Array<{ attempt: number; action: StepRepairAction; error: string; pageInfo: PageInfo }>,
|
|
725
|
+
originalStep: ScriptStep,
|
|
726
|
+
originalError: any
|
|
727
|
+
): string {
|
|
728
|
+
if (repairHistory.length === 0) {
|
|
729
|
+
return `Original failure: ${this.safeSerializeError(originalError)}`;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
let history = `Original failure: ${this.safeSerializeError(originalError)}\n\n`;
|
|
733
|
+
history += `Previous repair attempts:\n`;
|
|
734
|
+
|
|
735
|
+
repairHistory.forEach((attempt, index) => {
|
|
736
|
+
history += `Attempt ${attempt.attempt}:\n`;
|
|
737
|
+
history += ` Operation: ${attempt.action.operation}\n`;
|
|
738
|
+
if (attempt.action.newStep) {
|
|
739
|
+
history += ` Description: ${attempt.action.newStep.description}\n`;
|
|
740
|
+
history += ` Code: ${attempt.action.newStep.code}\n`;
|
|
741
|
+
}
|
|
742
|
+
history += ` Error: ${attempt.error}\n`;
|
|
743
|
+
if (index < repairHistory.length - 1) {
|
|
744
|
+
history += `\n`;
|
|
745
|
+
}
|
|
746
|
+
});
|
|
747
|
+
|
|
748
|
+
return history;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
private buildRecentRepairsContext(
|
|
752
|
+
recentRepairs: Array<{
|
|
753
|
+
stepNumber: number;
|
|
754
|
+
operation: string;
|
|
755
|
+
originalDescription?: string;
|
|
756
|
+
newDescription?: string;
|
|
757
|
+
originalCode?: string;
|
|
758
|
+
newCode?: string;
|
|
759
|
+
}>
|
|
760
|
+
): string {
|
|
761
|
+
if (recentRepairs.length === 0) {
|
|
762
|
+
return 'No recent repairs to consider.';
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
let context = 'Recent successful repairs that may affect this step:\n\n';
|
|
766
|
+
|
|
767
|
+
recentRepairs.forEach((repair, index) => {
|
|
768
|
+
context += `Step ${repair.stepNumber} was successfully repaired:\n`;
|
|
769
|
+
context += ` Operation: ${repair.operation}\n`;
|
|
770
|
+
|
|
771
|
+
if (repair.operation === 'REMOVE') {
|
|
772
|
+
context += ` Removed: "${repair.originalDescription}"\n`;
|
|
773
|
+
context += ` Code removed:\n ${repair.originalCode?.replace(/\n/g, '\n ')}\n`;
|
|
774
|
+
} else if (repair.operation === 'INSERT') {
|
|
775
|
+
context += ` Inserted: "${repair.newDescription}"\n`;
|
|
776
|
+
context += ` Code inserted:\n ${repair.newCode?.replace(/\n/g, '\n ')}\n`;
|
|
777
|
+
} else {
|
|
778
|
+
context += ` Original: "${repair.originalDescription}"\n`;
|
|
779
|
+
context += ` Repaired: "${repair.newDescription}"\n`;
|
|
780
|
+
context += ` Code changed from:\n ${repair.originalCode?.replace(/\n/g, '\n ')}\n`;
|
|
781
|
+
context += ` To:\n ${repair.newCode?.replace(/\n/g, '\n ')}\n`;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
if (index < recentRepairs.length - 1) {
|
|
785
|
+
context += `\n`;
|
|
786
|
+
}
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
context += '\nConsider how these changes might affect the current step and adjust accordingly.';
|
|
790
|
+
return context;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
private async applyRepairActionInContext(
|
|
794
|
+
action: StepRepairAction,
|
|
795
|
+
steps: (ScriptStep & { success?: boolean; error?: string })[],
|
|
796
|
+
currentIndex: number,
|
|
797
|
+
page: Page,
|
|
798
|
+
executionContext: string,
|
|
799
|
+
contextVariables: Map<string, any>
|
|
800
|
+
): Promise<{ success: boolean; error?: string; updatedContext?: string }> {
|
|
801
|
+
try {
|
|
802
|
+
switch (action.operation) {
|
|
803
|
+
case StepOperation.MODIFY:
|
|
804
|
+
if (action.newStep && action.stepIndex !== undefined) {
|
|
805
|
+
// Modify existing step
|
|
806
|
+
steps[action.stepIndex] = {
|
|
807
|
+
...action.newStep,
|
|
808
|
+
success: false,
|
|
809
|
+
error: undefined
|
|
810
|
+
};
|
|
811
|
+
// Test the modified step with current page state and variables
|
|
812
|
+
await this.executeStepCode(action.newStep.code, page);
|
|
813
|
+
return { success: true, updatedContext: executionContext + action.newStep.code };
|
|
814
|
+
}
|
|
815
|
+
break;
|
|
816
|
+
|
|
817
|
+
case StepOperation.INSERT:
|
|
818
|
+
if (action.newStep && action.insertAfterIndex !== undefined) {
|
|
819
|
+
// Insert new step after specified index
|
|
820
|
+
const insertIndex = action.insertAfterIndex + 1;
|
|
821
|
+
const newStep = {
|
|
822
|
+
...action.newStep,
|
|
823
|
+
success: false,
|
|
824
|
+
error: undefined
|
|
825
|
+
};
|
|
826
|
+
console.log(`INSERT: Inserting step at index ${insertIndex} with description "${newStep.description}"`);
|
|
827
|
+
console.log(`INSERT: Steps before insertion:`, steps.map((s, i) => `${i}: "${s.description}" (success: ${s.success})`));
|
|
828
|
+
|
|
829
|
+
// Preserve success status of existing steps before insertion
|
|
830
|
+
const successStatusMap = new Map(steps.map((step, index) => [index, { success: step.success, error: step.error }]));
|
|
831
|
+
|
|
832
|
+
steps.splice(insertIndex, 0, newStep);
|
|
833
|
+
|
|
834
|
+
// Restore success status for steps that were shifted by the insertion
|
|
835
|
+
// Steps at insertIndex and before keep their original status
|
|
836
|
+
// Steps after insertIndex need to be shifted to their new positions
|
|
837
|
+
for (let i = insertIndex + 1; i < steps.length; i++) {
|
|
838
|
+
const originalIndex = i - 1; // The step that was originally at this position
|
|
839
|
+
if (successStatusMap.has(originalIndex)) {
|
|
840
|
+
const status = successStatusMap.get(originalIndex)!;
|
|
841
|
+
steps[i].success = status.success;
|
|
842
|
+
steps[i].error = status.error;
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
// CRITICAL FIX: Ensure the inserted step doesn't overwrite existing step data
|
|
847
|
+
// The new step should only have its own description, not inherit from existing steps
|
|
848
|
+
console.log(`INSERT: Final step array after restoration:`, steps.map((s, i) => `${i}: "${s.description}" (success: ${s.success})`));
|
|
849
|
+
|
|
850
|
+
console.log(`INSERT: Steps after insertion:`, steps.map((s, i) => `${i}: "${s.description}" (success: ${s.success})`));
|
|
851
|
+
// Test the new step with current page state
|
|
852
|
+
await this.executeStepCode(action.newStep.code, page);
|
|
853
|
+
return { success: true, updatedContext: executionContext + action.newStep.code };
|
|
854
|
+
}
|
|
855
|
+
break;
|
|
856
|
+
|
|
857
|
+
case StepOperation.REMOVE:
|
|
858
|
+
if (action.stepIndex !== undefined) {
|
|
859
|
+
// Remove step
|
|
860
|
+
steps.splice(action.stepIndex, 1);
|
|
861
|
+
return { success: true, updatedContext: executionContext };
|
|
862
|
+
}
|
|
863
|
+
break;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
return { success: false, error: 'Invalid repair action' };
|
|
867
|
+
} catch (error) {
|
|
868
|
+
return {
|
|
869
|
+
success: false,
|
|
870
|
+
error: error instanceof Error ? error.message : 'Unknown error during repair action'
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
private async applyRepairAction(
|
|
876
|
+
action: StepRepairAction,
|
|
877
|
+
steps: (ScriptStep & { success?: boolean; error?: string })[],
|
|
878
|
+
currentIndex: number,
|
|
879
|
+
page: Page
|
|
880
|
+
): Promise<{ success: boolean; error?: string }> {
|
|
881
|
+
try {
|
|
882
|
+
switch (action.operation) {
|
|
883
|
+
case StepOperation.MODIFY:
|
|
884
|
+
if (action.newStep && action.stepIndex !== undefined) {
|
|
885
|
+
// Modify existing step
|
|
886
|
+
steps[action.stepIndex] = {
|
|
887
|
+
...action.newStep,
|
|
888
|
+
success: false,
|
|
889
|
+
error: undefined
|
|
890
|
+
};
|
|
891
|
+
// Test the modified step
|
|
892
|
+
await this.executeStepCode(action.newStep.code, page);
|
|
893
|
+
return { success: true };
|
|
894
|
+
}
|
|
895
|
+
break;
|
|
896
|
+
|
|
897
|
+
case StepOperation.INSERT:
|
|
898
|
+
if (action.newStep && action.insertAfterIndex !== undefined) {
|
|
899
|
+
// Insert new step after specified index
|
|
900
|
+
const insertIndex = action.insertAfterIndex + 1;
|
|
901
|
+
const newStep = {
|
|
902
|
+
...action.newStep,
|
|
903
|
+
success: false,
|
|
904
|
+
error: undefined
|
|
905
|
+
};
|
|
906
|
+
steps.splice(insertIndex, 0, newStep);
|
|
907
|
+
// Test the inserted step
|
|
908
|
+
await this.executeStepCode(action.newStep.code, page);
|
|
909
|
+
return { success: true };
|
|
910
|
+
}
|
|
911
|
+
break;
|
|
912
|
+
|
|
913
|
+
case StepOperation.REMOVE:
|
|
914
|
+
if (action.stepIndex !== undefined) {
|
|
915
|
+
// Remove the step
|
|
916
|
+
steps.splice(action.stepIndex, 1);
|
|
917
|
+
return { success: true };
|
|
918
|
+
}
|
|
919
|
+
break;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
return { success: false, error: 'Invalid repair action' };
|
|
923
|
+
} catch (error) {
|
|
924
|
+
return {
|
|
925
|
+
success: false,
|
|
926
|
+
error: error instanceof Error ? error.message : 'Repair action execution failed'
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
|
|
932
|
+
|
|
933
|
+
private generateUpdatedScript(steps: (ScriptStep & { success?: boolean; error?: string })[], repairAdvice?: string): string {
|
|
934
|
+
const scriptLines = [
|
|
935
|
+
"import { test, expect } from '@playwright/test';",
|
|
936
|
+
`test('repairedTest', async ({ page, browser, context }) => {`
|
|
937
|
+
];
|
|
938
|
+
|
|
939
|
+
steps.forEach((step, index) => {
|
|
940
|
+
scriptLines.push(` // Step ${index + 1}: ${step.description}`);
|
|
941
|
+
const codeLines = step.code.split('\n');
|
|
942
|
+
codeLines.forEach(line => {
|
|
943
|
+
scriptLines.push(` ${line}`);
|
|
944
|
+
});
|
|
945
|
+
});
|
|
946
|
+
|
|
947
|
+
scriptLines.push('});');
|
|
948
|
+
const script = scriptLines.join('\n');
|
|
949
|
+
|
|
950
|
+
// Add TestChimp comment to the repaired script with repair advice
|
|
951
|
+
return addTestChimpComment(script, repairAdvice);
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
|
|
955
|
+
/**
|
|
956
|
+
* Initialize browser with configuration (delegates to utility function)
|
|
957
|
+
*/
|
|
958
|
+
private async initializeBrowser(playwrightConfig?: string, headless?: boolean, playwrightConfigFilePath?: string): Promise<{ browser: Browser; context: BrowserContext; page: Page }> {
|
|
959
|
+
return initializeBrowser(playwrightConfig, headless, playwrightConfigFilePath);
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
/**
|
|
963
|
+
* Safely serialize error information, filtering out non-serializable values
|
|
964
|
+
*/
|
|
965
|
+
private safeSerializeError(error: any): string {
|
|
966
|
+
try {
|
|
967
|
+
if (error instanceof Error) {
|
|
968
|
+
return error.message;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
if (typeof error === 'string') {
|
|
972
|
+
return error;
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
if (typeof error === 'object' && error !== null) {
|
|
976
|
+
// Try to extract meaningful information without serializing the entire object
|
|
977
|
+
const safeError: any = {};
|
|
978
|
+
|
|
979
|
+
// Copy safe properties
|
|
980
|
+
if (error.message) safeError.message = error.message;
|
|
981
|
+
if (error.name) safeError.name = error.name;
|
|
982
|
+
if (error.code) safeError.code = error.code;
|
|
983
|
+
if (error.status) safeError.status = error.status;
|
|
984
|
+
|
|
985
|
+
// Try to get stack trace safely
|
|
986
|
+
if (error.stack && typeof error.stack === 'string') {
|
|
987
|
+
safeError.stack = error.stack;
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
return JSON.stringify(safeError);
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
return String(error);
|
|
994
|
+
} catch (serializationError) {
|
|
995
|
+
// If even safe serialization fails, return a basic string representation
|
|
996
|
+
return `Error: ${String(error)}`;
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
}
|