testchimp-runner-core 0.1.21 → 0.1.23

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.
@@ -1 +1 @@
1
- {"version":3,"file":"execution-service.d.ts","sourceRoot":"","sources":["../src/execution-service.ts"],"names":[],"mappings":"AAMA,OAAO,EACL,0BAA0B,EAC1B,2BAA2B,EAE3B,sBAAsB,EACtB,uBAAuB,EAKvB,wBAAwB,EACxB,yBAAyB,EAG1B,MAAM,SAAS,CAAC;AAKjB,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAW3C,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC7C,OAAO,EAAE,gBAAgB,EAA+C,MAAM,qBAAqB,CAAC;AAmbpG;;GAEG;AACH,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,iBAAiB,CAAoB;IAC7C,OAAO,CAAC,SAAS,CAAY;IAC7B,OAAO,CAAC,WAAW,CAAc;IACjC,OAAO,CAAC,gBAAgB,CAAC,CAAmB;IAC5C,OAAO,CAAC,kBAAkB,CAAqB;IAC/C,OAAO,CAAC,uBAAuB,CAAS;IACxC,OAAO,CAAC,gBAAgB,CAAgC;IACxD,OAAO,CAAC,MAAM,CAAC,CAA8D;IAC7E,OAAO,CAAC,iBAAiB,CAAoB;IAC7C,OAAO,CAAC,cAAc,CAAkB;IAExC;;OAEG;IACH,aAAa;gBAKX,UAAU,CAAC,EAAE,UAAU,EACvB,UAAU,CAAC,EAAE,MAAM,EACnB,uBAAuB,GAAE,MAAW,EACpC,WAAW,CAAC,EAAE,WAAW,EACzB,gBAAgB,CAAC,EAAE,gBAAgB;IA8BrC;;OAEG;IACH,SAAS,CAAC,MAAM,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,KAAK,GAAG,OAAO,GAAG,MAAM,KAAK,IAAI,GAAG,IAAI;IAKpF;;OAEG;IACH,OAAO,CAAC,GAAG;IASX,OAAO,CAAC,WAAW;IAanB;;OAEG;IACG,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAIjC;;;OAGG;IACH,aAAa,CAAC,UAAU,EAAE,UAAU,GAAG,IAAI;IAQ3C;;OAEG;IACG,aAAa,CAAC,OAAO,EAAE,sBAAsB,GAAG,OAAO,CAAC,uBAAuB,CAAC;IAkBtF;;;;;;;;;;OAUG;IACG,cAAc,CAClB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,GAAG,EACT,OAAO,CAAC,EAAE,GAAG,EACb,cAAc,CAAC,EAAE,GAAG,EACpB,OAAO,CAAC,EAAE,MAAM,GACf,OAAO,CAAC,IAAI,CAAC;IAuBhB;;OAEG;YACW,qBAAqB;IAmBnC;;OAEG;IACG,gBAAgB,CAAC,OAAO,EAAE,0BAA0B,GAAG,OAAO,CAAC,2BAA2B,CAAC;IAgCjG;;OAEG;IACH,OAAO,CAAC,qBAAqB;IAsC7B;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAI5B;;OAEG;IACH,OAAO,IAAI,OAAO;IAIlB;;;;OAIG;YACW,+BAA+B;IAowB7C;;;OAGG;YACW,iBAAiB;YAiGjB,UAAU;YAkLV,eAAe;YAoKf,oBAAoB;YAiBpB,eAAe;IAuC7B,OAAO,CAAC,qBAAqB;IAK7B;;OAEG;YACW,iBAAiB;IAI/B;;;;OAIG;IACG,WAAW,CAAC,OAAO,EAAE,wBAAwB,GAAG,OAAO,CAAC,yBAAyB,CAAC;IAorBxF;;;;;OAKG;YACW,WAAW;CAgI1B"}
1
+ {"version":3,"file":"execution-service.d.ts","sourceRoot":"","sources":["../src/execution-service.ts"],"names":[],"mappings":"AAMA,OAAO,EACL,0BAA0B,EAC1B,2BAA2B,EAE3B,sBAAsB,EACtB,uBAAuB,EAKvB,wBAAwB,EACxB,yBAAyB,EAG1B,MAAM,SAAS,CAAC;AAKjB,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAY3C,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC7C,OAAO,EAAE,gBAAgB,EAAuE,MAAM,qBAAqB,CAAC;AAmb5H;;GAEG;AACH,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,iBAAiB,CAAoB;IAC7C,OAAO,CAAC,SAAS,CAAY;IAC7B,OAAO,CAAC,WAAW,CAAc;IACjC,OAAO,CAAC,gBAAgB,CAAC,CAAmB;IAC5C,OAAO,CAAC,kBAAkB,CAAqB;IAC/C,OAAO,CAAC,uBAAuB,CAAS;IACxC,OAAO,CAAC,gBAAgB,CAAgC;IACxD,OAAO,CAAC,MAAM,CAAC,CAA8D;IAC7E,OAAO,CAAC,iBAAiB,CAAoB;IAC7C,OAAO,CAAC,cAAc,CAAkB;IAExC;;OAEG;IACH,aAAa;gBAKX,UAAU,CAAC,EAAE,UAAU,EACvB,UAAU,CAAC,EAAE,MAAM,EACnB,uBAAuB,GAAE,MAAW,EACpC,WAAW,CAAC,EAAE,WAAW,EACzB,gBAAgB,CAAC,EAAE,gBAAgB;IA8BrC;;OAEG;IACH,SAAS,CAAC,MAAM,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,KAAK,GAAG,OAAO,GAAG,MAAM,KAAK,IAAI,GAAG,IAAI;IAKpF;;OAEG;IACH,OAAO,CAAC,GAAG;IASX,OAAO,CAAC,WAAW;IAanB;;OAEG;IACG,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAIjC;;;OAGG;IACH,aAAa,CAAC,UAAU,EAAE,UAAU,GAAG,IAAI;IAQ3C;;OAEG;IACG,aAAa,CAAC,OAAO,EAAE,sBAAsB,GAAG,OAAO,CAAC,uBAAuB,CAAC;IAkBtF;;;;;;;;;;OAUG;IACG,cAAc,CAClB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,GAAG,EACT,OAAO,CAAC,EAAE,GAAG,EACb,cAAc,CAAC,EAAE,GAAG,EACpB,OAAO,CAAC,EAAE,MAAM,GACf,OAAO,CAAC,IAAI,CAAC;IAuBhB;;OAEG;YACW,qBAAqB;IAmBnC;;OAEG;IACG,gBAAgB,CAAC,OAAO,EAAE,0BAA0B,GAAG,OAAO,CAAC,2BAA2B,CAAC;IAgCjG;;OAEG;IACH,OAAO,CAAC,qBAAqB;IAsC7B;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAI5B;;OAEG;IACH,OAAO,IAAI,OAAO;IAIlB;;;;OAIG;YACW,+BAA+B;IA86B7C;;;OAGG;YACW,iBAAiB;YAiGjB,UAAU;YAoMV,eAAe;YAsKf,oBAAoB;YAiBpB,eAAe;IAuC7B,OAAO,CAAC,qBAAqB;IAK7B;;OAEG;YACW,iBAAiB;IAI/B;;;;OAIG;IACG,WAAW,CAAC,OAAO,EAAE,wBAAwB,GAAG,OAAO,CAAC,yBAAyB,CAAC;IAuxBxF;;;;;OAKG;YACW,WAAW;CAoI1B"}
@@ -51,6 +51,7 @@ const import_utils_1 = require("./utils/import-utils");
51
51
  const initialization_code_utils_1 = require("./utils/initialization-code-utils");
52
52
  const script_parser_utils_1 = require("./utils/script-parser-utils");
53
53
  const script_generator_utils_1 = require("./utils/script-generator-utils");
54
+ const pom_denormalizer_1 = require("./utils/pom-denormalizer");
54
55
  const progress_reporter_1 = require("./progress-reporter");
55
56
  const backend_proxy_llm_provider_1 = require("./providers/backend-proxy-llm-provider");
56
57
  const build_info_1 = require("./build-info");
@@ -645,12 +646,71 @@ class ExecutionService {
645
646
  * Variables persist across steps without re-execution
646
647
  */
647
648
  async executeStepsInPersistentContext(steps, page, browser, browserContext, options, sharedContext) {
648
- this.log(`[executeStepsInPersistentContext] Called with ${steps.length} steps, mode=${options.mode}, originalScript=${!!options.originalScript}, tempDir=${!!options.tempDir}, sharedContext=${!!sharedContext}`);
649
+ this.log(`[executeStepsInPersistentContext] Called with ${steps.length} steps, mode=${options.mode}, originalScript=${!!options.originalScript}, tempDir=${!!options.tempDir}, sharedContext=${!!sharedContext}, runPomDenormalized=${!!options.runPomDenormalized}`);
649
650
  const { expect, test } = require('@playwright/test');
650
651
  const { ai } = require('ai-wright');
651
652
  // Use shared context if provided, otherwise create new context
652
653
  const persistentContext = sharedContext || new PersistentExecutionContext(page, expect, test, ai, browser, browserContext, options.tempDir);
653
654
  const isSharedContext = !!sharedContext;
655
+ const resolvedTestFileDir = (() => {
656
+ if (!options.tempDir) {
657
+ return undefined;
658
+ }
659
+ if (options.testFolderPath && options.testFolderPath.length > 0) {
660
+ return path.join(options.tempDir, ...options.testFolderPath);
661
+ }
662
+ const defaultDir = path.join(options.tempDir, 'tests');
663
+ try {
664
+ const testsDir = defaultDir;
665
+ const fs = require('fs');
666
+ if (!fs.existsSync(testsDir)) {
667
+ this.log(`Tests directory ${testsDir} does not exist, using default for module resolution`, 'warn');
668
+ return defaultDir;
669
+ }
670
+ const findTestFile = (dir, depth = 0, maxDepth = 10) => {
671
+ if (depth > maxDepth) {
672
+ return null;
673
+ }
674
+ try {
675
+ const files = fs.readdirSync(dir);
676
+ for (const file of files) {
677
+ const fullPath = path.join(dir, file);
678
+ try {
679
+ const stat = fs.statSync(fullPath);
680
+ if (stat.isDirectory()) {
681
+ const found = findTestFile(fullPath, depth + 1, maxDepth);
682
+ if (found)
683
+ return found;
684
+ }
685
+ else if (file.endsWith('.spec.ts') || file.endsWith('.spec.js') ||
686
+ file.endsWith('.test.ts') || file.endsWith('.test.js')) {
687
+ return fullPath;
688
+ }
689
+ }
690
+ catch (statError) {
691
+ continue;
692
+ }
693
+ }
694
+ }
695
+ catch (e) {
696
+ return null;
697
+ }
698
+ return null;
699
+ };
700
+ const testFilePath = findTestFile(testsDir);
701
+ if (testFilePath) {
702
+ const testFileDir = path.dirname(testFilePath);
703
+ this.log(`Found test file at ${testFilePath}, using directory ${testFileDir} for module resolution`);
704
+ return testFileDir;
705
+ }
706
+ this.log(`No test file found in ${testsDir}, using tests directory for module resolution`, 'warn');
707
+ return testsDir;
708
+ }
709
+ catch (searchError) {
710
+ this.log(`Could not search for test file: ${searchError.message}, using tests directory for module resolution`, 'warn');
711
+ return defaultDir;
712
+ }
713
+ })();
654
714
  // Extract and execute imports from original script if provided
655
715
  this.log(`[executeStepsInPersistentContext] Checking imports: originalScript=${!!options.originalScript}, tempDir=${!!options.tempDir}`);
656
716
  if (options.originalScript && options.tempDir) {
@@ -664,60 +724,7 @@ class ExecutionService {
664
724
  // Execute imports with proper context - find the actual test file path
665
725
  // All tests are within the tests folder
666
726
  // We search for test files in tempDir/tests directory tree
667
- let testFileDir = path.join(options.tempDir, 'tests'); // Default to tests directory
668
- try {
669
- const fs = require('fs');
670
- const testsDir = path.join(options.tempDir, 'tests');
671
- // Check if tests directory exists
672
- if (!fs.existsSync(testsDir)) {
673
- this.log(`Tests directory ${testsDir} does not exist, using default for module resolution`, 'warn');
674
- }
675
- else {
676
- // Recursive search with depth limit to prevent infinite loops
677
- const findTestFile = (dir, depth = 0, maxDepth = 10) => {
678
- if (depth > maxDepth) {
679
- return null; // Prevent infinite recursion
680
- }
681
- try {
682
- const files = fs.readdirSync(dir);
683
- for (const file of files) {
684
- const fullPath = path.join(dir, file);
685
- try {
686
- const stat = fs.statSync(fullPath);
687
- if (stat.isDirectory()) {
688
- const found = findTestFile(fullPath, depth + 1, maxDepth);
689
- if (found)
690
- return found;
691
- }
692
- else if (file.endsWith('.spec.ts') || file.endsWith('.spec.js') ||
693
- file.endsWith('.test.ts') || file.endsWith('.test.js')) {
694
- return fullPath; // Found a test file
695
- }
696
- }
697
- catch (statError) {
698
- // Skip files we can't stat
699
- continue;
700
- }
701
- }
702
- }
703
- catch (e) {
704
- // Continue searching in other directories
705
- }
706
- return null;
707
- };
708
- const testFilePath = findTestFile(testsDir);
709
- if (testFilePath) {
710
- testFileDir = path.dirname(testFilePath);
711
- this.log(`Found test file at ${testFilePath}, using directory ${testFileDir} for module resolution`);
712
- }
713
- else {
714
- this.log(`No test file found in ${testsDir}, using tests directory for module resolution`, 'warn');
715
- }
716
- }
717
- }
718
- catch (searchError) {
719
- this.log(`Could not search for test file: ${searchError.message}, using tests directory for module resolution`, 'warn');
720
- }
727
+ const testFileDir = resolvedTestFileDir || path.join(options.tempDir, 'tests');
721
728
  // Store test file directory in context for module resolution
722
729
  persistentContext.setTestFileDir(testFileDir);
723
730
  this.log(`Module resolution context: testFileDir=${testFileDir}, tempDir=${options.tempDir}`);
@@ -945,6 +952,8 @@ class ExecutionService {
945
952
  isVariableDeclaration,
946
953
  intentComment: step.description !== step.code ? step.description : undefined,
947
954
  screenStateAnnotation: undefined, // Will be in description for pre-parsed steps
955
+ scenarioAnnotation: step.scenarioAnnotation,
956
+ scenarioAnnotationLine: undefined,
948
957
  stepId: step.id // Preserve stepId (should be hash-based for test body statements)
949
958
  };
950
959
  });
@@ -963,6 +972,80 @@ class ExecutionService {
963
972
  }
964
973
  return { success: false, error: 'No script or steps provided for execution' };
965
974
  }
975
+ if (options.runPomDenormalized && options.originalScript) {
976
+ const originalCount = allStatements.length;
977
+ const originalStepIds = allStatements
978
+ .map((stmt, idx) => ({ idx, stepId: stmt.stepId, code: stmt.code.substring(0, 50) }))
979
+ .filter(s => s.stepId);
980
+ this.log(`[executeStepsInPersistentContext] Starting POM denormalization: ${originalCount} original statements, ` +
981
+ `${originalStepIds.length} with stepIds`);
982
+ // Convert to ScriptStep[] for denormalization
983
+ const scriptSteps = allStatements.map(stmt => ({
984
+ code: stmt.code,
985
+ description: stmt.intentComment || '',
986
+ id: stmt.stepId, // stepId may not exist for InitializationCodeUtils results
987
+ isVariableDeclaration: stmt.isVariableDeclaration
988
+ }));
989
+ // Denormalize POM calls
990
+ const denormalizedSteps = pom_denormalizer_1.PomDenormalizer.denormalizePomSteps(scriptSteps, {
991
+ originalScript: options.originalScript,
992
+ tempDir: options.tempDir || '',
993
+ testFileDir: resolvedTestFileDir,
994
+ logger: (msg, level) => this.log(msg, level)
995
+ });
996
+ const denormalizedCount = denormalizedSteps.length;
997
+ const denormalizedStepIds = denormalizedSteps
998
+ .map((step, idx) => ({ idx, stepId: step.id, isVar: step.isVariableDeclaration, code: step.code.substring(0, 50) }))
999
+ .filter(s => s.stepId && !s.isVar);
1000
+ this.log(`[executeStepsInPersistentContext] Denormalization complete: ${originalCount} -> ${denormalizedCount} statements ` +
1001
+ `(${denormalizedCount - originalCount > 0 ? '+' : ''}${denormalizedCount - originalCount}), ` +
1002
+ `${denormalizedStepIds.length} non-variable steps with stepIds`);
1003
+ if (denormalizedStepIds.length > 0 && this.log) {
1004
+ const stepIdPreview = denormalizedStepIds.slice(0, 5).map(s => `idx=${s.idx} stepId=${s.stepId?.substring(0, 16)}...`).join(', ');
1005
+ this.log(`[executeStepsInPersistentContext] Sample denormalized stepIds: ${stepIdPreview}${denormalizedStepIds.length > 5 ? '...' : ''}`);
1006
+ }
1007
+ // Convert back to inline type format
1008
+ allStatements = denormalizedSteps.map(step => ({
1009
+ code: step.code,
1010
+ isVariableDeclaration: step.isVariableDeclaration || false,
1011
+ intentComment: step.description !== step.code ? step.description : undefined,
1012
+ screenStateAnnotation: undefined,
1013
+ scenarioAnnotation: step.scenarioAnnotation,
1014
+ scenarioAnnotationLine: undefined,
1015
+ stepId: step.id
1016
+ }));
1017
+ // Verify stepId preservation
1018
+ const preservedStepIds = allStatements
1019
+ .map((stmt, idx) => ({ idx, stepId: stmt.stepId, isVar: stmt.isVariableDeclaration }))
1020
+ .filter(s => s.stepId && !s.isVar);
1021
+ this.log(`[executeStepsInPersistentContext] StepId preservation: ${preservedStepIds.length} non-variable statements have stepIds ` +
1022
+ `(expected: ${denormalizedStepIds.length})`);
1023
+ // Verify stepId matching between denormalized and preserved
1024
+ if (preservedStepIds.length !== denormalizedStepIds.length) {
1025
+ this.log(`[executeStepsInPersistentContext] WARNING: StepId count mismatch! ` +
1026
+ `Denormalized: ${denormalizedStepIds.length}, Preserved: ${preservedStepIds.length}`, 'warn');
1027
+ }
1028
+ else {
1029
+ // Check if stepIds match
1030
+ let matchCount = 0;
1031
+ for (let i = 0; i < Math.min(denormalizedStepIds.length, preservedStepIds.length); i++) {
1032
+ const denorm = denormalizedStepIds[i];
1033
+ const preserved = preservedStepIds.find(p => p.idx === denorm.idx);
1034
+ if (preserved && preserved.stepId === denorm.stepId) {
1035
+ matchCount++;
1036
+ }
1037
+ }
1038
+ this.log(`[executeStepsInPersistentContext] StepId matching: ${matchCount}/${Math.min(denormalizedStepIds.length, preservedStepIds.length)} stepIds match`);
1039
+ }
1040
+ }
1041
+ else {
1042
+ if (!options.runPomDenormalized) {
1043
+ this.log(`[executeStepsInPersistentContext] POM denormalization skipped: runPomDenormalized=false`);
1044
+ }
1045
+ else if (!options.originalScript) {
1046
+ this.log(`[executeStepsInPersistentContext] POM denormalization skipped: originalScript not provided`);
1047
+ }
1048
+ }
966
1049
  // Build stepId -> statement index map before execution
967
1050
  const statementIndexByStepId = new Map();
968
1051
  for (let idx = 0; idx < allStatements.length; idx++) {
@@ -1012,6 +1095,54 @@ class ExecutionService {
1012
1095
  this.log(`[executeStepsInPersistentContext] Jumping to step index ${nextIndex} (stepId: ${normalizedStepId})${normalizedNote} [${contextLabel}]`);
1013
1096
  return targetIndex;
1014
1097
  };
1098
+ // Scenario coverage tracking (@Scenario annotations)
1099
+ const scenarioTitlesOrdered = [];
1100
+ const scenarioTitleToIndex = new Map();
1101
+ const scenarioBoundaryByStatementIndex = new Map();
1102
+ for (let idx = 0; idx < allStatements.length; idx++) {
1103
+ const scenarioTitle = allStatements[idx].scenarioAnnotation?.trim();
1104
+ if (!scenarioTitle) {
1105
+ continue;
1106
+ }
1107
+ if (scenarioTitleToIndex.has(scenarioTitle)) {
1108
+ continue; // dedupe by title (ignore subsequent occurrences)
1109
+ }
1110
+ scenarioTitleToIndex.set(scenarioTitle, scenarioTitlesOrdered.length);
1111
+ scenarioTitlesOrdered.push(scenarioTitle);
1112
+ scenarioBoundaryByStatementIndex.set(idx, scenarioTitle);
1113
+ }
1114
+ let currentScenarioTitle;
1115
+ let currentScenarioIndex = -1;
1116
+ const completedScenarioTitles = new Set();
1117
+ const emitScenarioCompletion = async (title, status) => {
1118
+ if (!title || completedScenarioTitles.has(title)) {
1119
+ return;
1120
+ }
1121
+ completedScenarioTitles.add(title);
1122
+ if (this.progressReporter?.onScenarioCompletion) {
1123
+ await this.progressReporter.onScenarioCompletion(title, status);
1124
+ }
1125
+ };
1126
+ const finalizeScenarioFailure = async () => {
1127
+ if (scenarioTitlesOrdered.length === 0) {
1128
+ return;
1129
+ }
1130
+ if (!currentScenarioTitle) {
1131
+ for (const title of scenarioTitlesOrdered) {
1132
+ await emitScenarioCompletion(title, progress_reporter_1.ScenarioCoverageStatus.NOT_ATTEMPTED);
1133
+ }
1134
+ return;
1135
+ }
1136
+ await emitScenarioCompletion(currentScenarioTitle, progress_reporter_1.ScenarioCoverageStatus.FAILED);
1137
+ for (let idx = currentScenarioIndex + 1; idx < scenarioTitlesOrdered.length; idx++) {
1138
+ await emitScenarioCompletion(scenarioTitlesOrdered[idx], progress_reporter_1.ScenarioCoverageStatus.NOT_ATTEMPTED);
1139
+ }
1140
+ };
1141
+ const finalizeScenarioSuccess = async () => {
1142
+ if (currentScenarioTitle) {
1143
+ await emitScenarioCompletion(currentScenarioTitle, progress_reporter_1.ScenarioCoverageStatus.SUCCESSFUL);
1144
+ }
1145
+ };
1015
1146
  // Execute all statements sequentially
1016
1147
  // Variable declarations execute silently, other statements trigger callbacks
1017
1148
  let stepNumber = 0; // Track step number (excludes variables)
@@ -1046,6 +1177,17 @@ class ExecutionService {
1046
1177
  i = jumpIndexBefore;
1047
1178
  continue;
1048
1179
  }
1180
+ // Scenario boundary handling (before executing statement)
1181
+ const scenarioBoundaryTitle = scenarioBoundaryByStatementIndex.get(i);
1182
+ if (scenarioBoundaryTitle) {
1183
+ if (currentScenarioTitle && currentScenarioTitle !== scenarioBoundaryTitle) {
1184
+ await emitScenarioCompletion(currentScenarioTitle, progress_reporter_1.ScenarioCoverageStatus.SUCCESSFUL);
1185
+ }
1186
+ if (!completedScenarioTitles.has(scenarioBoundaryTitle)) {
1187
+ currentScenarioTitle = scenarioBoundaryTitle;
1188
+ currentScenarioIndex = scenarioTitleToIndex.get(scenarioBoundaryTitle) ?? -1;
1189
+ }
1190
+ }
1049
1191
  // Check cancellation
1050
1192
  if (this.progressReporter?.shouldContinue && options.jobId) {
1051
1193
  const shouldContinue = await this.progressReporter.shouldContinue(options.jobId);
@@ -1079,14 +1221,6 @@ class ExecutionService {
1079
1221
  let shouldSkipStep = false;
1080
1222
  if (this.progressReporter?.beforeStepStart && options.jobId) {
1081
1223
  const { description, baseStepId, stepId, runNumber } = stepMeta;
1082
- // Log stepId usage for debugging
1083
- if (stmt.stepId) {
1084
- const stepIdPreview = stmt.stepId.length > 16 ? `${stmt.stepId.substring(0, 16)}...` : stmt.stepId;
1085
- this.log(`[stepId] beforeStepStart: Using hash-based stepId: ${stepIdPreview} (stepNumber: ${stepNumber}, run: ${runNumber})`);
1086
- }
1087
- else {
1088
- this.log(`[stepId] beforeStepStart: WARNING: No hash-based stepId, using fallback: ${baseStepId} (stepNumber: ${stepNumber}, run: ${runNumber})`);
1089
- }
1090
1224
  const beforeStepResult = await this.progressReporter.beforeStepStart({
1091
1225
  stepId,
1092
1226
  stepNumber,
@@ -1215,6 +1349,7 @@ class ExecutionService {
1215
1349
  if (!isSharedContext) {
1216
1350
  persistentContext.dispose();
1217
1351
  }
1352
+ await finalizeScenarioFailure();
1218
1353
  return { success: false, failedStepIndex: i, error: errorMsg };
1219
1354
  }
1220
1355
  else {
@@ -1270,6 +1405,7 @@ class ExecutionService {
1270
1405
  if (!isSharedContext) {
1271
1406
  persistentContext.dispose();
1272
1407
  }
1408
+ await finalizeScenarioFailure();
1273
1409
  return { success: false, failedStepIndex: i, error: errorMsg };
1274
1410
  }
1275
1411
  }
@@ -1279,6 +1415,7 @@ class ExecutionService {
1279
1415
  if (!isSharedContext) {
1280
1416
  persistentContext.dispose();
1281
1417
  }
1418
+ await finalizeScenarioFailure();
1282
1419
  return { success: false, failedStepIndex: i, error: `Variable declaration failed: ${errorMsg}` };
1283
1420
  }
1284
1421
  }
@@ -1299,6 +1436,7 @@ class ExecutionService {
1299
1436
  code: s.code,
1300
1437
  success: true
1301
1438
  }));
1439
+ await finalizeScenarioSuccess();
1302
1440
  return { success: true, updatedSteps };
1303
1441
  }
1304
1442
  /**
@@ -1401,12 +1539,18 @@ class ExecutionService {
1401
1539
  }
1402
1540
  // Execute script in persistent context (STOP on first error)
1403
1541
  // Pre-parsed steps from consumer (if any) will be ignored - AST extracts from script
1542
+ this.log(`[runExactly] Calling executeStepsInPersistentContext (existing browser): ` +
1543
+ `runPomDenormalized=${request.runPomDenormalized}, ` +
1544
+ `originalScript=${!!request.script} (length: ${request.script?.length || 0}), ` +
1545
+ `tempDir=${!!request.tempDir}`);
1404
1546
  const result = await this.executeStepsInPersistentContext(request.steps || [], // Pre-parsed steps (optional, for backward compatibility)
1405
1547
  page, browser, context, {
1406
1548
  mode: 'RUN_EXACTLY',
1407
1549
  jobId: request.jobId,
1408
1550
  tempDir: request.tempDir,
1409
- originalScript: request.script // AST will extract statements from this
1551
+ originalScript: request.script, // AST will extract statements from this
1552
+ testFolderPath: request.testFolderPath,
1553
+ runPomDenormalized: request.runPomDenormalized
1410
1554
  });
1411
1555
  // LIFECYCLE: afterEndTest
1412
1556
  if (this.progressReporter?.afterEndTest) {
@@ -1454,12 +1598,18 @@ class ExecutionService {
1454
1598
  }
1455
1599
  // Execute script in persistent context (STOP on first error)
1456
1600
  // Pre-parsed steps from consumer (if any) will be ignored - AST extracts from script
1601
+ this.log(`[runExactly] Calling executeStepsInPersistentContext (new browser, attempt ${attempt}): ` +
1602
+ `runPomDenormalized=${request.runPomDenormalized}, ` +
1603
+ `originalScript=${!!request.script} (length: ${request.script?.length || 0}), ` +
1604
+ `tempDir=${!!request.tempDir}`);
1457
1605
  const result = await this.executeStepsInPersistentContext(request.steps || [], // Pre-parsed steps (optional, for backward compatibility)
1458
1606
  page, browser, context, {
1459
1607
  mode: 'RUN_EXACTLY',
1460
1608
  jobId: request.jobId,
1461
1609
  tempDir: request.tempDir,
1462
- originalScript: request.script // AST will extract statements from this
1610
+ originalScript: request.script, // AST will extract statements from this
1611
+ testFolderPath: request.testFolderPath,
1612
+ runPomDenormalized: request.runPomDenormalized
1463
1613
  });
1464
1614
  // LIFECYCLE: afterEndTest
1465
1615
  if (this.progressReporter?.afterEndTest) {
@@ -1575,7 +1725,9 @@ class ExecutionService {
1575
1725
  jobId: request.jobId,
1576
1726
  model,
1577
1727
  tempDir: request.tempDir,
1578
- originalScript: request.script // Pass original script for import extraction
1728
+ originalScript: request.script, // Pass original script for import extraction
1729
+ testFolderPath: request.testFolderPath,
1730
+ runPomDenormalized: request.runPomDenormalized
1579
1731
  });
1580
1732
  const updatedSteps = result.updatedSteps || steps;
1581
1733
  const allStepsSuccessful = result.success;
@@ -1886,7 +2038,7 @@ class ExecutionService {
1886
2038
  this.log(`Executing ${flattened.fileLevelHooks.beforeAll.length} file-level beforeAll hook(s)...`);
1887
2039
  try {
1888
2040
  for (const hook of flattened.fileLevelHooks.beforeAll) {
1889
- await this.executeHook(hook.code, page, context, browser, request.tempDir, originalScript, fileLevelContext || undefined, hook);
2041
+ await this.executeHook(hook.code, page, context, browser, request.tempDir, originalScript, request.testFolderPath, request.runPomDenormalized, fileLevelContext || undefined, hook);
1890
2042
  }
1891
2043
  this.log('File-level beforeAll hooks executed successfully');
1892
2044
  }
@@ -1957,7 +2109,7 @@ class ExecutionService {
1957
2109
  // Get or create suite context (includes file variables + suite variables)
1958
2110
  const suiteContext = await getOrCreateSuiteContext(suitePath, suite.suiteVariables, flattened.fileVariables);
1959
2111
  for (const hook of suite.beforeAll) {
1960
- await this.executeHook(hook.code, page, context, browser, request.tempDir, originalScript, suiteContext, hook);
2112
+ await this.executeHook(hook.code, page, context, browser, request.tempDir, originalScript, request.testFolderPath, request.runPomDenormalized, suiteContext, hook);
1961
2113
  }
1962
2114
  suiteBeforeAllExecuted.add(suiteKey);
1963
2115
  this.log(`Suite-level beforeAll hooks executed for suite: ${suiteDisplayName}`);
@@ -1982,7 +2134,7 @@ class ExecutionService {
1982
2134
  this.log(`Executing ${flattened.fileLevelHooks.beforeEach.length} file-level beforeEach hook(s) for test: ${test.fullName}`);
1983
2135
  try {
1984
2136
  for (const hook of flattened.fileLevelHooks.beforeEach) {
1985
- const stepsExecuted = await this.executeHook(hook.code, page, context, browser, request.tempDir, originalScript, fileLevelContext || undefined, hook, jobId, testStepCounter);
2137
+ const stepsExecuted = await this.executeHook(hook.code, page, context, browser, request.tempDir, originalScript, request.testFolderPath, request.runPomDenormalized, fileLevelContext || undefined, hook, jobId, testStepCounter);
1986
2138
  testStepCounter += stepsExecuted;
1987
2139
  }
1988
2140
  }
@@ -2000,7 +2152,7 @@ class ExecutionService {
2000
2152
  // Get or create suite context for this test's suite
2001
2153
  const suiteContext = await getOrCreateSuiteContext(testData.suitePath, testData.suiteVariables, flattened.fileVariables);
2002
2154
  for (const hook of testData.suiteBeforeEachHooks) {
2003
- const stepsExecuted = await this.executeHook(hook.code, page, context, browser, request.tempDir, originalScript, suiteContext, hook, jobId, testStepCounter);
2155
+ const stepsExecuted = await this.executeHook(hook.code, page, context, browser, request.tempDir, originalScript, request.testFolderPath, request.runPomDenormalized, suiteContext, hook, jobId, testStepCounter);
2004
2156
  testStepCounter += stepsExecuted;
2005
2157
  }
2006
2158
  }
@@ -2044,7 +2196,8 @@ class ExecutionService {
2044
2196
  id: stepId,
2045
2197
  code: stmt.code,
2046
2198
  description: stmt.intentComment || stmt.code.trim().substring(0, 100) || '',
2047
- isVariableDeclaration: stmt.isVariableDeclaration // Pass AST-based detection flag
2199
+ isVariableDeclaration: stmt.isVariableDeclaration, // Pass AST-based detection flag
2200
+ scenarioAnnotation: stmt.scenarioAnnotation
2048
2201
  };
2049
2202
  });
2050
2203
  // Get or create suite context for this test's suite
@@ -2061,7 +2214,9 @@ class ExecutionService {
2061
2214
  jobId: jobId,
2062
2215
  tempDir: request.tempDir,
2063
2216
  originalScript: testScript, // For module resolution (imports)
2064
- model: request.model
2217
+ model: request.model,
2218
+ testFolderPath: request.testFolderPath,
2219
+ runPomDenormalized: request.runPomDenormalized
2065
2220
  }, suiteContext // Pass shared context
2066
2221
  );
2067
2222
  if (!result.success) {
@@ -2110,7 +2265,7 @@ class ExecutionService {
2110
2265
  // Get or create suite context for this test's suite
2111
2266
  const suiteContext = await getOrCreateSuiteContext(testData.suitePath, testData.suiteVariables, flattened.fileVariables);
2112
2267
  for (const hook of testData.suiteAfterEachHooks) {
2113
- const stepsExecuted = await this.executeHook(hook.code, page, context, browser, request.tempDir, originalScript, suiteContext, hook, jobId, testStepCounter);
2268
+ const stepsExecuted = await this.executeHook(hook.code, page, context, browser, request.tempDir, originalScript, request.testFolderPath, request.runPomDenormalized, suiteContext, hook, jobId, testStepCounter);
2114
2269
  testStepCounter += stepsExecuted;
2115
2270
  }
2116
2271
  }
@@ -2125,7 +2280,7 @@ class ExecutionService {
2125
2280
  this.log(`Executing ${flattened.fileLevelHooks.afterEach.length} file-level afterEach hook(s) for test: ${test.fullName}`);
2126
2281
  try {
2127
2282
  for (const hook of flattened.fileLevelHooks.afterEach) {
2128
- const stepsExecuted = await this.executeHook(hook.code, page, context, browser, request.tempDir, originalScript, fileLevelContext || undefined, hook, jobId, testStepCounter);
2283
+ const stepsExecuted = await this.executeHook(hook.code, page, context, browser, request.tempDir, originalScript, request.testFolderPath, request.runPomDenormalized, fileLevelContext || undefined, hook, jobId, testStepCounter);
2129
2284
  testStepCounter += stepsExecuted;
2130
2285
  }
2131
2286
  }
@@ -2164,7 +2319,7 @@ class ExecutionService {
2164
2319
  // Get or create suite context (should already exist, but safe to call)
2165
2320
  const suiteContext = await getOrCreateSuiteContext(suitePath, suite.suiteVariables, flattened.fileVariables);
2166
2321
  for (const hook of suite.afterAll) {
2167
- await this.executeHook(hook.code, page, context, browser, request.tempDir, originalScript, suiteContext, hook);
2322
+ await this.executeHook(hook.code, page, context, browser, request.tempDir, originalScript, request.testFolderPath, request.runPomDenormalized, suiteContext, hook);
2168
2323
  }
2169
2324
  this.log(`Suite-level afterAll hooks executed for suite: ${suiteDisplayName}`);
2170
2325
  }
@@ -2229,7 +2384,7 @@ class ExecutionService {
2229
2384
  this.log(`Executing ${flattened.fileLevelHooks.afterAll.length} file-level afterAll hook(s)...`);
2230
2385
  try {
2231
2386
  for (const hook of flattened.fileLevelHooks.afterAll) {
2232
- await this.executeHook(hook.code, page, context, browser, request.tempDir, originalScript, fileLevelContext || undefined, hook);
2387
+ await this.executeHook(hook.code, page, context, browser, request.tempDir, originalScript, request.testFolderPath, request.runPomDenormalized, fileLevelContext || undefined, hook);
2233
2388
  }
2234
2389
  this.log('File-level afterAll hooks executed successfully');
2235
2390
  }
@@ -2295,7 +2450,7 @@ class ExecutionService {
2295
2450
  * If jobId is provided, executes step-wise with callbacks for step reporting
2296
2451
  * @returns Number of non-variable steps executed (for step counter tracking)
2297
2452
  */
2298
- async executeHook(hookCode, page, context, browser, tempDir, originalScript, sharedContext, hook, jobId, stepIdOffset = 0) {
2453
+ async executeHook(hookCode, page, context, browser, tempDir, originalScript, testFolderPath, runPomDenormalized, sharedContext, hook, jobId, stepIdOffset = 0) {
2299
2454
  const { expect, test } = require('@playwright/test');
2300
2455
  const { ai } = require('ai-wright');
2301
2456
  // Construct hook script with imports from original script (for module resolution)
@@ -2334,7 +2489,9 @@ class ExecutionService {
2334
2489
  mode: types_1.ExecutionMode.RUN_EXACTLY,
2335
2490
  jobId: jobId,
2336
2491
  tempDir: tempDir,
2337
- originalScript: hookScript
2492
+ originalScript: hookScript,
2493
+ testFolderPath: testFolderPath,
2494
+ runPomDenormalized: runPomDenormalized
2338
2495
  }, hookContext // Pass shared context
2339
2496
  );
2340
2497
  if (!result.success) {