@apiquest/fracture 1.0.2 → 1.0.4
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/README.md +119 -0
- package/bin/cli.js +2 -2
- package/dist/CollectionRunner.js +3 -3
- package/dist/ScriptEngine.js +4 -4
- package/dist/cli/plugin-commands.d.ts.map +1 -1
- package/dist/cli/plugin-commands.js +2 -1
- package/dist/cli/plugin-commands.js.map +1 -1
- package/package.json +55 -50
- package/src/CollectionAnalyzer.ts +102 -102
- package/src/CollectionRunner.ts +1423 -1423
- package/src/CollectionRunner.types.ts +9 -9
- package/src/CollectionValidator.ts +289 -289
- package/src/ConsoleReporter.ts +143 -143
- package/src/CookieJar.ts +258 -258
- package/src/DagScheduler.ts +439 -439
- package/src/Logger.ts +85 -85
- package/src/PluginLoader.ts +126 -126
- package/src/PluginManager.ts +208 -208
- package/src/PluginResolver.ts +154 -154
- package/src/QuestAPI.ts +764 -764
- package/src/QuestAPI.types.ts +33 -33
- package/src/QuestTestAPI.ts +164 -164
- package/src/RequestFilter.ts +224 -224
- package/src/ScriptEngine.ts +219 -219
- package/src/ScriptValidator.ts +428 -428
- package/src/TaskGraph.ts +598 -598
- package/src/TestCounter.ts +109 -109
- package/src/VariableResolver.ts +114 -114
- package/src/cli/index.ts +480 -480
- package/src/cli/plugin-commands.ts +342 -341
- package/src/cli/plugin-discovery.ts +44 -44
- package/src/index.ts +24 -24
- package/src/utils.ts +52 -52
- package/tsconfig.json +20 -20
- package/tsconfig.test.json +5 -5
- package/vitest.config.ts +22 -22
- package/dist/ExecutionTree.d.ts +0 -77
- package/dist/ExecutionTree.d.ts.map +0 -1
- package/dist/ExecutionTree.js +0 -265
- package/dist/ExecutionTree.js.map +0 -1
- package/dist/fracture/src/CollectionAnalyzer.d.ts +0 -17
- package/dist/fracture/src/CollectionAnalyzer.d.ts.map +0 -1
- package/dist/fracture/src/CollectionAnalyzer.js +0 -70
- package/dist/fracture/src/CollectionAnalyzer.js.map +0 -1
- package/dist/fracture/src/CollectionRunner.d.ts +0 -39
- package/dist/fracture/src/CollectionRunner.d.ts.map +0 -1
- package/dist/fracture/src/CollectionRunner.js +0 -802
- package/dist/fracture/src/CollectionRunner.js.map +0 -1
- package/dist/fracture/src/CollectionRunner.types.d.ts +0 -8
- package/dist/fracture/src/CollectionRunner.types.d.ts.map +0 -1
- package/dist/fracture/src/CollectionRunner.types.js +0 -2
- package/dist/fracture/src/CollectionRunner.types.js.map +0 -1
- package/dist/fracture/src/CollectionValidator.d.ts +0 -14
- package/dist/fracture/src/CollectionValidator.d.ts.map +0 -1
- package/dist/fracture/src/CollectionValidator.js +0 -145
- package/dist/fracture/src/CollectionValidator.js.map +0 -1
- package/dist/fracture/src/ConsoleReporter.d.ts +0 -24
- package/dist/fracture/src/ConsoleReporter.d.ts.map +0 -1
- package/dist/fracture/src/ConsoleReporter.js +0 -123
- package/dist/fracture/src/ConsoleReporter.js.map +0 -1
- package/dist/fracture/src/CookieJar.d.ts +0 -70
- package/dist/fracture/src/CookieJar.d.ts.map +0 -1
- package/dist/fracture/src/CookieJar.js +0 -233
- package/dist/fracture/src/CookieJar.js.map +0 -1
- package/dist/fracture/src/ExecutionTree.d.ts +0 -77
- package/dist/fracture/src/ExecutionTree.d.ts.map +0 -1
- package/dist/fracture/src/ExecutionTree.js +0 -258
- package/dist/fracture/src/ExecutionTree.js.map +0 -1
- package/dist/fracture/src/Logger.d.ts +0 -25
- package/dist/fracture/src/Logger.d.ts.map +0 -1
- package/dist/fracture/src/Logger.js +0 -78
- package/dist/fracture/src/Logger.js.map +0 -1
- package/dist/fracture/src/PluginLoader.d.ts +0 -23
- package/dist/fracture/src/PluginLoader.d.ts.map +0 -1
- package/dist/fracture/src/PluginLoader.js +0 -102
- package/dist/fracture/src/PluginLoader.js.map +0 -1
- package/dist/fracture/src/PluginManager.d.ts +0 -64
- package/dist/fracture/src/PluginManager.d.ts.map +0 -1
- package/dist/fracture/src/PluginManager.js +0 -162
- package/dist/fracture/src/PluginManager.js.map +0 -1
- package/dist/fracture/src/PluginResolver.d.ts +0 -35
- package/dist/fracture/src/PluginResolver.d.ts.map +0 -1
- package/dist/fracture/src/PluginResolver.js +0 -128
- package/dist/fracture/src/PluginResolver.js.map +0 -1
- package/dist/fracture/src/QuestAPI.d.ts +0 -9
- package/dist/fracture/src/QuestAPI.d.ts.map +0 -1
- package/dist/fracture/src/QuestAPI.js +0 -679
- package/dist/fracture/src/QuestAPI.js.map +0 -1
- package/dist/fracture/src/QuestAPI.types.d.ts +0 -35
- package/dist/fracture/src/QuestAPI.types.d.ts.map +0 -1
- package/dist/fracture/src/QuestAPI.types.js +0 -3
- package/dist/fracture/src/QuestAPI.types.js.map +0 -1
- package/dist/fracture/src/QuestTestAPI.d.ts +0 -12
- package/dist/fracture/src/QuestTestAPI.d.ts.map +0 -1
- package/dist/fracture/src/QuestTestAPI.js +0 -133
- package/dist/fracture/src/QuestTestAPI.js.map +0 -1
- package/dist/fracture/src/ScriptEngine.d.ts +0 -21
- package/dist/fracture/src/ScriptEngine.d.ts.map +0 -1
- package/dist/fracture/src/ScriptEngine.js +0 -183
- package/dist/fracture/src/ScriptEngine.js.map +0 -1
- package/dist/fracture/src/ScriptValidator.d.ts +0 -68
- package/dist/fracture/src/ScriptValidator.d.ts.map +0 -1
- package/dist/fracture/src/ScriptValidator.js +0 -351
- package/dist/fracture/src/ScriptValidator.js.map +0 -1
- package/dist/fracture/src/TestCounter.d.ts +0 -18
- package/dist/fracture/src/TestCounter.d.ts.map +0 -1
- package/dist/fracture/src/TestCounter.js +0 -82
- package/dist/fracture/src/TestCounter.js.map +0 -1
- package/dist/fracture/src/VariableResolver.d.ts +0 -20
- package/dist/fracture/src/VariableResolver.d.ts.map +0 -1
- package/dist/fracture/src/VariableResolver.js +0 -100
- package/dist/fracture/src/VariableResolver.js.map +0 -1
- package/dist/fracture/src/cli/index.d.ts +0 -3
- package/dist/fracture/src/cli/index.d.ts.map +0 -1
- package/dist/fracture/src/cli/index.js +0 -347
- package/dist/fracture/src/cli/index.js.map +0 -1
- package/dist/fracture/src/cli/plugin-commands.d.ts +0 -6
- package/dist/fracture/src/cli/plugin-commands.d.ts.map +0 -1
- package/dist/fracture/src/cli/plugin-commands.js +0 -263
- package/dist/fracture/src/cli/plugin-commands.js.map +0 -1
- package/dist/fracture/src/cli/plugin-discovery.d.ts +0 -11
- package/dist/fracture/src/cli/plugin-discovery.d.ts.map +0 -1
- package/dist/fracture/src/cli/plugin-discovery.js +0 -64
- package/dist/fracture/src/cli/plugin-discovery.js.map +0 -1
- package/dist/fracture/src/index.d.ts +0 -13
- package/dist/fracture/src/index.d.ts.map +0 -1
- package/dist/fracture/src/index.js +0 -17
- package/dist/fracture/src/index.js.map +0 -1
- package/dist/fracture/src/utils.d.ts +0 -28
- package/dist/fracture/src/utils.d.ts.map +0 -1
- package/dist/fracture/src/utils.js +0 -48
- package/dist/fracture/src/utils.js.map +0 -1
- package/dist/plugin-auth/src/apikey-auth.d.ts +0 -3
- package/dist/plugin-auth/src/apikey-auth.d.ts.map +0 -1
- package/dist/plugin-auth/src/apikey-auth.js +0 -73
- package/dist/plugin-auth/src/apikey-auth.js.map +0 -1
- package/dist/plugin-auth/src/basic-auth.d.ts +0 -3
- package/dist/plugin-auth/src/basic-auth.d.ts.map +0 -1
- package/dist/plugin-auth/src/basic-auth.js +0 -61
- package/dist/plugin-auth/src/basic-auth.js.map +0 -1
- package/dist/plugin-auth/src/bearer-auth.d.ts +0 -3
- package/dist/plugin-auth/src/bearer-auth.d.ts.map +0 -1
- package/dist/plugin-auth/src/bearer-auth.js +0 -49
- package/dist/plugin-auth/src/bearer-auth.js.map +0 -1
- package/dist/plugin-auth/src/helpers.d.ts +0 -3
- package/dist/plugin-auth/src/helpers.d.ts.map +0 -1
- package/dist/plugin-auth/src/helpers.js +0 -8
- package/dist/plugin-auth/src/helpers.js.map +0 -1
- package/dist/plugin-auth/src/index.d.ts +0 -10
- package/dist/plugin-auth/src/index.d.ts.map +0 -1
- package/dist/plugin-auth/src/index.js +0 -25
- package/dist/plugin-auth/src/index.js.map +0 -1
- package/dist/plugin-auth/src/oauth2-auth.d.ts +0 -35
- package/dist/plugin-auth/src/oauth2-auth.d.ts.map +0 -1
- package/dist/plugin-auth/src/oauth2-auth.js +0 -266
- package/dist/plugin-auth/src/oauth2-auth.js.map +0 -1
- package/dist/plugin-http/src/index.d.ts +0 -4
- package/dist/plugin-http/src/index.d.ts.map +0 -1
- package/dist/plugin-http/src/index.js +0 -266
- package/dist/plugin-http/src/index.js.map +0 -1
- package/dist/plugin-vault-file/src/index.d.ts +0 -67
- package/dist/plugin-vault-file/src/index.d.ts.map +0 -1
- package/dist/plugin-vault-file/src/index.js +0 -171
- package/dist/plugin-vault-file/src/index.js.map +0 -1
- package/dist/types.d.ts +0 -374
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -13
- package/dist/types.js.map +0 -1
package/src/ScriptEngine.ts
CHANGED
|
@@ -1,219 +1,219 @@
|
|
|
1
|
-
import vm from 'vm';
|
|
2
|
-
import { expect } from 'chai';
|
|
3
|
-
import _ from 'lodash';
|
|
4
|
-
import type { ExecutionContext, ScriptResult, TestResult } from '@apiquest/types';
|
|
5
|
-
import { ScriptType } from '@apiquest/types';
|
|
6
|
-
import { Logger } from './Logger.js';
|
|
7
|
-
import { createQuestAPI } from './QuestAPI.js';
|
|
8
|
-
import { isNullOrWhitespace } from './utils.js';
|
|
9
|
-
|
|
10
|
-
interface ConsoleAPI {
|
|
11
|
-
log(...args: unknown[]): void;
|
|
12
|
-
info(...args: unknown[]): void;
|
|
13
|
-
warn(...args: unknown[]): void;
|
|
14
|
-
error(...args: unknown[]): void;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export class ScriptEngine {
|
|
18
|
-
private tests: TestResult[] = [];
|
|
19
|
-
private consoleOutput: string[] = [];
|
|
20
|
-
private logger: Logger;
|
|
21
|
-
|
|
22
|
-
constructor(baseLogger?: Logger) {
|
|
23
|
-
this.logger = baseLogger?.createLogger('ScriptEngine') ?? new Logger('ScriptEngine');
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Execute a script in VM context with quest API
|
|
28
|
-
*/
|
|
29
|
-
async execute(
|
|
30
|
-
script: string,
|
|
31
|
-
context: ExecutionContext,
|
|
32
|
-
scriptType: ScriptType,
|
|
33
|
-
emitAssertion: (test: TestResult) => void
|
|
34
|
-
): Promise<ScriptResult> {
|
|
35
|
-
if (isNullOrWhitespace(script)) {
|
|
36
|
-
this.logger.trace('Empty script, skipping execution');
|
|
37
|
-
return {
|
|
38
|
-
success: true,
|
|
39
|
-
tests: [],
|
|
40
|
-
consoleOutput: []
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const scriptPreview = script.length > 100 ? script.substring(0, 100) + '...' : script;
|
|
45
|
-
this.logger.debug(`Executing ${scriptType} script (${script.length} chars)`);
|
|
46
|
-
this.logger.trace(`Script preview: ${scriptPreview}`);
|
|
47
|
-
|
|
48
|
-
this.tests = [];
|
|
49
|
-
this.consoleOutput = [];
|
|
50
|
-
|
|
51
|
-
try {
|
|
52
|
-
this.logger.trace('Creating quest API sandbox');
|
|
53
|
-
const questAPI = createQuestAPI(context, scriptType, this.tests, emitAssertion);
|
|
54
|
-
|
|
55
|
-
const sandbox = {
|
|
56
|
-
quest: questAPI,
|
|
57
|
-
expect,
|
|
58
|
-
_,
|
|
59
|
-
console: this.createConsoleAPI(),
|
|
60
|
-
require: this.createRequire(),
|
|
61
|
-
setTimeout,
|
|
62
|
-
setInterval,
|
|
63
|
-
clearTimeout,
|
|
64
|
-
clearInterval,
|
|
65
|
-
Promise,
|
|
66
|
-
Buffer,
|
|
67
|
-
AbortController,
|
|
68
|
-
AbortSignal,
|
|
69
|
-
signal: context.abortSignal
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
const vmContext = vm.createContext(sandbox);
|
|
73
|
-
this.logger.trace('VM context created');
|
|
74
|
-
|
|
75
|
-
// Wrap script in async function to support top-level await
|
|
76
|
-
const wrappedScript = `
|
|
77
|
-
(async () => {
|
|
78
|
-
${script}
|
|
79
|
-
})()
|
|
80
|
-
`;
|
|
81
|
-
|
|
82
|
-
// Execute script and get the promise
|
|
83
|
-
const startTime = Date.now();
|
|
84
|
-
const result = vm.runInContext(wrappedScript, vmContext, {
|
|
85
|
-
timeout: 30000,
|
|
86
|
-
displayErrors: true
|
|
87
|
-
}) as unknown;
|
|
88
|
-
|
|
89
|
-
// If result is a promise, wait for it
|
|
90
|
-
if (result !== null && result !== undefined && typeof (result as { then?: unknown }).then === 'function') {
|
|
91
|
-
await (result as Promise<void>);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// Wait for any other pending promises
|
|
95
|
-
await new Promise(resolve => setImmediate(resolve));
|
|
96
|
-
|
|
97
|
-
const duration = Date.now() - startTime;
|
|
98
|
-
this.logger.debug(`Script executed successfully in ${duration}ms`);
|
|
99
|
-
this.logger.trace(`Tests collected: ${this.tests.length}, Console output: ${this.consoleOutput.length} lines`);
|
|
100
|
-
|
|
101
|
-
return {
|
|
102
|
-
success: true,
|
|
103
|
-
tests: this.tests,
|
|
104
|
-
consoleOutput: this.consoleOutput
|
|
105
|
-
};
|
|
106
|
-
} catch (error: unknown) {
|
|
107
|
-
const errorMsg = (error as { message?: string }).message ?? String(error);
|
|
108
|
-
const errorStack = (error as { stack?: string }).stack;
|
|
109
|
-
const errorName = (error as { name?: string }).name;
|
|
110
|
-
|
|
111
|
-
// Check if this was an abort
|
|
112
|
-
if (errorName === 'AbortError' || errorMsg.includes('abort') || errorMsg.includes('Abort')) {
|
|
113
|
-
this.logger.debug('Script execution interrupted by abort signal');
|
|
114
|
-
return {
|
|
115
|
-
success: false,
|
|
116
|
-
tests: this.tests,
|
|
117
|
-
error: 'Script aborted',
|
|
118
|
-
consoleOutput: this.consoleOutput
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
this.logger.error(`Script execution failed: ${errorMsg}`);
|
|
123
|
-
if (errorStack !== undefined) {
|
|
124
|
-
this.logger.trace(`Error stack: ${errorStack}`);
|
|
125
|
-
}
|
|
126
|
-
return {
|
|
127
|
-
success: false,
|
|
128
|
-
tests: this.tests,
|
|
129
|
-
error: errorMsg,
|
|
130
|
-
consoleOutput: this.consoleOutput
|
|
131
|
-
};
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Create console API that captures output
|
|
137
|
-
*/
|
|
138
|
-
private createConsoleAPI(): ConsoleAPI {
|
|
139
|
-
const self = this;
|
|
140
|
-
|
|
141
|
-
const safeStringify = (value: unknown): string => {
|
|
142
|
-
if (typeof value === 'string') return value;
|
|
143
|
-
if (value === null || value === undefined) return String(value);
|
|
144
|
-
if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') return String(value);
|
|
145
|
-
if (typeof value === 'symbol') return value.toString();
|
|
146
|
-
if (typeof value === 'function') return '[Function]';
|
|
147
|
-
|
|
148
|
-
try {
|
|
149
|
-
const seen = new WeakSet<object>();
|
|
150
|
-
return JSON.stringify(
|
|
151
|
-
value,
|
|
152
|
-
(_key, val: unknown) => {
|
|
153
|
-
if (typeof val === 'bigint') {
|
|
154
|
-
return val.toString();
|
|
155
|
-
}
|
|
156
|
-
if (typeof val === 'function') {
|
|
157
|
-
return '[Function]';
|
|
158
|
-
}
|
|
159
|
-
if (typeof val === 'symbol') {
|
|
160
|
-
return val.toString();
|
|
161
|
-
}
|
|
162
|
-
if (typeof val === 'object' && val !== null) {
|
|
163
|
-
if (seen.has(val)) {
|
|
164
|
-
return '[Circular]';
|
|
165
|
-
}
|
|
166
|
-
seen.add(val);
|
|
167
|
-
}
|
|
168
|
-
return val as string | number | boolean | null;
|
|
169
|
-
},
|
|
170
|
-
2
|
|
171
|
-
);
|
|
172
|
-
} catch {
|
|
173
|
-
return String(value);
|
|
174
|
-
}
|
|
175
|
-
};
|
|
176
|
-
|
|
177
|
-
return {
|
|
178
|
-
log(...args: unknown[]) {
|
|
179
|
-
const message = args.map(safeStringify).join(' ');
|
|
180
|
-
self.consoleOutput.push(message);
|
|
181
|
-
console.log(message);
|
|
182
|
-
},
|
|
183
|
-
info(...args: unknown[]) {
|
|
184
|
-
const message = args.map(safeStringify).join(' ');
|
|
185
|
-
self.consoleOutput.push(`[INFO] ${message}`);
|
|
186
|
-
console.info(message);
|
|
187
|
-
},
|
|
188
|
-
warn(...args: unknown[]) {
|
|
189
|
-
const message = args.map(safeStringify).join(' ');
|
|
190
|
-
self.consoleOutput.push(`[WARN] ${message}`);
|
|
191
|
-
console.warn(message);
|
|
192
|
-
},
|
|
193
|
-
error(...args: unknown[]) {
|
|
194
|
-
const message = args.map(safeStringify).join(' ');
|
|
195
|
-
self.consoleOutput.push(`[ERROR] ${message}`);
|
|
196
|
-
console.error(message);
|
|
197
|
-
}
|
|
198
|
-
};
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
/**
|
|
202
|
-
* Create minimal require function
|
|
203
|
-
*/
|
|
204
|
-
private createRequire() {
|
|
205
|
-
return (moduleName: string) => {
|
|
206
|
-
const allowedModules: Record<string, unknown> = {
|
|
207
|
-
'chai': require('chai') as unknown,
|
|
208
|
-
'lodash': require('lodash') as unknown,
|
|
209
|
-
'moment': require('moment') as unknown
|
|
210
|
-
};
|
|
211
|
-
|
|
212
|
-
if (moduleName in allowedModules) {
|
|
213
|
-
return allowedModules[moduleName];
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
throw new Error(`Module '${moduleName}' is not allowed in scripts`);
|
|
217
|
-
};
|
|
218
|
-
}
|
|
219
|
-
}
|
|
1
|
+
import vm from 'vm';
|
|
2
|
+
import { expect } from 'chai';
|
|
3
|
+
import _ from 'lodash';
|
|
4
|
+
import type { ExecutionContext, ScriptResult, TestResult } from '@apiquest/types';
|
|
5
|
+
import { ScriptType } from '@apiquest/types';
|
|
6
|
+
import { Logger } from './Logger.js';
|
|
7
|
+
import { createQuestAPI } from './QuestAPI.js';
|
|
8
|
+
import { isNullOrWhitespace } from './utils.js';
|
|
9
|
+
|
|
10
|
+
interface ConsoleAPI {
|
|
11
|
+
log(...args: unknown[]): void;
|
|
12
|
+
info(...args: unknown[]): void;
|
|
13
|
+
warn(...args: unknown[]): void;
|
|
14
|
+
error(...args: unknown[]): void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class ScriptEngine {
|
|
18
|
+
private tests: TestResult[] = [];
|
|
19
|
+
private consoleOutput: string[] = [];
|
|
20
|
+
private logger: Logger;
|
|
21
|
+
|
|
22
|
+
constructor(baseLogger?: Logger) {
|
|
23
|
+
this.logger = baseLogger?.createLogger('ScriptEngine') ?? new Logger('ScriptEngine');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Execute a script in VM context with quest API
|
|
28
|
+
*/
|
|
29
|
+
async execute(
|
|
30
|
+
script: string,
|
|
31
|
+
context: ExecutionContext,
|
|
32
|
+
scriptType: ScriptType,
|
|
33
|
+
emitAssertion: (test: TestResult) => void
|
|
34
|
+
): Promise<ScriptResult> {
|
|
35
|
+
if (isNullOrWhitespace(script)) {
|
|
36
|
+
this.logger.trace('Empty script, skipping execution');
|
|
37
|
+
return {
|
|
38
|
+
success: true,
|
|
39
|
+
tests: [],
|
|
40
|
+
consoleOutput: []
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const scriptPreview = script.length > 100 ? script.substring(0, 100) + '...' : script;
|
|
45
|
+
this.logger.debug(`Executing ${scriptType} script (${script.length} chars)`);
|
|
46
|
+
this.logger.trace(`Script preview: ${scriptPreview}`);
|
|
47
|
+
|
|
48
|
+
this.tests = [];
|
|
49
|
+
this.consoleOutput = [];
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
this.logger.trace('Creating quest API sandbox');
|
|
53
|
+
const questAPI = createQuestAPI(context, scriptType, this.tests, emitAssertion);
|
|
54
|
+
|
|
55
|
+
const sandbox = {
|
|
56
|
+
quest: questAPI,
|
|
57
|
+
expect,
|
|
58
|
+
_,
|
|
59
|
+
console: this.createConsoleAPI(),
|
|
60
|
+
require: this.createRequire(),
|
|
61
|
+
setTimeout,
|
|
62
|
+
setInterval,
|
|
63
|
+
clearTimeout,
|
|
64
|
+
clearInterval,
|
|
65
|
+
Promise,
|
|
66
|
+
Buffer,
|
|
67
|
+
AbortController,
|
|
68
|
+
AbortSignal,
|
|
69
|
+
signal: context.abortSignal
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const vmContext = vm.createContext(sandbox);
|
|
73
|
+
this.logger.trace('VM context created');
|
|
74
|
+
|
|
75
|
+
// Wrap script in async function to support top-level await
|
|
76
|
+
const wrappedScript = `
|
|
77
|
+
(async () => {
|
|
78
|
+
${script}
|
|
79
|
+
})()
|
|
80
|
+
`;
|
|
81
|
+
|
|
82
|
+
// Execute script and get the promise
|
|
83
|
+
const startTime = Date.now();
|
|
84
|
+
const result = vm.runInContext(wrappedScript, vmContext, {
|
|
85
|
+
timeout: 30000,
|
|
86
|
+
displayErrors: true
|
|
87
|
+
}) as unknown;
|
|
88
|
+
|
|
89
|
+
// If result is a promise, wait for it
|
|
90
|
+
if (result !== null && result !== undefined && typeof (result as { then?: unknown }).then === 'function') {
|
|
91
|
+
await (result as Promise<void>);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Wait for any other pending promises
|
|
95
|
+
await new Promise(resolve => setImmediate(resolve));
|
|
96
|
+
|
|
97
|
+
const duration = Date.now() - startTime;
|
|
98
|
+
this.logger.debug(`Script executed successfully in ${duration}ms`);
|
|
99
|
+
this.logger.trace(`Tests collected: ${this.tests.length}, Console output: ${this.consoleOutput.length} lines`);
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
success: true,
|
|
103
|
+
tests: this.tests,
|
|
104
|
+
consoleOutput: this.consoleOutput
|
|
105
|
+
};
|
|
106
|
+
} catch (error: unknown) {
|
|
107
|
+
const errorMsg = (error as { message?: string }).message ?? String(error);
|
|
108
|
+
const errorStack = (error as { stack?: string }).stack;
|
|
109
|
+
const errorName = (error as { name?: string }).name;
|
|
110
|
+
|
|
111
|
+
// Check if this was an abort
|
|
112
|
+
if (errorName === 'AbortError' || errorMsg.includes('abort') || errorMsg.includes('Abort')) {
|
|
113
|
+
this.logger.debug('Script execution interrupted by abort signal');
|
|
114
|
+
return {
|
|
115
|
+
success: false,
|
|
116
|
+
tests: this.tests,
|
|
117
|
+
error: 'Script aborted',
|
|
118
|
+
consoleOutput: this.consoleOutput
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
this.logger.error(`Script execution failed: ${errorMsg}`);
|
|
123
|
+
if (errorStack !== undefined) {
|
|
124
|
+
this.logger.trace(`Error stack: ${errorStack}`);
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
success: false,
|
|
128
|
+
tests: this.tests,
|
|
129
|
+
error: errorMsg,
|
|
130
|
+
consoleOutput: this.consoleOutput
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Create console API that captures output
|
|
137
|
+
*/
|
|
138
|
+
private createConsoleAPI(): ConsoleAPI {
|
|
139
|
+
const self = this;
|
|
140
|
+
|
|
141
|
+
const safeStringify = (value: unknown): string => {
|
|
142
|
+
if (typeof value === 'string') return value;
|
|
143
|
+
if (value === null || value === undefined) return String(value);
|
|
144
|
+
if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') return String(value);
|
|
145
|
+
if (typeof value === 'symbol') return value.toString();
|
|
146
|
+
if (typeof value === 'function') return '[Function]';
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
const seen = new WeakSet<object>();
|
|
150
|
+
return JSON.stringify(
|
|
151
|
+
value,
|
|
152
|
+
(_key, val: unknown) => {
|
|
153
|
+
if (typeof val === 'bigint') {
|
|
154
|
+
return val.toString();
|
|
155
|
+
}
|
|
156
|
+
if (typeof val === 'function') {
|
|
157
|
+
return '[Function]';
|
|
158
|
+
}
|
|
159
|
+
if (typeof val === 'symbol') {
|
|
160
|
+
return val.toString();
|
|
161
|
+
}
|
|
162
|
+
if (typeof val === 'object' && val !== null) {
|
|
163
|
+
if (seen.has(val)) {
|
|
164
|
+
return '[Circular]';
|
|
165
|
+
}
|
|
166
|
+
seen.add(val);
|
|
167
|
+
}
|
|
168
|
+
return val as string | number | boolean | null;
|
|
169
|
+
},
|
|
170
|
+
2
|
|
171
|
+
);
|
|
172
|
+
} catch {
|
|
173
|
+
return String(value);
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
log(...args: unknown[]) {
|
|
179
|
+
const message = args.map(safeStringify).join(' ');
|
|
180
|
+
self.consoleOutput.push(message);
|
|
181
|
+
console.log(message);
|
|
182
|
+
},
|
|
183
|
+
info(...args: unknown[]) {
|
|
184
|
+
const message = args.map(safeStringify).join(' ');
|
|
185
|
+
self.consoleOutput.push(`[INFO] ${message}`);
|
|
186
|
+
console.info(message);
|
|
187
|
+
},
|
|
188
|
+
warn(...args: unknown[]) {
|
|
189
|
+
const message = args.map(safeStringify).join(' ');
|
|
190
|
+
self.consoleOutput.push(`[WARN] ${message}`);
|
|
191
|
+
console.warn(message);
|
|
192
|
+
},
|
|
193
|
+
error(...args: unknown[]) {
|
|
194
|
+
const message = args.map(safeStringify).join(' ');
|
|
195
|
+
self.consoleOutput.push(`[ERROR] ${message}`);
|
|
196
|
+
console.error(message);
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Create minimal require function
|
|
203
|
+
*/
|
|
204
|
+
private createRequire() {
|
|
205
|
+
return (moduleName: string) => {
|
|
206
|
+
const allowedModules: Record<string, unknown> = {
|
|
207
|
+
'chai': require('chai') as unknown,
|
|
208
|
+
'lodash': require('lodash') as unknown,
|
|
209
|
+
'moment': require('moment') as unknown
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
if (moduleName in allowedModules) {
|
|
213
|
+
return allowedModules[moduleName];
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
throw new Error(`Module '${moduleName}' is not allowed in scripts`);
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
}
|