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.
@@ -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
- console.log(`runExactly: deflake_run_count = ${request.deflake_run_count}, totalAttempts = ${totalAttempts}`);
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
- console.log(`Attempting deflake run ${attempt}/${totalAttempts}`);
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
- console.log(`Initial run failed: ${lastError.message}`);
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
- console.log(`Deflaking attempt ${attempt} failed, trying again... (${attempt + 1}/${totalAttempts})`);
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
- console.log('Attempting runExactly first (with deflaking if configured)...');
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
- console.log('runExactly failed, starting AI repair process...');
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
- console.log('Initializing repair browser and parsing script...');
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
- console.log('Starting AI repair with parsed steps...');
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
- console.log('Step success status:', updatedSteps.map((step, index) => `Step ${index + 1}: ${step.success ? 'SUCCESS' : 'FAILED'}`));
276
- console.log('All steps successful:', allStepsSuccessful);
277
- console.log('Has successful repairs:', hasSuccessfulRepairs);
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
- console.log(`Step ${index + 1} details: success=${step.success}, description="${step.description}"`);
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
- console.log('Attempting LLM-based script parsing...');
373
+ this.log('Attempting LLM-based script parsing...');
333
374
  const result = await this.llmFacade.parseScriptIntoSteps(script, model);
334
- console.log('LLM parsing successful, got', result.length, 'steps');
375
+ this.log(`LLM parsing successful, got ${result.length} steps`);
335
376
  return result;
336
377
  } catch (error) {
337
- console.log('LLM parsing failed, falling back to code parsing:', error);
378
+ this.log(`LLM parsing failed, falling back to code parsing: ${error}`);
338
379
  const fallbackResult = this.parseScriptIntoStepsFallback(script);
339
- console.log('Fallback parsing successful, got', fallbackResult.length, 'steps');
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
- console.log(`Loop iteration: i=${i}, step description="${step.description}", total steps=${updatedSteps.length}`);
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
- console.log(`Attempting Step ${i + 1}: ${step.description}`);
420
- console.log(` Code: ${step.code}`);
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
- console.log(`Step ${i + 1} executed successfully: ${step.description}`);
424
- console.log(`Step ${i + 1} success status set to: ${step.success}`);
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
- console.log(`Step ${i + 1} failed: ${step.description}`);
431
- console.log(` Failed code: ${step.code}`);
432
- console.log(` Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
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
- console.log(` Stack trace: ${error.stack}`);
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
- console.log(`Step ${i + 1} repair attempt ${attempt}/${maxTries}`);
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
- console.log(`AI decided to stop repair at attempt ${attempt}: ${repairSuggestion.reason}`);
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
- console.log(`🔧 Applying repair action:`, {
489
- operation: repairAction.operation,
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
- console.log(`🔧 Steps array after repair:`, updatedSteps.map((s, idx) => `${idx}: "${s.description}" (success: ${s.success})`));
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
- console.log(`Step ${i + 1} marked as successful after MODIFY repair`);
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
- console.log(`Step ${i + 1} repair action ${commandInfo} executed successfully on attempt ${attempt}`);
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
- console.log(`INSERT operation: current i=${i}, insertAfterIndex=${repairAction.insertAfterIndex}`);
563
- console.log(`INSERT: Steps array length before: ${updatedSteps.length}`);
564
- console.log(`INSERT: Steps before operation:`, updatedSteps.map((s, idx) => `${idx}: "${s.description}" (success: ${s.success})`));
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
- console.log(`INSERT before current position: incrementing i from ${i} to ${i + 1}`);
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
- console.log(`INSERT at/after current position: keeping i at ${i}`);
607
+ this.log(`INSERT at/after current position: keeping i at ${i}`);
573
608
  }
574
609
 
575
- console.log(`INSERT: Steps array length after: ${updatedSteps.length}`);
576
- console.log(`INSERT: Steps after operation:`, updatedSteps.map((s, idx) => `${idx}: "${s.description}" (success: ${s.success})`));
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
- console.log(`Step ${i + 1} repair attempt ${attempt} failed (${commandInfo}): ${repairErrorMessage}`);
636
+ this.log(`Step ${i + 1} repair attempt ${attempt} failed (${commandInfo}): ${repairErrorMessage}`);
602
637
  if (repairError instanceof Error && repairError.stack) {
603
- console.log(` Repair stack trace: ${repairError.stack}`);
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
- console.log(`Step ${i + 1} failed after ${maxTries} repair attempts`);
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
- console.log(`INSERT: Inserting step at index ${insertIndex} with description "${newStep.description}"`);
859
- console.log(`INSERT: Steps before insertion:`, steps.map((s, i) => `${i}: "${s.description}" (success: ${s.success})`));
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
- console.log(`INSERT: Final step array after restoration:`, steps.map((s, i) => `${i}: "${s.description}" (success: ${s.success})`));
915
+ this.log(`INSERT: Final step array after restoration: ${steps.map((s, i) => `${i}: "${s.description}" (success: ${s.success})`).join(', ')}`);
881
916
 
882
- console.log(`INSERT: Steps after insertion:`, steps.map((s, i) => `${i}: "${s.description}" (success: ${s.success})`));
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
  /**