testchimp-runner-core 0.0.75 → 0.0.77

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.
@@ -36,28 +36,184 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.ExecutionService = void 0;
37
37
  const vm = __importStar(require("vm"));
38
38
  const path = __importStar(require("path"));
39
+ const async_hooks_1 = require("async_hooks");
39
40
  const playwright_mcp_service_1 = require("./playwright-mcp-service");
40
41
  const types_1 = require("./types");
41
42
  const browser_utils_1 = require("./utils/browser-utils");
42
43
  const llm_facade_1 = require("./llm-facade");
43
- const script_utils_1 = require("./script-utils");
44
44
  const credit_usage_service_1 = require("./credit-usage-service");
45
45
  const model_constants_1 = require("./model-constants");
46
46
  const ai_command_utils_1 = require("./utils/ai-command-utils");
47
47
  const playwright_runner_1 = require("./utils/playwright-runner");
48
+ const import_utils_1 = require("./utils/import-utils");
49
+ const initialization_code_utils_1 = require("./utils/initialization-code-utils");
50
+ const script_parser_utils_1 = require("./utils/script-parser-utils");
51
+ const step_execution_utils_1 = require("./utils/step-execution-utils");
52
+ const script_generator_utils_1 = require("./utils/script-generator-utils");
48
53
  const backend_proxy_llm_provider_1 = require("./providers/backend-proxy-llm-provider");
49
54
  const build_info_1 = require("./build-info");
50
55
  const orchestrator_1 = require("./orchestrator");
56
+ class ModuleResolutionManager {
57
+ /**
58
+ * Install the global hook (only once, shared by all instances)
59
+ */
60
+ static installHook() {
61
+ if (this.hookInstalled) {
62
+ return; // Already installed
63
+ }
64
+ const Module = require('module');
65
+ const path = require('path');
66
+ const fs = require('fs');
67
+ this.originalResolveFilename = Module._resolveFilename;
68
+ // Cache scriptservice node_modules path from execution-service.js location
69
+ // execution-service.js is at: .../scriptservice/node_modules/testchimp-runner-core/dist/execution-service.js
70
+ // Go up 2 levels to get to scriptservice/node_modules
71
+ try {
72
+ const executionServicePath = __filename; // This file's path
73
+ const distDir = path.dirname(executionServicePath);
74
+ const runnerCoreDir = path.dirname(distDir);
75
+ const nodeModulesDir = path.dirname(runnerCoreDir);
76
+ if (fs.existsSync(nodeModulesDir) && path.basename(nodeModulesDir) === 'node_modules') {
77
+ this.scriptserviceNodeModules = nodeModulesDir;
78
+ console.log(`[ModuleResolution] Cached scriptservice node_modules: ${this.scriptserviceNodeModules}`);
79
+ }
80
+ }
81
+ catch (e) {
82
+ // Ignore errors
83
+ }
84
+ // Install hook that uses AsyncLocalStorage to get the current tempDir and testFileDir
85
+ Module._resolveFilename = function (request, parent, isMain, options) {
86
+ // Get resolution context for current execution
87
+ const context = ModuleResolutionManager.asyncLocalStorage.getStore();
88
+ // If no context, use original resolution (for requires outside our execution context)
89
+ if (!context) {
90
+ return ModuleResolutionManager.originalResolveFilename.call(this, request, parent, isMain, options);
91
+ }
92
+ // Log all module resolution attempts when we have context
93
+ console.log(`[ModuleResolution] Hook intercepted: "${request}" (parent: ${parent?.filename || 'none'})`);
94
+ // Handle relative imports
95
+ if (request.startsWith('.')) {
96
+ // Use standard Node.js relative path resolution
97
+ // The folder structure is exactly as specified in the DB (folderPath)
98
+ // Resolve relative to testFileDir (the directory where the test file is located)
99
+ const resolveDir = context.testFileDir || (parent?.filename ? path.dirname(parent.filename) : context.tempDir);
100
+ // Log module resolution attempt
101
+ console.log(`[ModuleResolution] Resolving relative import: "${request}"`);
102
+ console.log(`[ModuleResolution] resolveDir: ${resolveDir}`);
103
+ // Resolve the relative path using standard Node.js resolution
104
+ // This will correctly resolve based on the actual folder structure in tempDir
105
+ const resolvedPath = path.resolve(resolveDir, request);
106
+ console.log(`[ModuleResolution] resolvedPath: ${resolvedPath}`);
107
+ // Try extensions in order: .page.ts, .page.js (POM files)
108
+ const extensions = ['.page.ts', '.page.js'];
109
+ for (const ext of extensions) {
110
+ const testPath = resolvedPath + ext;
111
+ try {
112
+ if (fs.existsSync(testPath)) {
113
+ console.log(`[ModuleResolution] ✓ Found file: ${testPath}`);
114
+ return testPath;
115
+ }
116
+ else {
117
+ console.log(`[ModuleResolution] ✗ File does not exist: ${testPath}`);
118
+ }
119
+ }
120
+ catch (e) {
121
+ console.log(`[ModuleResolution] ✗ Error checking file: ${testPath} - ${e}`);
122
+ }
123
+ }
124
+ // If no extension worked, try original resolution
125
+ try {
126
+ return ModuleResolutionManager.originalResolveFilename.call(this, request, parent, isMain, options);
127
+ }
128
+ catch (err) {
129
+ // If that fails, try resolving as directory with index file
130
+ for (const ext of extensions) {
131
+ const indexPath = path.join(resolvedPath, 'index' + ext);
132
+ try {
133
+ if (fs.existsSync(indexPath)) {
134
+ return indexPath;
135
+ }
136
+ }
137
+ catch (e) {
138
+ // Continue
139
+ }
140
+ }
141
+ throw err;
142
+ }
143
+ }
144
+ // For non-relative imports (like 'ai-wright', '@playwright/test'), use standard Node.js resolution
145
+ // These should resolve from node_modules where runner-core is installed (scriptservice's node_modules)
146
+ console.log(`[ModuleResolution] Resolving non-relative import: "${request}"`);
147
+ try {
148
+ // Use original Node.js resolution (will find from node_modules)
149
+ const resolved = ModuleResolutionManager.originalResolveFilename.call(this, request, parent, isMain, options);
150
+ console.log(`[ModuleResolution] ✓ Resolved to: ${resolved}`);
151
+ return resolved;
152
+ }
153
+ catch (err) {
154
+ console.log(`[ModuleResolution] ✗ Failed to resolve: ${err.message}`);
155
+ // Build enhanced paths that include scriptservice's node_modules
156
+ // Find node_modules by looking up from the execution-service.js location
157
+ const enhancedPaths = [...(options?.paths || [])];
158
+ // Add tempDir paths
159
+ if (context.tempDir) {
160
+ enhancedPaths.push(context.tempDir);
161
+ enhancedPaths.push(path.join(context.tempDir, 'node_modules'));
162
+ }
163
+ // Add scriptservice's node_modules (where runner-core is installed)
164
+ // Use the cached path from execution-service.js location
165
+ if (ModuleResolutionManager.scriptserviceNodeModules) {
166
+ enhancedPaths.push(ModuleResolutionManager.scriptserviceNodeModules);
167
+ console.log(`[ModuleResolution] Added scriptservice node_modules to paths: ${ModuleResolutionManager.scriptserviceNodeModules}`);
168
+ }
169
+ // Try with enhanced paths
170
+ try {
171
+ const enhancedOptions = {
172
+ ...options,
173
+ paths: enhancedPaths
174
+ };
175
+ const resolved = ModuleResolutionManager.originalResolveFilename.call(this, request, parent, isMain, enhancedOptions);
176
+ console.log(`[ModuleResolution] ✓ Resolved (with enhanced paths): ${resolved}`);
177
+ return resolved;
178
+ }
179
+ catch (resolveErr) {
180
+ console.log(`[ModuleResolution] ✗ Failed with enhanced paths: ${resolveErr}`);
181
+ // Fall back to original error
182
+ throw err;
183
+ }
184
+ }
185
+ };
186
+ this.hookInstalled = true;
187
+ }
188
+ /**
189
+ * Run code in an isolated context with a specific tempDir
190
+ */
191
+ static runWithTempDir(tempDir, fn) {
192
+ return this.asyncLocalStorage.run({ tempDir }, fn);
193
+ }
194
+ static runWithContext(context, fn) {
195
+ return this.asyncLocalStorage.run(context, fn);
196
+ }
197
+ }
198
+ ModuleResolutionManager.asyncLocalStorage = new async_hooks_1.AsyncLocalStorage();
199
+ ModuleResolutionManager.hookInstalled = false;
200
+ ModuleResolutionManager.scriptserviceNodeModules = null;
51
201
  /**
52
202
  * Persistent execution context using vm.createContext
53
203
  * Maintains variable scope across multiple step executions
54
204
  * Variables defined in earlier steps remain available in later steps without re-evaluation
55
205
  */
56
206
  class PersistentExecutionContext {
57
- constructor(page, expect, test, ai, browser, browserContext) {
207
+ constructor(page, expect, test, ai, browser, browserContext, tempDir) {
208
+ this.tempDir = tempDir;
209
+ // Install global hook if not already installed (only happens once)
210
+ if (tempDir) {
211
+ ModuleResolutionManager.installHook();
212
+ this.setupTypeScriptSupport(tempDir);
213
+ }
58
214
  // Create V8 context with Playwright globals
59
215
  // ai-wright will handle timeout extension internally (test.setTimeout or page.setDefaultTimeout)
60
- this.context = vm.createContext({
216
+ const contextGlobals = {
61
217
  // Playwright objects
62
218
  page,
63
219
  expect,
@@ -67,15 +223,15 @@ class PersistentExecutionContext {
67
223
  context: browserContext,
68
224
  // Node.js globals
69
225
  console,
70
- require,
226
+ require, // Normal require - hook will intercept resolution
71
227
  setTimeout,
72
228
  setInterval,
73
229
  clearTimeout,
74
230
  clearInterval,
75
231
  Buffer,
76
232
  process,
77
- __dirname,
78
- __filename,
233
+ __dirname: tempDir || __dirname,
234
+ __filename: tempDir ? path.join(tempDir, 'script.ts') : __filename,
79
235
  // JavaScript built-ins
80
236
  Math,
81
237
  Date,
@@ -91,20 +247,87 @@ class PersistentExecutionContext {
91
247
  Map,
92
248
  WeakSet,
93
249
  WeakMap,
94
- });
250
+ globalThis: undefined, // Will be set after context creation
251
+ };
252
+ this.context = vm.createContext(contextGlobals);
253
+ // Set globalThis to point to the context object itself
254
+ // This allows assignments like globalThis.varName = value to work
255
+ contextGlobals.globalThis = this.context;
256
+ }
257
+ /**
258
+ * Set up TypeScript support by generating tsconfig.json and registering ts-node
259
+ */
260
+ setupTypeScriptSupport(tempDir) {
261
+ const path = require('path');
262
+ const fs = require('fs');
263
+ // Generate minimal tsconfig.json if it doesn't exist
264
+ const tsconfigPath = path.join(tempDir, 'tsconfig.json');
265
+ if (!fs.existsSync(tsconfigPath)) {
266
+ const tsconfig = {
267
+ compilerOptions: {
268
+ target: 'ES2020',
269
+ module: 'commonjs',
270
+ esModuleInterop: true,
271
+ skipLibCheck: true,
272
+ allowJs: true,
273
+ resolveJsonModule: true,
274
+ moduleResolution: 'node'
275
+ },
276
+ include: ['**/*.ts', '**/*.js'],
277
+ exclude: ['node_modules']
278
+ };
279
+ try {
280
+ fs.writeFileSync(tsconfigPath, JSON.stringify(tsconfig, null, 2));
281
+ }
282
+ catch (e) {
283
+ // If we can't write tsconfig, continue without it
284
+ // ts-node might still work with defaults
285
+ }
286
+ }
287
+ // Register ts-node with tempDir as project root
288
+ try {
289
+ require('ts-node').register({
290
+ project: tsconfigPath,
291
+ transpileOnly: true, // Faster, skip type checking
292
+ compilerOptions: {
293
+ module: 'commonjs',
294
+ esModuleInterop: true
295
+ }
296
+ });
297
+ }
298
+ catch (e) {
299
+ // ts-node not available or registration failed
300
+ // That's okay - will work for .js files, .ts files will fail with clear error
301
+ }
95
302
  }
96
303
  /**
97
304
  * Execute code in persistent context
98
305
  * Variables defined here persist for future executions
306
+ * Uses AsyncLocalStorage to ensure module resolution is isolated per execution
99
307
  */
100
308
  async executeCode(code) {
101
309
  // Wrap in async IIFE for await support
102
310
  const wrappedCode = `(async () => { ${code} })()`;
103
- // Compile and execute in persistent context
104
- const script = new vm.Script(wrappedCode);
105
- const result = script.runInContext(this.context);
106
- // Wait for async completion
107
- await result;
311
+ // If tempDir is set, run in isolated context with module resolution
312
+ if (this.tempDir) {
313
+ // Execute within AsyncLocalStorage context so module resolution hook can access tempDir
314
+ // Use the stored testFileDir if available, otherwise use tempDir
315
+ const context = {
316
+ tempDir: this.tempDir,
317
+ testFileDir: this.context.__testFileDir
318
+ };
319
+ await ModuleResolutionManager.runWithContext(context, async () => {
320
+ const script = new vm.Script(wrappedCode);
321
+ const result = script.runInContext(this.context);
322
+ await result;
323
+ });
324
+ }
325
+ else {
326
+ // No tempDir - execute normally without module resolution
327
+ const script = new vm.Script(wrappedCode);
328
+ const result = script.runInContext(this.context);
329
+ await result;
330
+ }
108
331
  }
109
332
  /**
110
333
  * Get list of user-defined variables (for debugging)
@@ -123,6 +346,8 @@ class PersistentExecutionContext {
123
346
  /**
124
347
  * Dispose the context (cleanup)
125
348
  * Called after test execution completes
349
+ * Note: Module resolution hook is shared and not restored here
350
+ * (it remains installed for other concurrent executions)
126
351
  */
127
352
  dispose() {
128
353
  // V8 will garbage collect the context when no longer referenced
@@ -368,10 +593,306 @@ class ExecutionService {
368
593
  * Variables persist across steps without re-execution
369
594
  */
370
595
  async executeStepsInPersistentContext(steps, page, browser, browserContext, options) {
596
+ this.log(`[executeStepsInPersistentContext] Called with ${steps.length} steps, mode=${options.mode}, originalScript=${!!options.originalScript}, tempDir=${!!options.tempDir}`);
371
597
  const { expect, test } = require('@playwright/test');
372
598
  const { ai } = require('ai-wright');
373
- // Create persistent execution context
374
- const persistentContext = new PersistentExecutionContext(page, expect, test, ai, browser, browserContext);
599
+ // Create persistent execution context with module resolution support
600
+ const persistentContext = new PersistentExecutionContext(page, expect, test, ai, browser, browserContext, options.tempDir);
601
+ // Extract and execute imports from original script if provided
602
+ this.log(`[executeStepsInPersistentContext] Checking imports: originalScript=${!!options.originalScript}, tempDir=${!!options.tempDir}`);
603
+ if (options.originalScript && options.tempDir) {
604
+ try {
605
+ this.log(`Extracting import statements from script (length: ${options.originalScript.length})`);
606
+ const importStatements = import_utils_1.ImportUtils.extractImportStatements(options.originalScript, (msg) => this.log(msg));
607
+ if (importStatements.length > 0) {
608
+ this.log(`Executing ${importStatements.length} import statement(s) before steps`);
609
+ // Convert ES6 imports to CommonJS requires and execute
610
+ const requireStatements = import_utils_1.ImportUtils.convertImportsToRequires(importStatements, (msg) => this.log(msg));
611
+ // Execute imports with proper context - find the actual test file path
612
+ // All tests are within the tests folder
613
+ // We search for test files in tempDir/tests directory tree
614
+ let testFileDir = path.join(options.tempDir, 'tests'); // Default to tests directory
615
+ try {
616
+ const fs = require('fs');
617
+ const testsDir = path.join(options.tempDir, 'tests');
618
+ // Check if tests directory exists
619
+ if (!fs.existsSync(testsDir)) {
620
+ this.log(`Tests directory ${testsDir} does not exist, using default for module resolution`, 'warn');
621
+ }
622
+ else {
623
+ // Recursive search with depth limit to prevent infinite loops
624
+ const findTestFile = (dir, depth = 0, maxDepth = 10) => {
625
+ if (depth > maxDepth) {
626
+ return null; // Prevent infinite recursion
627
+ }
628
+ try {
629
+ const files = fs.readdirSync(dir);
630
+ for (const file of files) {
631
+ const fullPath = path.join(dir, file);
632
+ try {
633
+ const stat = fs.statSync(fullPath);
634
+ if (stat.isDirectory()) {
635
+ const found = findTestFile(fullPath, depth + 1, maxDepth);
636
+ if (found)
637
+ return found;
638
+ }
639
+ else if (file.endsWith('.spec.ts') || file.endsWith('.spec.js') ||
640
+ file.endsWith('.test.ts') || file.endsWith('.test.js')) {
641
+ return fullPath; // Found a test file
642
+ }
643
+ }
644
+ catch (statError) {
645
+ // Skip files we can't stat
646
+ continue;
647
+ }
648
+ }
649
+ }
650
+ catch (e) {
651
+ // Continue searching in other directories
652
+ }
653
+ return null;
654
+ };
655
+ const testFilePath = findTestFile(testsDir);
656
+ if (testFilePath) {
657
+ testFileDir = path.dirname(testFilePath);
658
+ this.log(`Found test file at ${testFilePath}, using directory ${testFileDir} for module resolution`);
659
+ }
660
+ else {
661
+ this.log(`No test file found in ${testsDir}, using tests directory for module resolution`, 'warn');
662
+ }
663
+ }
664
+ }
665
+ catch (searchError) {
666
+ this.log(`Could not search for test file: ${searchError.message}, using tests directory for module resolution`, 'warn');
667
+ }
668
+ // Store test file directory in context for module resolution
669
+ persistentContext.context.__testFileDir = testFileDir;
670
+ this.log(`Module resolution context: testFileDir=${testFileDir}, tempDir=${options.tempDir}`);
671
+ this.log(`Executing require statements in persistent context...`);
672
+ try {
673
+ // Execute require statements - variables will be in the IIFE scope
674
+ // We need to explicitly assign them to the context so they're accessible later
675
+ // The issue is that const declarations in an IIFE don't leak to the outer context
676
+ // Solution: Execute the requires and then manually assign to context
677
+ const context = persistentContext.context;
678
+ // Execute require statements and capture results
679
+ // We execute each require and capture the module, then assign exports to context
680
+ // This approach is more robust - we execute the require and then extract what was imported
681
+ const requireLines = requireStatements.split('\n').filter(line => line.trim().length > 0);
682
+ const captureCodeParts = [];
683
+ const importMappings = [];
684
+ requireLines.forEach((line, idx) => {
685
+ // Extract module path
686
+ const modulePathMatch = line.match(/require\(['"]([^'"]+)['"]\)/);
687
+ if (!modulePathMatch)
688
+ return;
689
+ const modulePath = modulePathMatch[1];
690
+ // Extract variable names from the require statement
691
+ // Handle: const { X, Y } = require(...), const X = require(...), const * as X = require(...)
692
+ const varNames = [];
693
+ const namedMatch = line.match(/const\s+\{([^}]+)\}\s*=/);
694
+ const defaultMatch = line.match(/const\s+(\w+)\s*=/);
695
+ const namespaceMatch = line.match(/const\s+\*\s+as\s+(\w+)\s*=/);
696
+ if (namedMatch) {
697
+ // Named imports: const { X, Y } = require(...)
698
+ const names = namedMatch[1].split(',').map(n => n.trim().split(' as ')[0].trim());
699
+ varNames.push(...names);
700
+ }
701
+ else if (namespaceMatch) {
702
+ // Namespace import: const * as X = require(...)
703
+ varNames.push(namespaceMatch[1]);
704
+ }
705
+ else if (defaultMatch) {
706
+ // Default import: const X = require(...)
707
+ varNames.push(defaultMatch[1]);
708
+ }
709
+ importMappings.push({ line, modulePath, varNames });
710
+ // Generate code to execute require and capture exports
711
+ captureCodeParts.push(`
712
+ const _mod${idx} = require('${modulePath}');
713
+ if (_mod${idx} !== null && _mod${idx} !== undefined) {
714
+ ${varNames.map((varName, varIdx) => {
715
+ if (namedMatch) {
716
+ // Named import - get from module
717
+ return `_importResults['${varName}'] = _mod${idx}.${varName};`;
718
+ }
719
+ else if (namespaceMatch) {
720
+ // Namespace import - get entire module
721
+ return `_importResults['${varName}'] = _mod${idx};`;
722
+ }
723
+ else {
724
+ // Default import - get default or entire module
725
+ return `_importResults['${varName}'] = _mod${idx}.default !== undefined ? _mod${idx}.default : _mod${idx};`;
726
+ }
727
+ }).join('\n ')}
728
+ }
729
+ `);
730
+ });
731
+ const captureCode = `
732
+ (async () => {
733
+ const _importResults = {};
734
+ ${captureCodeParts.join('\n')}
735
+ return _importResults;
736
+ })()
737
+ `;
738
+ this.log(`Executing require statements with capture...`);
739
+ let importResults = {};
740
+ try {
741
+ if (options.tempDir) {
742
+ importResults = await ModuleResolutionManager.runWithContext({ tempDir: options.tempDir, testFileDir: context.__testFileDir }, async () => {
743
+ const script = new vm.Script(captureCode);
744
+ const result = script.runInContext(context);
745
+ return await result;
746
+ });
747
+ }
748
+ else {
749
+ const script = new vm.Script(captureCode);
750
+ const result = script.runInContext(context);
751
+ importResults = await result;
752
+ }
753
+ // Assign results to context
754
+ for (const [key, value] of Object.entries(importResults)) {
755
+ context[key] = value;
756
+ this.log(`Assigned ${key} to context`);
757
+ }
758
+ }
759
+ catch (importExecError) {
760
+ // If import execution fails (e.g., due to missing dependencies in POM files),
761
+ // try to execute imports individually to see which ones succeed
762
+ this.log(`Import execution failed: ${importExecError.message}. Attempting individual imports...`, 'warn');
763
+ // Execute each require statement individually and capture results
764
+ // This is more robust - we execute the require and capture all exports
765
+ const individualRequires = requireStatements.split('\n').filter(line => line.trim().length > 0);
766
+ for (const requireStmt of individualRequires) {
767
+ try {
768
+ // Execute the require statement and capture the module
769
+ // We'll extract the module path if possible, but if not, execute as-is
770
+ const modulePathMatch = requireStmt.match(/require\(['"]([^'"]+)['"]\)/);
771
+ const modulePath = modulePathMatch ? modulePathMatch[1] : null;
772
+ let moduleResult;
773
+ if (modulePath && options.tempDir) {
774
+ // Execute require for the specific module
775
+ moduleResult = await ModuleResolutionManager.runWithContext({ tempDir: options.tempDir, testFileDir: context.__testFileDir }, async () => {
776
+ const script = new vm.Script(`(() => require('${modulePath}'))()`);
777
+ return script.runInContext(context);
778
+ });
779
+ }
780
+ else {
781
+ // Execute the full require statement as-is
782
+ if (options.tempDir) {
783
+ moduleResult = await ModuleResolutionManager.runWithContext({ tempDir: options.tempDir, testFileDir: context.__testFileDir }, async () => {
784
+ // Try to extract variable name, but execute the require
785
+ const varNameMatch = requireStmt.match(/const\s+(\w+)\s*=/);
786
+ if (varNameMatch) {
787
+ const script = new vm.Script(`(() => { ${requireStmt}; return ${varNameMatch[1]}; })()`);
788
+ return script.runInContext(context);
789
+ }
790
+ else {
791
+ // Side-effect import - just execute
792
+ const script = new vm.Script(`(() => { ${requireStmt}; return null; })()`);
793
+ script.runInContext(context);
794
+ return null;
795
+ }
796
+ });
797
+ }
798
+ else {
799
+ const varNameMatch = requireStmt.match(/const\s+(\w+)\s*=/);
800
+ if (varNameMatch) {
801
+ const script = new vm.Script(`(() => { ${requireStmt}; return ${varNameMatch[1]}; })()`);
802
+ moduleResult = script.runInContext(context);
803
+ }
804
+ else {
805
+ const script = new vm.Script(`(() => { ${requireStmt}; return null; })()`);
806
+ script.runInContext(context);
807
+ moduleResult = null;
808
+ }
809
+ }
810
+ }
811
+ // Assign all exports from the module to context
812
+ // This handles named imports, default imports, and namespace imports generically
813
+ if (moduleResult && typeof moduleResult === 'object') {
814
+ // Assign all named exports
815
+ Object.keys(moduleResult).forEach(key => {
816
+ if (key !== 'default') {
817
+ context[key] = moduleResult[key];
818
+ this.log(`Assigned ${key} to context`);
819
+ }
820
+ });
821
+ // Assign default export if present
822
+ if (moduleResult.default !== undefined) {
823
+ // Try to find the variable name from the require statement
824
+ const varNameMatch = requireStmt.match(/const\s+(\w+)\s*=/);
825
+ if (varNameMatch) {
826
+ context[varNameMatch[1]] = moduleResult.default;
827
+ this.log(`Assigned ${varNameMatch[1]} (default) to context`);
828
+ }
829
+ }
830
+ }
831
+ else if (moduleResult !== null && moduleResult !== undefined) {
832
+ // Module is not an object (e.g., primitive or function)
833
+ const varNameMatch = requireStmt.match(/const\s+(\w+)\s*=/);
834
+ if (varNameMatch) {
835
+ context[varNameMatch[1]] = moduleResult;
836
+ this.log(`Assigned ${varNameMatch[1]} to context`);
837
+ }
838
+ }
839
+ this.log(`Successfully executed: ${requireStmt.substring(0, 80)}...`);
840
+ }
841
+ catch (individualError) {
842
+ this.log(`Failed to execute: ${requireStmt.substring(0, 80)}... - ${individualError.message}`, 'warn');
843
+ // Continue with other imports
844
+ }
845
+ }
846
+ }
847
+ this.log('Import statements executed successfully');
848
+ // Verify that the imports were actually loaded by checking the context
849
+ const contextKeys = Object.keys(context);
850
+ const userVars = contextKeys.filter((key) => !['page', 'expect', 'test', 'ai', 'browser', 'context', 'console', 'require',
851
+ 'setTimeout', 'setInterval', 'clearTimeout', 'clearInterval', 'Buffer',
852
+ 'process', '__dirname', '__filename', '__testFileDir', 'Math', 'Date',
853
+ 'JSON', 'Promise', 'Array', 'Object', 'String', 'Number', 'Boolean',
854
+ 'Error', 'Set', 'Map', 'WeakSet', 'WeakMap'].includes(key));
855
+ this.log(`Context variables after import execution: ${userVars.join(', ') || '(none)'}`);
856
+ }
857
+ catch (execError) {
858
+ this.log(`Error executing require statements: ${execError.message}`, 'error');
859
+ this.log(`Stack trace: ${execError.stack}`, 'error');
860
+ throw execError;
861
+ }
862
+ }
863
+ }
864
+ catch (importError) {
865
+ this.log(`Warning: Failed to execute imports: ${importError.message}`, 'warn');
866
+ // Continue execution - some imports might not be needed
867
+ }
868
+ // Extract and execute initialization code from the test script
869
+ // This includes code like: const signInPage = new SignInPage(page);
870
+ // We execute this using the same mechanism as steps (persistentContext.executeCode)
871
+ // But we need to transform const/let to assignments so variables are available in context
872
+ if (options.originalScript) {
873
+ try {
874
+ this.log('Extracting initialization code from test script...');
875
+ const initializationCode = initialization_code_utils_1.InitializationCodeUtils.extractInitializationCode(options.originalScript, (msg, level) => this.log(msg, level));
876
+ if (initializationCode) {
877
+ this.log(`Executing initialization code: ${initializationCode.substring(0, 100)}...`);
878
+ // Transform const/let declarations to assignments so they create properties on context
879
+ // This is necessary because const/let create block-scoped variables, not context properties
880
+ const { transformedCode } = initialization_code_utils_1.InitializationCodeUtils.transformInitializationCode(initializationCode, (msg, level) => this.log(msg, level));
881
+ // Execute using the same mechanism as steps
882
+ // This ensures consistent behavior and module resolution
883
+ await persistentContext.executeCode(transformedCode);
884
+ this.log('Initialization code executed successfully');
885
+ }
886
+ else {
887
+ this.log('No initialization code found in test script');
888
+ }
889
+ }
890
+ catch (initError) {
891
+ this.log(`Warning: Failed to execute initialization code: ${initError.message}`, 'warn');
892
+ // Continue execution - initialization might not be needed
893
+ }
894
+ }
895
+ }
375
896
  this.log(`Executing ${steps.length} steps in persistent context (mode: ${options.mode})`);
376
897
  // Track executed step descriptions for repair context
377
898
  const executedStepDescriptions = [];
@@ -404,7 +925,12 @@ class ExecutionService {
404
925
  await persistentContext.executeCode(step.code);
405
926
  this.log(` ✓ Step ${i + 1} succeeded`);
406
927
  // Mark step as successful
407
- step.success = true;
928
+ // Ensure we're updating the step in the array (important after repair when new object was created)
929
+ updatedSteps[i].success = true;
930
+ // Ensure code is preserved in array (defensive - step is a reference but be explicit after repair)
931
+ if (step.code && step.code.trim().length > 0) {
932
+ updatedSteps[i].code = step.code;
933
+ }
408
934
  // Track executed step for repair context
409
935
  executedStepDescriptions.push(step.description);
410
936
  // LIFECYCLE: onStepComplete (success)
@@ -544,6 +1070,7 @@ class ExecutionService {
544
1070
  const totalAttempts = deflakeRunCount + 1; // Original run + deflake attempts
545
1071
  let lastError = null;
546
1072
  this.log(`runExactly: deflakeRunCount = ${deflakeRunCount}, totalAttempts = ${totalAttempts}`);
1073
+ this.log(`runExactly: request.script = ${!!request.script} (length: ${request.script?.length || 0}), request.tempDir = ${request.tempDir}`);
547
1074
  // Script content should be provided by the caller (TestChimpService)
548
1075
  // The TestChimpService handles file reading through the appropriate FileHandler
549
1076
  if (!request.script) {
@@ -575,7 +1102,9 @@ class ExecutionService {
575
1102
  // Execute steps in persistent context (STOP on first error)
576
1103
  const result = await this.executeStepsInPersistentContext(steps, page, browser, context, {
577
1104
  mode: 'RUN_EXACTLY',
578
- jobId: request.jobId
1105
+ jobId: request.jobId,
1106
+ tempDir: request.tempDir,
1107
+ originalScript: request.script // Pass original script for import extraction
579
1108
  });
580
1109
  // LIFECYCLE: afterEndTest
581
1110
  if (this.progressReporter?.afterEndTest) {
@@ -630,7 +1159,9 @@ class ExecutionService {
630
1159
  // Execute steps in persistent context (STOP on first error)
631
1160
  const result = await this.executeStepsInPersistentContext(steps, page, browser, context, {
632
1161
  mode: 'RUN_EXACTLY',
633
- jobId: request.jobId
1162
+ jobId: request.jobId,
1163
+ tempDir: request.tempDir,
1164
+ originalScript: request.script // Pass original script for import extraction
634
1165
  });
635
1166
  // LIFECYCLE: afterEndTest
636
1167
  if (this.progressReporter?.afterEndTest) {
@@ -744,7 +1275,9 @@ class ExecutionService {
744
1275
  const result = await this.executeStepsInPersistentContext(steps, repairPage, repairBrowser, repairContext, {
745
1276
  mode: 'RUN_WITH_AI_REPAIR',
746
1277
  jobId: request.jobId,
747
- model
1278
+ model,
1279
+ tempDir: request.tempDir,
1280
+ originalScript: request.script // Pass original script for import extraction
748
1281
  });
749
1282
  const updatedSteps = result.updatedSteps || steps;
750
1283
  const allStepsSuccessful = result.success;
@@ -813,52 +1346,11 @@ class ExecutionService {
813
1346
  }
814
1347
  catch (error) {
815
1348
  this.log(`LLM parsing failed, falling back to code parsing: ${error}`);
816
- const fallbackResult = this.parseScriptIntoStepsFallback(script);
1349
+ const fallbackResult = script_parser_utils_1.ScriptParserUtils.parseScriptIntoStepsFallback(script);
817
1350
  this.log(`Fallback parsing successful, got ${fallbackResult.length} steps`);
818
1351
  return fallbackResult;
819
1352
  }
820
1353
  }
821
- parseScriptIntoStepsFallback(script) {
822
- const lines = script.split('\n');
823
- const steps = [];
824
- let currentStep = null;
825
- let currentCode = [];
826
- for (const line of lines) {
827
- const trimmedLine = line.trim();
828
- // Check for step comment
829
- if (trimmedLine.startsWith('// Step ')) {
830
- // Save previous step if exists and has code
831
- if (currentStep) {
832
- const code = currentCode.join('\n').trim();
833
- const cleanedCode = this.cleanStepCode(code);
834
- if (cleanedCode) {
835
- currentStep.code = cleanedCode;
836
- steps.push(currentStep);
837
- }
838
- }
839
- // Start new step
840
- const description = trimmedLine.replace(/^\/\/\s*Step\s*\d+:\s*/, '').replace(/\s*\[FAILED\]\s*$/, '').trim();
841
- currentStep = { description, code: '' };
842
- currentCode = [];
843
- }
844
- else if (trimmedLine && !trimmedLine.startsWith('import') && !trimmedLine.startsWith('test(') && !trimmedLine.startsWith('});')) {
845
- // Add code line to current step
846
- if (currentStep) {
847
- currentCode.push(line);
848
- }
849
- }
850
- }
851
- // Add the last step if it has code
852
- if (currentStep) {
853
- const code = currentCode.join('\n').trim();
854
- const cleanedCode = this.cleanStepCode(code);
855
- if (cleanedCode) {
856
- currentStep.code = cleanedCode;
857
- steps.push(currentStep);
858
- }
859
- }
860
- return steps;
861
- }
862
1354
  async repairStepsWithAI(steps, page, repairFlexibility, model, jobId) {
863
1355
  let updatedSteps = [...steps];
864
1356
  const maxTries = 3;
@@ -964,7 +1456,7 @@ class ExecutionService {
964
1456
  // Unexpected error in the tracking logic itself
965
1457
  this.log(`Unexpected error in step execution tracking: ${error}`);
966
1458
  step.success = false;
967
- step.error = this.safeSerializeError(error);
1459
+ step.error = step_execution_utils_1.StepExecutionUtils.safeSerializeError(error);
968
1460
  stepWasAttempted = true; // Mark as attempted (failed)
969
1461
  }
970
1462
  // Only attempt repair if step was actually tried and failed
@@ -1096,7 +1588,7 @@ class ExecutionService {
1096
1588
  page.setDefaultNavigationTimeout(30000);
1097
1589
  try {
1098
1590
  // Clean and validate the code before execution
1099
- const cleanedCode = this.cleanStepCode(code);
1591
+ const cleanedCode = script_parser_utils_1.ScriptParserUtils.cleanStepCode(code);
1100
1592
  if (!cleanedCode || cleanedCode.trim().length === 0) {
1101
1593
  throw new Error('Step code is empty or contains only comments');
1102
1594
  }
@@ -1120,20 +1612,6 @@ class ExecutionService {
1120
1612
  page.setDefaultNavigationTimeout(30000);
1121
1613
  }
1122
1614
  }
1123
- /**
1124
- * Validate step code has executable content (preserves comments)
1125
- */
1126
- cleanStepCode(code) {
1127
- if (!code || code.trim().length === 0) {
1128
- return '';
1129
- }
1130
- // Check if there are any executable statements (including those with comments)
1131
- 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);
1132
- if (!hasExecutableCode) {
1133
- return '';
1134
- }
1135
- return code; // Return the original code without removing comments
1136
- }
1137
1615
  // Legacy repair helper methods (now unused but kept for compilation)
1138
1616
  buildFailureHistory() { return ''; }
1139
1617
  buildRecentRepairsContext() { return ''; }
@@ -1141,43 +1619,7 @@ class ExecutionService {
1141
1619
  return { success: false };
1142
1620
  }
1143
1621
  generateUpdatedScript(steps, repairAdvice, originalScript) {
1144
- // Extract test name and hashtags from original script if provided
1145
- let testName = 'repairedTest';
1146
- let hashtags = [];
1147
- if (originalScript) {
1148
- const testNameMatch = originalScript.match(/test\(['"]([^'"]+)['"]/);
1149
- if (testNameMatch) {
1150
- testName = testNameMatch[1];
1151
- }
1152
- // Extract hashtags from TestChimp comment
1153
- const hashtagMatch = originalScript.match(/#\w+(?:\s+#\w+)*/);
1154
- if (hashtagMatch) {
1155
- hashtags = hashtagMatch[0].split(/\s+/).filter(tag => tag.startsWith('#'));
1156
- }
1157
- }
1158
- const scriptLines = [
1159
- "import { test, expect } from '@playwright/test';"
1160
- ];
1161
- const needsAiImport = steps.some(step => (0, ai_command_utils_1.containsAiCommand)(step.code));
1162
- if (needsAiImport) {
1163
- scriptLines.push("import { ai } from 'ai-wright';");
1164
- }
1165
- scriptLines.push('');
1166
- scriptLines.push(`test('${testName}', async ({ page, browser, context }) => {`);
1167
- steps.forEach((step, index) => {
1168
- // Only add step if it has code to execute
1169
- if (step.code && step.code.trim().length > 0) {
1170
- scriptLines.push(` // ${step.description}`);
1171
- const codeLines = step.code.split('\n');
1172
- codeLines.forEach(line => {
1173
- scriptLines.push(` ${line}`);
1174
- });
1175
- }
1176
- });
1177
- scriptLines.push('});');
1178
- const script = scriptLines.join('\n');
1179
- // Add TestChimp comment with hashtags and repair advice
1180
- return (0, script_utils_1.addTestChimpComment)(script, repairAdvice, hashtags);
1622
+ return script_generator_utils_1.ScriptGeneratorUtils.generateUpdatedScript(steps, repairAdvice, originalScript);
1181
1623
  }
1182
1624
  /**
1183
1625
  * Initialize browser with configuration (delegates to utility function)
@@ -1198,7 +1640,7 @@ class ExecutionService {
1198
1640
  let remainingCommands = [];
1199
1641
  let error;
1200
1642
  // Parse step code into individual statements (handles multi-line await blocks)
1201
- const statements = this.splitIntoExecutableStatements(stepCode);
1643
+ const statements = step_execution_utils_1.StepExecutionUtils.splitIntoExecutableStatements(stepCode);
1202
1644
  // Separate variable declarations and Playwright commands
1203
1645
  const declarations = []; // const/let/var declarations
1204
1646
  const commands = []; // executable statements (await/page/expect/ai/control flow)
@@ -1241,7 +1683,7 @@ class ExecutionService {
1241
1683
  successfulCommands: [],
1242
1684
  failingCommand: stepCode,
1243
1685
  remainingCommands: [],
1244
- error: this.safeSerializeError(err)
1686
+ error: step_execution_utils_1.StepExecutionUtils.safeSerializeError(err)
1245
1687
  };
1246
1688
  }
1247
1689
  }
@@ -1276,7 +1718,7 @@ class ExecutionService {
1276
1718
  catch (err) {
1277
1719
  // This command failed - it's the failing one
1278
1720
  failingCommand = cmd;
1279
- error = this.safeSerializeError(err);
1721
+ error = step_execution_utils_1.StepExecutionUtils.safeSerializeError(err);
1280
1722
  // Capture remaining commands that were not executed
1281
1723
  remainingCommands = commands.slice(cmdIdx + 1);
1282
1724
  this.log(` ✗ Command failed: ${cmd.substring(0, 60)}...`);
@@ -1289,90 +1731,6 @@ class ExecutionService {
1289
1731
  }
1290
1732
  return { successfulCommands, failingCommand, remainingCommands, error };
1291
1733
  }
1292
- /**
1293
- * Split raw step code into syntactically complete statements.
1294
- * Uses Function compilation to detect when a statement is complete so that
1295
- * multi-line await blocks (e.g., ai.verify) are preserved.
1296
- */
1297
- splitIntoExecutableStatements(stepCode) {
1298
- const statements = [];
1299
- const lines = stepCode.split('\n');
1300
- let buffer = [];
1301
- const flushBuffer = () => {
1302
- if (buffer.length === 0) {
1303
- return;
1304
- }
1305
- const statement = buffer.join('\n').trim();
1306
- if (statement.length > 0) {
1307
- statements.push(statement);
1308
- }
1309
- buffer = [];
1310
- };
1311
- const isIncomplete = (code) => {
1312
- const trimmed = code.trim();
1313
- if (trimmed.length === 0) {
1314
- return false;
1315
- }
1316
- try {
1317
- new Function('page', 'expect', 'test', 'ai', `return async () => {\n${code}\n};`);
1318
- return false;
1319
- }
1320
- catch (error) {
1321
- if (error instanceof SyntaxError) {
1322
- return true;
1323
- }
1324
- return false;
1325
- }
1326
- };
1327
- for (const rawLine of lines) {
1328
- if (buffer.length === 0 && rawLine.trim().length === 0) {
1329
- continue;
1330
- }
1331
- buffer.push(rawLine);
1332
- const candidate = buffer.join('\n');
1333
- if (!isIncomplete(candidate)) {
1334
- flushBuffer();
1335
- }
1336
- }
1337
- flushBuffer();
1338
- return statements;
1339
- }
1340
- /**
1341
- * Safely serialize error information, filtering out non-serializable values
1342
- */
1343
- safeSerializeError(error) {
1344
- try {
1345
- if (error instanceof Error) {
1346
- return error.message;
1347
- }
1348
- if (typeof error === 'string') {
1349
- return error;
1350
- }
1351
- if (typeof error === 'object' && error !== null) {
1352
- // Try to extract meaningful information without serializing the entire object
1353
- const safeError = {};
1354
- // Copy safe properties
1355
- if (error.message)
1356
- safeError.message = error.message;
1357
- if (error.name)
1358
- safeError.name = error.name;
1359
- if (error.code)
1360
- safeError.code = error.code;
1361
- if (error.status)
1362
- safeError.status = error.status;
1363
- // Try to get stack trace safely
1364
- if (error.stack && typeof error.stack === 'string') {
1365
- safeError.stack = error.stack;
1366
- }
1367
- return JSON.stringify(safeError);
1368
- }
1369
- return String(error);
1370
- }
1371
- catch (serializationError) {
1372
- // If even safe serialization fails, return a basic string representation
1373
- return `Error: ${String(error)}`;
1374
- }
1375
- }
1376
1734
  }
1377
1735
  exports.ExecutionService = ExecutionService;
1378
1736
  //# sourceMappingURL=execution-service.js.map