testchimp-runner-core 0.0.75 → 0.0.76
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/execution-service.d.ts +0 -15
- package/dist/execution-service.d.ts.map +1 -1
- package/dist/execution-service.js +552 -199
- package/dist/execution-service.js.map +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/utils/import-utils.d.ts +18 -0
- package/dist/utils/import-utils.d.ts.map +1 -0
- package/dist/utils/import-utils.js +80 -0
- package/dist/utils/import-utils.js.map +1 -0
- package/dist/utils/initialization-code-utils.d.ts +27 -0
- package/dist/utils/initialization-code-utils.d.ts.map +1 -0
- package/dist/utils/initialization-code-utils.js +272 -0
- package/dist/utils/initialization-code-utils.js.map +1 -0
- package/dist/utils/script-generator-utils.d.ts +14 -0
- package/dist/utils/script-generator-utils.d.ts.map +1 -0
- package/dist/utils/script-generator-utils.js +54 -0
- package/dist/utils/script-generator-utils.js.map +1 -0
- package/dist/utils/script-parser-utils.d.ts +20 -0
- package/dist/utils/script-parser-utils.d.ts.map +1 -0
- package/dist/utils/script-parser-utils.js +126 -0
- package/dist/utils/script-parser-utils.js.map +1 -0
- package/dist/utils/step-execution-utils.d.ts +16 -0
- package/dist/utils/step-execution-utils.d.ts.map +1 -0
- package/dist/utils/step-execution-utils.js +94 -0
- package/dist/utils/step-execution-utils.js.map +1 -0
- package/package.json +7 -1
|
@@ -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
|
-
|
|
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
|
-
//
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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 = [];
|
|
@@ -544,6 +1065,7 @@ class ExecutionService {
|
|
|
544
1065
|
const totalAttempts = deflakeRunCount + 1; // Original run + deflake attempts
|
|
545
1066
|
let lastError = null;
|
|
546
1067
|
this.log(`runExactly: deflakeRunCount = ${deflakeRunCount}, totalAttempts = ${totalAttempts}`);
|
|
1068
|
+
this.log(`runExactly: request.script = ${!!request.script} (length: ${request.script?.length || 0}), request.tempDir = ${request.tempDir}`);
|
|
547
1069
|
// Script content should be provided by the caller (TestChimpService)
|
|
548
1070
|
// The TestChimpService handles file reading through the appropriate FileHandler
|
|
549
1071
|
if (!request.script) {
|
|
@@ -575,7 +1097,9 @@ class ExecutionService {
|
|
|
575
1097
|
// Execute steps in persistent context (STOP on first error)
|
|
576
1098
|
const result = await this.executeStepsInPersistentContext(steps, page, browser, context, {
|
|
577
1099
|
mode: 'RUN_EXACTLY',
|
|
578
|
-
jobId: request.jobId
|
|
1100
|
+
jobId: request.jobId,
|
|
1101
|
+
tempDir: request.tempDir,
|
|
1102
|
+
originalScript: request.script // Pass original script for import extraction
|
|
579
1103
|
});
|
|
580
1104
|
// LIFECYCLE: afterEndTest
|
|
581
1105
|
if (this.progressReporter?.afterEndTest) {
|
|
@@ -630,7 +1154,9 @@ class ExecutionService {
|
|
|
630
1154
|
// Execute steps in persistent context (STOP on first error)
|
|
631
1155
|
const result = await this.executeStepsInPersistentContext(steps, page, browser, context, {
|
|
632
1156
|
mode: 'RUN_EXACTLY',
|
|
633
|
-
jobId: request.jobId
|
|
1157
|
+
jobId: request.jobId,
|
|
1158
|
+
tempDir: request.tempDir,
|
|
1159
|
+
originalScript: request.script // Pass original script for import extraction
|
|
634
1160
|
});
|
|
635
1161
|
// LIFECYCLE: afterEndTest
|
|
636
1162
|
if (this.progressReporter?.afterEndTest) {
|
|
@@ -744,7 +1270,9 @@ class ExecutionService {
|
|
|
744
1270
|
const result = await this.executeStepsInPersistentContext(steps, repairPage, repairBrowser, repairContext, {
|
|
745
1271
|
mode: 'RUN_WITH_AI_REPAIR',
|
|
746
1272
|
jobId: request.jobId,
|
|
747
|
-
model
|
|
1273
|
+
model,
|
|
1274
|
+
tempDir: request.tempDir,
|
|
1275
|
+
originalScript: request.script // Pass original script for import extraction
|
|
748
1276
|
});
|
|
749
1277
|
const updatedSteps = result.updatedSteps || steps;
|
|
750
1278
|
const allStepsSuccessful = result.success;
|
|
@@ -813,52 +1341,11 @@ class ExecutionService {
|
|
|
813
1341
|
}
|
|
814
1342
|
catch (error) {
|
|
815
1343
|
this.log(`LLM parsing failed, falling back to code parsing: ${error}`);
|
|
816
|
-
const fallbackResult =
|
|
1344
|
+
const fallbackResult = script_parser_utils_1.ScriptParserUtils.parseScriptIntoStepsFallback(script);
|
|
817
1345
|
this.log(`Fallback parsing successful, got ${fallbackResult.length} steps`);
|
|
818
1346
|
return fallbackResult;
|
|
819
1347
|
}
|
|
820
1348
|
}
|
|
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
1349
|
async repairStepsWithAI(steps, page, repairFlexibility, model, jobId) {
|
|
863
1350
|
let updatedSteps = [...steps];
|
|
864
1351
|
const maxTries = 3;
|
|
@@ -964,7 +1451,7 @@ class ExecutionService {
|
|
|
964
1451
|
// Unexpected error in the tracking logic itself
|
|
965
1452
|
this.log(`Unexpected error in step execution tracking: ${error}`);
|
|
966
1453
|
step.success = false;
|
|
967
|
-
step.error =
|
|
1454
|
+
step.error = step_execution_utils_1.StepExecutionUtils.safeSerializeError(error);
|
|
968
1455
|
stepWasAttempted = true; // Mark as attempted (failed)
|
|
969
1456
|
}
|
|
970
1457
|
// Only attempt repair if step was actually tried and failed
|
|
@@ -1096,7 +1583,7 @@ class ExecutionService {
|
|
|
1096
1583
|
page.setDefaultNavigationTimeout(30000);
|
|
1097
1584
|
try {
|
|
1098
1585
|
// Clean and validate the code before execution
|
|
1099
|
-
const cleanedCode =
|
|
1586
|
+
const cleanedCode = script_parser_utils_1.ScriptParserUtils.cleanStepCode(code);
|
|
1100
1587
|
if (!cleanedCode || cleanedCode.trim().length === 0) {
|
|
1101
1588
|
throw new Error('Step code is empty or contains only comments');
|
|
1102
1589
|
}
|
|
@@ -1120,20 +1607,6 @@ class ExecutionService {
|
|
|
1120
1607
|
page.setDefaultNavigationTimeout(30000);
|
|
1121
1608
|
}
|
|
1122
1609
|
}
|
|
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
1610
|
// Legacy repair helper methods (now unused but kept for compilation)
|
|
1138
1611
|
buildFailureHistory() { return ''; }
|
|
1139
1612
|
buildRecentRepairsContext() { return ''; }
|
|
@@ -1141,43 +1614,7 @@ class ExecutionService {
|
|
|
1141
1614
|
return { success: false };
|
|
1142
1615
|
}
|
|
1143
1616
|
generateUpdatedScript(steps, repairAdvice, originalScript) {
|
|
1144
|
-
|
|
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);
|
|
1617
|
+
return script_generator_utils_1.ScriptGeneratorUtils.generateUpdatedScript(steps, repairAdvice, originalScript);
|
|
1181
1618
|
}
|
|
1182
1619
|
/**
|
|
1183
1620
|
* Initialize browser with configuration (delegates to utility function)
|
|
@@ -1198,7 +1635,7 @@ class ExecutionService {
|
|
|
1198
1635
|
let remainingCommands = [];
|
|
1199
1636
|
let error;
|
|
1200
1637
|
// Parse step code into individual statements (handles multi-line await blocks)
|
|
1201
|
-
const statements =
|
|
1638
|
+
const statements = step_execution_utils_1.StepExecutionUtils.splitIntoExecutableStatements(stepCode);
|
|
1202
1639
|
// Separate variable declarations and Playwright commands
|
|
1203
1640
|
const declarations = []; // const/let/var declarations
|
|
1204
1641
|
const commands = []; // executable statements (await/page/expect/ai/control flow)
|
|
@@ -1241,7 +1678,7 @@ class ExecutionService {
|
|
|
1241
1678
|
successfulCommands: [],
|
|
1242
1679
|
failingCommand: stepCode,
|
|
1243
1680
|
remainingCommands: [],
|
|
1244
|
-
error:
|
|
1681
|
+
error: step_execution_utils_1.StepExecutionUtils.safeSerializeError(err)
|
|
1245
1682
|
};
|
|
1246
1683
|
}
|
|
1247
1684
|
}
|
|
@@ -1276,7 +1713,7 @@ class ExecutionService {
|
|
|
1276
1713
|
catch (err) {
|
|
1277
1714
|
// This command failed - it's the failing one
|
|
1278
1715
|
failingCommand = cmd;
|
|
1279
|
-
error =
|
|
1716
|
+
error = step_execution_utils_1.StepExecutionUtils.safeSerializeError(err);
|
|
1280
1717
|
// Capture remaining commands that were not executed
|
|
1281
1718
|
remainingCommands = commands.slice(cmdIdx + 1);
|
|
1282
1719
|
this.log(` ✗ Command failed: ${cmd.substring(0, 60)}...`);
|
|
@@ -1289,90 +1726,6 @@ class ExecutionService {
|
|
|
1289
1726
|
}
|
|
1290
1727
|
return { successfulCommands, failingCommand, remainingCommands, error };
|
|
1291
1728
|
}
|
|
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
1729
|
}
|
|
1377
1730
|
exports.ExecutionService = ExecutionService;
|
|
1378
1731
|
//# sourceMappingURL=execution-service.js.map
|