testchimp-runner-core 0.0.15 → 0.0.17
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/execution-service.d.ts +14 -0
- package/dist/execution-service.d.ts.map +1 -1
- package/dist/execution-service.js +84 -52
- package/dist/execution-service.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/execution-service.ts +87 -52
- package/src/index.ts +19 -0
- package/tsc +1029 -0
package/src/execution-service.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { initializeBrowser } from './utils/browser-utils';
|
|
|
16
16
|
import { LLMFacade } from './llm-facade';
|
|
17
17
|
import { AuthConfig } from './auth-config';
|
|
18
18
|
import { addTestChimpComment } from './script-utils';
|
|
19
|
+
import { CreditUsageService } from './credit-usage-service';
|
|
19
20
|
|
|
20
21
|
/**
|
|
21
22
|
* Service for orchestrating Playwright script execution
|
|
@@ -23,15 +24,42 @@ import { addTestChimpComment } from './script-utils';
|
|
|
23
24
|
export class ExecutionService {
|
|
24
25
|
private playwrightService: PlaywrightService;
|
|
25
26
|
private llmFacade: LLMFacade;
|
|
27
|
+
private creditUsageService: CreditUsageService;
|
|
26
28
|
private maxConcurrentExecutions: number;
|
|
27
29
|
private activeExecutions: Set<Promise<any>> = new Set();
|
|
30
|
+
private logger?: (message: string, level?: 'log' | 'error' | 'warn') => void;
|
|
28
31
|
|
|
29
32
|
constructor(authConfig?: AuthConfig, backendUrl?: string, maxConcurrentExecutions: number = 10) {
|
|
30
33
|
this.playwrightService = new PlaywrightService();
|
|
31
34
|
this.llmFacade = new LLMFacade(authConfig, backendUrl);
|
|
35
|
+
this.creditUsageService = new CreditUsageService(authConfig, backendUrl);
|
|
32
36
|
this.maxConcurrentExecutions = maxConcurrentExecutions;
|
|
33
37
|
}
|
|
34
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Set a logger callback for capturing execution logs
|
|
41
|
+
*/
|
|
42
|
+
setLogger(logger: (message: string, level?: 'log' | 'error' | 'warn') => void): void {
|
|
43
|
+
this.logger = logger;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Log a message using the configured logger or console
|
|
48
|
+
*/
|
|
49
|
+
private log(message: string, level: 'log' | 'error' | 'warn' = 'log'): void {
|
|
50
|
+
if (this.logger) {
|
|
51
|
+
this.logger(message, level);
|
|
52
|
+
} else {
|
|
53
|
+
if (level === 'error') {
|
|
54
|
+
console.error(message);
|
|
55
|
+
} else if (level === 'warn') {
|
|
56
|
+
console.warn(message);
|
|
57
|
+
} else {
|
|
58
|
+
console.log(message);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
35
63
|
/**
|
|
36
64
|
* Initialize the execution service
|
|
37
65
|
*/
|
|
@@ -39,6 +67,14 @@ export class ExecutionService {
|
|
|
39
67
|
await this.playwrightService.initialize();
|
|
40
68
|
}
|
|
41
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Set authentication configuration for the service
|
|
72
|
+
*/
|
|
73
|
+
setAuthConfig(authConfig: AuthConfig): void {
|
|
74
|
+
this.llmFacade.setAuthConfig(authConfig);
|
|
75
|
+
this.creditUsageService.setAuthConfig(authConfig);
|
|
76
|
+
}
|
|
77
|
+
|
|
42
78
|
|
|
43
79
|
/**
|
|
44
80
|
* Execute a script with optional AI repair capabilities
|
|
@@ -178,7 +214,7 @@ export class ExecutionService {
|
|
|
178
214
|
const totalAttempts = deflakeRunCount + 1; // Original run + deflake attempts
|
|
179
215
|
let lastError: Error | null = null;
|
|
180
216
|
|
|
181
|
-
|
|
217
|
+
this.log(`runExactly: deflake_run_count = ${request.deflake_run_count}, totalAttempts = ${totalAttempts}`);
|
|
182
218
|
|
|
183
219
|
// Script content should be provided by the caller (TestChimpService)
|
|
184
220
|
// The TestChimpService handles file reading through the appropriate FileHandler
|
|
@@ -187,7 +223,7 @@ export class ExecutionService {
|
|
|
187
223
|
}
|
|
188
224
|
|
|
189
225
|
for (let attempt = 1; attempt <= totalAttempts; attempt++) {
|
|
190
|
-
|
|
226
|
+
this.log(`Attempting deflake run ${attempt}/${totalAttempts}`);
|
|
191
227
|
const { browser, context, page } = await this.initializeBrowser(request.playwrightConfig, request.headless, request.playwrightConfigFilePath);
|
|
192
228
|
|
|
193
229
|
try {
|
|
@@ -204,7 +240,7 @@ export class ExecutionService {
|
|
|
204
240
|
};
|
|
205
241
|
} catch (error) {
|
|
206
242
|
lastError = error instanceof Error ? error : new Error('Script execution failed');
|
|
207
|
-
|
|
243
|
+
this.log(`Initial run failed: ${lastError.message}`);
|
|
208
244
|
|
|
209
245
|
try {
|
|
210
246
|
await browser.close();
|
|
@@ -214,7 +250,7 @@ export class ExecutionService {
|
|
|
214
250
|
|
|
215
251
|
// If this is not the last attempt, continue to next attempt
|
|
216
252
|
if (attempt < totalAttempts) {
|
|
217
|
-
|
|
253
|
+
this.log(`Deflaking attempt ${attempt} failed, trying again... (${attempt + 1}/${totalAttempts})`);
|
|
218
254
|
continue;
|
|
219
255
|
}
|
|
220
256
|
}
|
|
@@ -239,7 +275,7 @@ export class ExecutionService {
|
|
|
239
275
|
}
|
|
240
276
|
|
|
241
277
|
// First, try runExactly (which includes deflaking if configured)
|
|
242
|
-
|
|
278
|
+
this.log('Attempting runExactly first (with deflaking if configured)...');
|
|
243
279
|
const runExactlyResult = await this.runExactly(request, startTime, model);
|
|
244
280
|
|
|
245
281
|
// If runExactly succeeded, return that result
|
|
@@ -248,18 +284,18 @@ export class ExecutionService {
|
|
|
248
284
|
}
|
|
249
285
|
|
|
250
286
|
// runExactly failed, start AI repair
|
|
251
|
-
|
|
287
|
+
this.log('runExactly failed, starting AI repair process...');
|
|
252
288
|
|
|
253
289
|
try {
|
|
254
290
|
|
|
255
291
|
// Start browser initialization and script parsing in parallel for faster startup
|
|
256
|
-
|
|
292
|
+
this.log('Initializing repair browser and parsing script...');
|
|
257
293
|
const [steps, { browser: repairBrowser, context: repairContext, page: repairPage }] = await Promise.all([
|
|
258
294
|
this.parseScriptIntoSteps(request.script, model),
|
|
259
295
|
this.initializeBrowser(request.playwrightConfig, request.headless, request.playwrightConfigFilePath) // Use request.headless (defaults to false/headed)
|
|
260
296
|
]);
|
|
261
297
|
|
|
262
|
-
|
|
298
|
+
this.log('Starting AI repair with parsed steps...');
|
|
263
299
|
const updatedSteps = await this.repairStepsWithAI(steps, repairPage, repairFlexibility, model);
|
|
264
300
|
|
|
265
301
|
// Always generate the updated script
|
|
@@ -272,13 +308,13 @@ export class ExecutionService {
|
|
|
272
308
|
const hasSuccessfulRepairs = updatedSteps.some(step => step.success);
|
|
273
309
|
|
|
274
310
|
// Debug: Log step success status
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
311
|
+
this.log('Step success status: ' + updatedSteps.map((step, index) => `Step ${index + 1}: ${step.success ? 'SUCCESS' : 'FAILED'}`).join(', '));
|
|
312
|
+
this.log(`All steps successful: ${allStepsSuccessful}`);
|
|
313
|
+
this.log(`Has successful repairs: ${hasSuccessfulRepairs}`);
|
|
278
314
|
|
|
279
315
|
// Debug: Log individual step details
|
|
280
316
|
updatedSteps.forEach((step, index) => {
|
|
281
|
-
|
|
317
|
+
this.log(`Step ${index + 1} details: success=${step.success}, description="${step.description}"`);
|
|
282
318
|
});
|
|
283
319
|
|
|
284
320
|
// Update file if we have any successful repairs (partial or complete)
|
|
@@ -289,6 +325,11 @@ export class ExecutionService {
|
|
|
289
325
|
// Ensure the final script has the correct TestChimp comment format with repair advice
|
|
290
326
|
const scriptWithRepairAdvice = addTestChimpComment(finalScript, confidenceResponse.advice);
|
|
291
327
|
|
|
328
|
+
// Report credit usage for successful AI repair
|
|
329
|
+
this.creditUsageService.reportAIRepairCredit().catch(error => {
|
|
330
|
+
console.warn(`Failed to report credit usage for AI repair:`, error);
|
|
331
|
+
});
|
|
332
|
+
|
|
292
333
|
await repairBrowser.close();
|
|
293
334
|
|
|
294
335
|
return {
|
|
@@ -329,14 +370,14 @@ export class ExecutionService {
|
|
|
329
370
|
private async parseScriptIntoSteps(script: string, model: string): Promise<(ScriptStep & { success?: boolean; error?: string })[]> {
|
|
330
371
|
// First try LLM-based parsing
|
|
331
372
|
try {
|
|
332
|
-
|
|
373
|
+
this.log('Attempting LLM-based script parsing...');
|
|
333
374
|
const result = await this.llmFacade.parseScriptIntoSteps(script, model);
|
|
334
|
-
|
|
375
|
+
this.log(`LLM parsing successful, got ${result.length} steps`);
|
|
335
376
|
return result;
|
|
336
377
|
} catch (error) {
|
|
337
|
-
|
|
378
|
+
this.log(`LLM parsing failed, falling back to code parsing: ${error}`);
|
|
338
379
|
const fallbackResult = this.parseScriptIntoStepsFallback(script);
|
|
339
|
-
|
|
380
|
+
this.log(`Fallback parsing successful, got ${fallbackResult.length} steps`);
|
|
340
381
|
return fallbackResult;
|
|
341
382
|
}
|
|
342
383
|
}
|
|
@@ -412,26 +453,26 @@ export class ExecutionService {
|
|
|
412
453
|
let i = 0;
|
|
413
454
|
while (i < updatedSteps.length) {
|
|
414
455
|
const step = updatedSteps[i];
|
|
415
|
-
|
|
456
|
+
this.log(`Loop iteration: i=${i}, step description="${step.description}", total steps=${updatedSteps.length}`);
|
|
416
457
|
|
|
417
458
|
try {
|
|
418
459
|
// Try to execute the step directly without context replay
|
|
419
|
-
|
|
420
|
-
|
|
460
|
+
this.log(`Attempting Step ${i + 1}: ${step.description}`);
|
|
461
|
+
this.log(` Code: ${step.code}`);
|
|
421
462
|
await this.executeStepCode(step.code, page);
|
|
422
463
|
step.success = true;
|
|
423
|
-
|
|
424
|
-
|
|
464
|
+
this.log(`Step ${i + 1} executed successfully: ${step.description}`);
|
|
465
|
+
this.log(`Step ${i + 1} success status set to: ${step.success}`);
|
|
425
466
|
|
|
426
467
|
// Add this step's code to the execution context for future steps (for variable tracking)
|
|
427
468
|
executionContext += step.code + '\n';
|
|
428
469
|
i++; // Move to next step
|
|
429
470
|
} catch (error) {
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
471
|
+
this.log(`Step ${i + 1} failed: ${step.description}`);
|
|
472
|
+
this.log(` Failed code: ${step.code}`);
|
|
473
|
+
this.log(` Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
433
474
|
if (error instanceof Error && error.stack) {
|
|
434
|
-
|
|
475
|
+
this.log(` Stack trace: ${error.stack}`);
|
|
435
476
|
}
|
|
436
477
|
step.success = false;
|
|
437
478
|
step.error = this.safeSerializeError(error);
|
|
@@ -449,7 +490,7 @@ export class ExecutionService {
|
|
|
449
490
|
const originalCode = step.code;
|
|
450
491
|
|
|
451
492
|
for (let attempt = 1; attempt <= maxTries; attempt++) {
|
|
452
|
-
|
|
493
|
+
this.log(`Step ${i + 1} repair attempt ${attempt}/${maxTries}`);
|
|
453
494
|
|
|
454
495
|
// Get current page state for AI repair
|
|
455
496
|
const pageInfo = await this.getEnhancedPageInfo(page);
|
|
@@ -472,7 +513,7 @@ export class ExecutionService {
|
|
|
472
513
|
);
|
|
473
514
|
|
|
474
515
|
if (!repairSuggestion.shouldContinue) {
|
|
475
|
-
|
|
516
|
+
this.log(`AI decided to stop repair at attempt ${attempt}: ${repairSuggestion.reason}`);
|
|
476
517
|
break;
|
|
477
518
|
}
|
|
478
519
|
|
|
@@ -485,20 +526,14 @@ export class ExecutionService {
|
|
|
485
526
|
insertAfterIndex: repairSuggestion.action.operation === StepOperation.INSERT ? i - 1 : undefined // For INSERT, insert before current step
|
|
486
527
|
};
|
|
487
528
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
stepIndex: repairAction.stepIndex,
|
|
491
|
-
insertAfterIndex: repairAction.insertAfterIndex,
|
|
492
|
-
newStepDescription: repairAction.newStep?.description,
|
|
493
|
-
newStepCode: repairAction.newStep?.code
|
|
494
|
-
});
|
|
495
|
-
console.log(`🔧 Steps array before repair:`, updatedSteps.map((s, idx) => `${idx}: "${s.description}" (success: ${s.success})`));
|
|
529
|
+
this.log(`🔧 Applying repair action: ${repairAction.operation} on step ${repairAction.stepIndex}`);
|
|
530
|
+
this.log(`🔧 Steps array before repair: ${updatedSteps.map((s, idx) => `${idx}: "${s.description}" (success: ${s.success})`).join(', ')}`);
|
|
496
531
|
|
|
497
532
|
const result = await this.applyRepairActionInContext(repairAction, updatedSteps, i, page, executionContext, contextVariables);
|
|
498
533
|
|
|
499
534
|
if (result.success) {
|
|
500
535
|
repairSuccess = true;
|
|
501
|
-
|
|
536
|
+
this.log(`🔧 Steps array after repair: ${updatedSteps.map((s, idx) => `${idx}: "${s.description}" (success: ${s.success})`).join(', ')}`);
|
|
502
537
|
|
|
503
538
|
// Mark the appropriate step(s) as successful based on operation type
|
|
504
539
|
if (repairAction.operation === StepOperation.MODIFY) {
|
|
@@ -507,7 +542,7 @@ export class ExecutionService {
|
|
|
507
542
|
step.error = undefined;
|
|
508
543
|
updatedSteps[i].success = true;
|
|
509
544
|
updatedSteps[i].error = undefined;
|
|
510
|
-
|
|
545
|
+
this.log(`Step ${i + 1} marked as successful after MODIFY repair`);
|
|
511
546
|
} else if (repairAction.operation === StepOperation.INSERT) {
|
|
512
547
|
// For INSERT: mark the newly inserted step as successful
|
|
513
548
|
const insertIndex = repairAction.insertAfterIndex !== undefined ? repairAction.insertAfterIndex + 1 : i + 1;
|
|
@@ -527,7 +562,7 @@ export class ExecutionService {
|
|
|
527
562
|
repairAction.operation === StepOperation.REMOVE ?
|
|
528
563
|
`REMOVE: step at index ${repairAction.stepIndex}` :
|
|
529
564
|
repairAction.operation;
|
|
530
|
-
|
|
565
|
+
this.log(`Step ${i + 1} repair action ${commandInfo} executed successfully on attempt ${attempt}`);
|
|
531
566
|
|
|
532
567
|
// Update execution context based on the repair action
|
|
533
568
|
if (repairAction.operation === StepOperation.MODIFY && repairAction.newStep) {
|
|
@@ -559,21 +594,21 @@ export class ExecutionService {
|
|
|
559
594
|
// Update step index based on operation
|
|
560
595
|
if (repairAction.operation === StepOperation.INSERT) {
|
|
561
596
|
// For INSERT: inserted step is already executed
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
597
|
+
this.log(`INSERT operation: current i=${i}, insertAfterIndex=${repairAction.insertAfterIndex}`);
|
|
598
|
+
this.log(`INSERT: Steps array length before: ${updatedSteps.length}`);
|
|
599
|
+
this.log(`INSERT: Steps before operation: ${updatedSteps.map((s, idx) => `${idx}: "${s.description}" (success: ${s.success})`).join(', ')}`);
|
|
565
600
|
|
|
566
601
|
if (repairAction.insertAfterIndex !== undefined && repairAction.insertAfterIndex < i) {
|
|
567
602
|
// If inserting before current position, current step moved down by 1
|
|
568
|
-
|
|
603
|
+
this.log(`INSERT before current position: incrementing i from ${i} to ${i + 1}`);
|
|
569
604
|
i++; // Move to the original step that was pushed to the next position
|
|
570
605
|
} else {
|
|
571
606
|
// If inserting at or after current position, stay at current step
|
|
572
|
-
|
|
607
|
+
this.log(`INSERT at/after current position: keeping i at ${i}`);
|
|
573
608
|
}
|
|
574
609
|
|
|
575
|
-
|
|
576
|
-
|
|
610
|
+
this.log(`INSERT: Steps array length after: ${updatedSteps.length}`);
|
|
611
|
+
this.log(`INSERT: Steps after operation: ${updatedSteps.map((s, idx) => `${idx}: "${s.description}" (success: ${s.success})`).join(', ')}`);
|
|
577
612
|
} else if (repairAction.operation === StepOperation.REMOVE) {
|
|
578
613
|
// For REMOVE: stay at same index since the next step moved to current position
|
|
579
614
|
// Don't increment i because the array shifted left
|
|
@@ -598,9 +633,9 @@ export class ExecutionService {
|
|
|
598
633
|
repairSuggestion.action.operation === StepOperation.REMOVE ?
|
|
599
634
|
`REMOVE: step at index ${repairSuggestion.action.stepIndex}` :
|
|
600
635
|
repairSuggestion.action.operation;
|
|
601
|
-
|
|
636
|
+
this.log(`Step ${i + 1} repair attempt ${attempt} failed (${commandInfo}): ${repairErrorMessage}`);
|
|
602
637
|
if (repairError instanceof Error && repairError.stack) {
|
|
603
|
-
|
|
638
|
+
this.log(` Repair stack trace: ${repairError.stack}`);
|
|
604
639
|
}
|
|
605
640
|
|
|
606
641
|
// Record this attempt in history
|
|
@@ -616,7 +651,7 @@ export class ExecutionService {
|
|
|
616
651
|
}
|
|
617
652
|
|
|
618
653
|
if (!repairSuccess) {
|
|
619
|
-
|
|
654
|
+
this.log(`Step ${i + 1} failed after ${maxTries} repair attempts`);
|
|
620
655
|
break;
|
|
621
656
|
}
|
|
622
657
|
}
|
|
@@ -855,8 +890,8 @@ export class ExecutionService {
|
|
|
855
890
|
success: false,
|
|
856
891
|
error: undefined
|
|
857
892
|
};
|
|
858
|
-
|
|
859
|
-
|
|
893
|
+
this.log(`INSERT: Inserting step at index ${insertIndex} with description "${newStep.description}"`);
|
|
894
|
+
this.log(`INSERT: Steps before insertion: ${steps.map((s, i) => `${i}: "${s.description}" (success: ${s.success})`).join(', ')}`);
|
|
860
895
|
|
|
861
896
|
// Preserve success status of existing steps before insertion
|
|
862
897
|
const successStatusMap = new Map(steps.map((step, index) => [index, { success: step.success, error: step.error }]));
|
|
@@ -877,9 +912,9 @@ export class ExecutionService {
|
|
|
877
912
|
|
|
878
913
|
// CRITICAL FIX: Ensure the inserted step doesn't overwrite existing step data
|
|
879
914
|
// The new step should only have its own description, not inherit from existing steps
|
|
880
|
-
|
|
915
|
+
this.log(`INSERT: Final step array after restoration: ${steps.map((s, i) => `${i}: "${s.description}" (success: ${s.success})`).join(', ')}`);
|
|
881
916
|
|
|
882
|
-
|
|
917
|
+
this.log(`INSERT: Steps after insertion: ${steps.map((s, i) => `${i}: "${s.description}" (success: ${s.success})`).join(', ')}`);
|
|
883
918
|
// Test the new step with current page state
|
|
884
919
|
await this.executeStepCode(action.newStep.code, page);
|
|
885
920
|
return { success: true, updatedContext: executionContext + action.newStep.code };
|
package/src/index.ts
CHANGED
|
@@ -49,6 +49,8 @@ export class TestChimpService {
|
|
|
49
49
|
this.llmFacade = new LLMFacade(this.authConfig || undefined, this.backendUrl);
|
|
50
50
|
this.creditUsageService = new CreditUsageService(this.authConfig || undefined, this.backendUrl);
|
|
51
51
|
this.executionService = new ExecutionService(this.authConfig || undefined, this.backendUrl, maxWorkers || 10);
|
|
52
|
+
// Set the credit usage service for the execution service
|
|
53
|
+
this.executionService.setAuthConfig(this.authConfig || {} as AuthConfig);
|
|
52
54
|
this.scenarioService = new ScenarioService(maxWorkers || 2, this.fileHandler, this.authConfig || undefined, this.backendUrl);
|
|
53
55
|
}
|
|
54
56
|
|
|
@@ -64,6 +66,9 @@ export class TestChimpService {
|
|
|
64
66
|
this.executionService = new ExecutionService(this.authConfig, this.backendUrl, 10);
|
|
65
67
|
this.scenarioService = new ScenarioService(2, this.fileHandler, this.authConfig, this.backendUrl);
|
|
66
68
|
|
|
69
|
+
// Set auth config for the execution service
|
|
70
|
+
this.executionService.setAuthConfig(authConfig);
|
|
71
|
+
|
|
67
72
|
// Reinitialize the services
|
|
68
73
|
await this.executionService.initialize();
|
|
69
74
|
await this.scenarioService.initialize();
|
|
@@ -77,6 +82,20 @@ export class TestChimpService {
|
|
|
77
82
|
// Recreate services with new backend URL
|
|
78
83
|
this.llmFacade = new LLMFacade(this.authConfig || undefined, this.backendUrl);
|
|
79
84
|
this.creditUsageService = new CreditUsageService(this.authConfig || undefined, this.backendUrl);
|
|
85
|
+
this.executionService = new ExecutionService(this.authConfig || undefined, this.backendUrl, 10);
|
|
86
|
+
this.scenarioService = new ScenarioService(2, this.fileHandler, this.authConfig || undefined, this.backendUrl);
|
|
87
|
+
|
|
88
|
+
// Set auth config for the execution service
|
|
89
|
+
if (this.authConfig) {
|
|
90
|
+
this.executionService.setAuthConfig(this.authConfig);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Set logger callback for capturing execution logs
|
|
96
|
+
*/
|
|
97
|
+
setLogger(logger: (message: string, level?: 'log' | 'error' | 'warn') => void): void {
|
|
98
|
+
this.executionService.setLogger(logger);
|
|
80
99
|
}
|
|
81
100
|
|
|
82
101
|
/**
|