@apiquest/fracture 1.0.4 → 1.0.5
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 +90 -2
- package/dist/CollectionRunner.d.ts +2 -0
- package/dist/CollectionRunner.d.ts.map +1 -1
- package/dist/CollectionRunner.js +20 -2
- package/dist/CollectionRunner.js.map +1 -1
- package/dist/LibraryLoader.d.ts +49 -0
- package/dist/LibraryLoader.d.ts.map +1 -0
- package/dist/LibraryLoader.js +198 -0
- package/dist/LibraryLoader.js.map +1 -0
- package/dist/PluginLoader.d.ts.map +1 -1
- package/dist/PluginLoader.js +9 -6
- package/dist/PluginLoader.js.map +1 -1
- package/dist/PluginResolver.d.ts +1 -1
- package/dist/PluginResolver.d.ts.map +1 -1
- package/dist/PluginResolver.js +1 -1
- package/dist/PluginResolver.js.map +1 -1
- package/dist/ScriptEngine.d.ts +2 -1
- package/dist/ScriptEngine.d.ts.map +1 -1
- package/dist/ScriptEngine.js +15 -8
- package/dist/ScriptEngine.js.map +1 -1
- package/dist/cli/index.js +35 -3
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/plugin-commands.d.ts.map +1 -1
- package/dist/cli/plugin-commands.js +47 -81
- package/dist/cli/plugin-commands.js.map +1 -1
- package/dist/cli/plugin-installer.d.ts +48 -0
- package/dist/cli/plugin-installer.d.ts.map +1 -0
- package/dist/cli/plugin-installer.js +136 -0
- package/dist/cli/plugin-installer.js.map +1 -0
- package/dist/cli/plugin-registry.d.ts +17 -0
- package/dist/cli/plugin-registry.d.ts.map +1 -0
- package/dist/cli/plugin-registry.js +77 -0
- package/dist/cli/plugin-registry.js.map +1 -0
- package/package.json +1 -1
- package/src/CollectionAnalyzer.ts +0 -102
- package/src/CollectionRunner.ts +0 -1423
- package/src/CollectionRunner.types.ts +0 -9
- package/src/CollectionValidator.ts +0 -289
- package/src/ConsoleReporter.ts +0 -143
- package/src/CookieJar.ts +0 -258
- package/src/DagScheduler.ts +0 -439
- package/src/Logger.ts +0 -85
- package/src/PluginLoader.ts +0 -126
- package/src/PluginManager.ts +0 -208
- package/src/PluginResolver.ts +0 -154
- package/src/QuestAPI.ts +0 -764
- package/src/QuestAPI.types.ts +0 -33
- package/src/QuestTestAPI.ts +0 -164
- package/src/RequestFilter.ts +0 -224
- package/src/ScriptEngine.ts +0 -219
- package/src/ScriptValidator.ts +0 -428
- package/src/TaskGraph.ts +0 -598
- package/src/TestCounter.ts +0 -109
- package/src/VariableResolver.ts +0 -114
- package/src/cli/index.ts +0 -480
- package/src/cli/plugin-commands.ts +0 -342
- package/src/cli/plugin-discovery.ts +0 -44
- package/src/index.ts +0 -24
- package/src/utils.ts +0 -52
package/src/QuestAPI.types.ts
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
// Type definitions for Quest API
|
|
2
|
-
|
|
3
|
-
export interface RequestConfig {
|
|
4
|
-
url: string;
|
|
5
|
-
method?: string;
|
|
6
|
-
header?: Record<string, string>;
|
|
7
|
-
headers?: Record<string, string>;
|
|
8
|
-
body?: string | RequestBody;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export interface RequestBody {
|
|
12
|
-
mode?: 'raw' | 'urlencoded' | 'formdata';
|
|
13
|
-
raw?: string;
|
|
14
|
-
urlencoded?: Array<{ key: string; value: string }>;
|
|
15
|
-
formdata?: Array<{ key: string; value: string }>;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export interface ResponseObject {
|
|
19
|
-
status: number;
|
|
20
|
-
statusText: string;
|
|
21
|
-
body: string;
|
|
22
|
-
headers: Record<string, string | string[]>;
|
|
23
|
-
time: number;
|
|
24
|
-
json(): unknown | null;
|
|
25
|
-
text(): string;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export interface HistoryFilterCriteria {
|
|
29
|
-
path?: string;
|
|
30
|
-
name?: string;
|
|
31
|
-
iteration?: number;
|
|
32
|
-
id?: string;
|
|
33
|
-
}
|
package/src/QuestTestAPI.ts
DELETED
|
@@ -1,164 +0,0 @@
|
|
|
1
|
-
import type { TestResult } from '@apiquest/types';
|
|
2
|
-
import { ScriptType } from '@apiquest/types';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Special error class for quest.skip()
|
|
6
|
-
*/
|
|
7
|
-
class SkipError extends Error {
|
|
8
|
-
public readonly skipReason: string;
|
|
9
|
-
|
|
10
|
-
constructor(message: string) {
|
|
11
|
-
super(message);
|
|
12
|
-
this.name = 'SkipError';
|
|
13
|
-
this.skipReason = message;
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function isPromise(value: unknown): value is Promise<void> {
|
|
18
|
-
return value !== null &&
|
|
19
|
-
value !== undefined &&
|
|
20
|
-
typeof value === 'object' &&
|
|
21
|
-
typeof (value as { then?: unknown }).then === 'function';
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Creates the test API methods (quest.test, quest.skip, quest.fail)
|
|
26
|
-
* These methods allow scripts to define and control test assertions
|
|
27
|
-
*/
|
|
28
|
-
export function createQuestTestAPI(
|
|
29
|
-
tests: TestResult[],
|
|
30
|
-
scriptType: ScriptType,
|
|
31
|
-
emitAssertion: (test: TestResult) => void,
|
|
32
|
-
abortSignal?: AbortSignal
|
|
33
|
-
): {
|
|
34
|
-
test: (name: string, fn: () => void | Promise<void>) => void;
|
|
35
|
-
skip: (reason: string) => never;
|
|
36
|
-
fail: (message: string) => never;
|
|
37
|
-
} {
|
|
38
|
-
let isInsideTest = false;
|
|
39
|
-
|
|
40
|
-
return {
|
|
41
|
-
/**
|
|
42
|
-
* Define a test assertion
|
|
43
|
-
* Can only be called in postRequestScript or plugin event scripts
|
|
44
|
-
*/
|
|
45
|
-
test(name: string, fn: () => void | Promise<void>) {
|
|
46
|
-
// Enforce: tests can only be called in request post scripts (never pre, folder, or collection)
|
|
47
|
-
const allowedScriptTypes = [
|
|
48
|
-
ScriptType.PostRequest,
|
|
49
|
-
ScriptType.PluginEvent
|
|
50
|
-
];
|
|
51
|
-
if (!allowedScriptTypes.includes(scriptType)) {
|
|
52
|
-
throw new Error(
|
|
53
|
-
`quest.test() can only be called in request post scripts. ` +
|
|
54
|
-
`Current script type: ${scriptType}. ` +
|
|
55
|
-
`Tests require request/response context and cannot be used in ` +
|
|
56
|
-
`collectionPost, folderPost, or pre-request scripts.`
|
|
57
|
-
);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Check abort signal - skip test if already aborted
|
|
61
|
-
if (abortSignal?.aborted === true) {
|
|
62
|
-
const testResult: TestResult = {
|
|
63
|
-
name,
|
|
64
|
-
passed: false,
|
|
65
|
-
skipped: true,
|
|
66
|
-
error: 'Test skipped - execution aborted'
|
|
67
|
-
};
|
|
68
|
-
tests.push(testResult);
|
|
69
|
-
emitAssertion(testResult);
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
isInsideTest = true;
|
|
74
|
-
try {
|
|
75
|
-
const result = fn();
|
|
76
|
-
|
|
77
|
-
// If fn returns a Promise, handle it
|
|
78
|
-
if (isPromise(result)) {
|
|
79
|
-
result
|
|
80
|
-
.then(() => {
|
|
81
|
-
const testResult: TestResult = {
|
|
82
|
-
name,
|
|
83
|
-
passed: true,
|
|
84
|
-
skipped: false
|
|
85
|
-
};
|
|
86
|
-
tests.push(testResult);
|
|
87
|
-
emitAssertion(testResult);
|
|
88
|
-
})
|
|
89
|
-
.catch((error: unknown) => {
|
|
90
|
-
// Check if this is a skip error
|
|
91
|
-
const testResult: TestResult = error instanceof SkipError
|
|
92
|
-
? {
|
|
93
|
-
name,
|
|
94
|
-
passed: false,
|
|
95
|
-
skipped: true,
|
|
96
|
-
error: error.skipReason
|
|
97
|
-
}
|
|
98
|
-
: {
|
|
99
|
-
name,
|
|
100
|
-
passed: false,
|
|
101
|
-
skipped: false,
|
|
102
|
-
error: (error as { message?: string }).message ?? String(error)
|
|
103
|
-
};
|
|
104
|
-
tests.push(testResult);
|
|
105
|
-
emitAssertion(testResult);
|
|
106
|
-
})
|
|
107
|
-
.finally(() => {
|
|
108
|
-
isInsideTest = false;
|
|
109
|
-
});
|
|
110
|
-
} else {
|
|
111
|
-
// Synchronous test
|
|
112
|
-
const testResult: TestResult = {
|
|
113
|
-
name,
|
|
114
|
-
passed: true,
|
|
115
|
-
skipped: false
|
|
116
|
-
};
|
|
117
|
-
tests.push(testResult);
|
|
118
|
-
emitAssertion(testResult);
|
|
119
|
-
isInsideTest = false;
|
|
120
|
-
}
|
|
121
|
-
} catch (error: unknown) {
|
|
122
|
-
// Check if this is a skip error
|
|
123
|
-
const testResult: TestResult = error instanceof SkipError
|
|
124
|
-
? {
|
|
125
|
-
name,
|
|
126
|
-
passed: false,
|
|
127
|
-
skipped: true,
|
|
128
|
-
error: error.skipReason // Include skip reason in error field
|
|
129
|
-
}
|
|
130
|
-
: {
|
|
131
|
-
name,
|
|
132
|
-
passed: false,
|
|
133
|
-
skipped: false,
|
|
134
|
-
error: (error as { message?: string }).message ?? String(error)
|
|
135
|
-
};
|
|
136
|
-
tests.push(testResult);
|
|
137
|
-
emitAssertion(testResult);
|
|
138
|
-
isInsideTest = false;
|
|
139
|
-
}
|
|
140
|
-
},
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* Skip the current test
|
|
144
|
-
* Must be called inside quest.test() callback
|
|
145
|
-
*/
|
|
146
|
-
skip(reason: string): never {
|
|
147
|
-
if (!isInsideTest) {
|
|
148
|
-
throw new Error('quest.skip() must be called inside quest.test() callback');
|
|
149
|
-
}
|
|
150
|
-
throw new SkipError(reason);
|
|
151
|
-
},
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Fail the current test with custom message
|
|
155
|
-
* Must be called inside quest.test() callback
|
|
156
|
-
*/
|
|
157
|
-
fail(message: string): never {
|
|
158
|
-
if (!isInsideTest) {
|
|
159
|
-
throw new Error('quest.fail() must be called inside quest.test() callback');
|
|
160
|
-
}
|
|
161
|
-
throw new Error(message);
|
|
162
|
-
}
|
|
163
|
-
};
|
|
164
|
-
}
|
package/src/RequestFilter.ts
DELETED
|
@@ -1,224 +0,0 @@
|
|
|
1
|
-
import type { Collection, CollectionItem, Folder, Request, PathType } from '@apiquest/types';
|
|
2
|
-
|
|
3
|
-
export interface FilterOptions {
|
|
4
|
-
filter?: string;
|
|
5
|
-
excludeDeps?: boolean;
|
|
6
|
-
pruneEmptyFolders?: boolean; // Default: true
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
interface ItemWithPath {
|
|
10
|
-
item: CollectionItem;
|
|
11
|
-
path: PathType;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export class RequestFilter {
|
|
15
|
-
/**
|
|
16
|
-
* Filter collection removing non-matching requests and empty folders
|
|
17
|
-
* Returns filtered copy or original if no filtering
|
|
18
|
-
*/
|
|
19
|
-
static filterCollection(collection: Collection, options: FilterOptions): Collection {
|
|
20
|
-
if (options.filter === undefined) {
|
|
21
|
-
return collection;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const filterSet = this.getFilterSet(collection, options);
|
|
25
|
-
if (filterSet === null) {
|
|
26
|
-
return collection;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const pruneEmpty = options.pruneEmptyFolders !== false;
|
|
30
|
-
|
|
31
|
-
// Clone collection and filter items
|
|
32
|
-
const filtered: Collection = {
|
|
33
|
-
...collection,
|
|
34
|
-
items: this.filterItems(collection.items, filterSet, pruneEmpty)
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
return filtered;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Recursively filter items keeping only requests in filterSet
|
|
42
|
-
* Optionally prune empty folders
|
|
43
|
-
*/
|
|
44
|
-
private static filterItems(
|
|
45
|
-
items: CollectionItem[],
|
|
46
|
-
filterSet: Set<string>,
|
|
47
|
-
pruneEmpty: boolean
|
|
48
|
-
): CollectionItem[] {
|
|
49
|
-
const filtered: CollectionItem[] = [];
|
|
50
|
-
|
|
51
|
-
for (const item of items) {
|
|
52
|
-
if (item.type === 'request') {
|
|
53
|
-
if (filterSet.has(item.id)) {
|
|
54
|
-
filtered.push(item);
|
|
55
|
-
}
|
|
56
|
-
} else {
|
|
57
|
-
// Folder: recursively filter children
|
|
58
|
-
const filteredChildren = this.filterItems(item.items, filterSet, pruneEmpty);
|
|
59
|
-
|
|
60
|
-
if (!pruneEmpty || filteredChildren.length > 0) {
|
|
61
|
-
filtered.push({
|
|
62
|
-
...item,
|
|
63
|
-
items: filteredChildren
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
return filtered;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Get set of request IDs to execute
|
|
74
|
-
* Returns null if no filtering needed
|
|
75
|
-
*/
|
|
76
|
-
private static getFilterSet(collection: Collection, options: FilterOptions): Set<string> | null {
|
|
77
|
-
if (options.filter === undefined) {
|
|
78
|
-
return null;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// Collect requests matching filter
|
|
82
|
-
const matchingIds = new Set<string>();
|
|
83
|
-
try {
|
|
84
|
-
const filterRegex = new RegExp(options.filter);
|
|
85
|
-
|
|
86
|
-
// Walk collection structure and match paths
|
|
87
|
-
this.collectMatchingRequests(collection.items, 'collection:/', filterRegex, matchingIds);
|
|
88
|
-
} catch (error) {
|
|
89
|
-
// Invalid regex - no filtering
|
|
90
|
-
return null;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
if (options.excludeDeps !== true) {
|
|
94
|
-
return this.includeDependencies(collection, matchingIds);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
return matchingIds;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Build path with proper type prefix (same logic as TaskGraph.buildPath)
|
|
102
|
-
*/
|
|
103
|
-
private static buildPath(parent: string, name: string, type: 'folder' | 'request'): PathType {
|
|
104
|
-
// If parent is collection:/
|
|
105
|
-
if (parent === 'collection:/') {
|
|
106
|
-
return `${type}:/${name}` as PathType;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// Remove type prefix from parent path
|
|
110
|
-
const basePath = parent.replace(/^(folder|request):\//, '');
|
|
111
|
-
return `${type}:/${basePath}/${name}` as PathType;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Collect requests matching path filter (regex)
|
|
116
|
-
* Matches against both folder and request paths
|
|
117
|
-
* If a folder matches, all requests in that folder are included
|
|
118
|
-
*/
|
|
119
|
-
private static collectMatchingRequests(
|
|
120
|
-
items: CollectionItem[],
|
|
121
|
-
parentPath: string,
|
|
122
|
-
filterRegex: RegExp,
|
|
123
|
-
result: Set<string>
|
|
124
|
-
): void {
|
|
125
|
-
for (const item of items) {
|
|
126
|
-
if (item.type === 'folder') {
|
|
127
|
-
const folderPath = this.buildPath(parentPath, item.name, 'folder');
|
|
128
|
-
const folderMatches = filterRegex.test(folderPath);
|
|
129
|
-
|
|
130
|
-
if (folderMatches) {
|
|
131
|
-
// Folder matches - include ALL requests in this folder
|
|
132
|
-
this.collectAllRequests(item.items, result);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// Always recurse to children
|
|
136
|
-
this.collectMatchingRequests(item.items, folderPath, filterRegex, result);
|
|
137
|
-
} else {
|
|
138
|
-
// Request
|
|
139
|
-
const requestPath = this.buildPath(parentPath, item.name, 'request');
|
|
140
|
-
const requestMatches = filterRegex.test(requestPath);
|
|
141
|
-
|
|
142
|
-
if (requestMatches) {
|
|
143
|
-
result.add(item.id);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Collect all requests from items (helper for when folder matches)
|
|
151
|
-
*/
|
|
152
|
-
private static collectAllRequests(items: CollectionItem[], result: Set<string>): void {
|
|
153
|
-
for (const item of items) {
|
|
154
|
-
if (item.type === 'request') {
|
|
155
|
-
result.add(item.id);
|
|
156
|
-
} else {
|
|
157
|
-
this.collectAllRequests(item.items, result);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* Include dependencies by walking collection and resolving dependsOn
|
|
164
|
-
*/
|
|
165
|
-
private static includeDependencies(
|
|
166
|
-
collection: Collection,
|
|
167
|
-
matchingIds: Set<string>
|
|
168
|
-
): Set<string> {
|
|
169
|
-
const result = new Set<string>();
|
|
170
|
-
const depMap = new Map<string, string[]>();
|
|
171
|
-
|
|
172
|
-
// Build dependency map
|
|
173
|
-
this.buildDependencyMap(collection.items, depMap);
|
|
174
|
-
|
|
175
|
-
// Resolve dependencies recursively
|
|
176
|
-
for (const requestId of matchingIds) {
|
|
177
|
-
this.resolveDependencies(requestId, depMap, result);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
return result;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* Build map of request/folder ID -> dependencies
|
|
185
|
-
*/
|
|
186
|
-
private static buildDependencyMap(
|
|
187
|
-
items: CollectionItem[],
|
|
188
|
-
depMap: Map<string, string[]>
|
|
189
|
-
): void {
|
|
190
|
-
for (const item of items) {
|
|
191
|
-
if (item.type === 'request') {
|
|
192
|
-
depMap.set(item.id, item.dependsOn ?? []);
|
|
193
|
-
} else {
|
|
194
|
-
// Folders can also have dependencies
|
|
195
|
-
const folder = item;
|
|
196
|
-
const folderDeps = folder.dependsOn;
|
|
197
|
-
if (folderDeps !== undefined && folderDeps.length > 0) {
|
|
198
|
-
depMap.set(folder.id, folderDeps);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// Recurse to children
|
|
202
|
-
this.buildDependencyMap(folder.items, depMap);
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/**
|
|
208
|
-
* Recursively resolve dependencies for a request/folder
|
|
209
|
-
*/
|
|
210
|
-
private static resolveDependencies(
|
|
211
|
-
itemId: string,
|
|
212
|
-
depMap: Map<string, string[]>,
|
|
213
|
-
result: Set<string>
|
|
214
|
-
): void {
|
|
215
|
-
result.add(itemId);
|
|
216
|
-
|
|
217
|
-
const deps = depMap.get(itemId) ?? [];
|
|
218
|
-
for (const depId of deps) {
|
|
219
|
-
if (!result.has(depId)) {
|
|
220
|
-
this.resolveDependencies(depId, depMap, result);
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
}
|
package/src/ScriptEngine.ts
DELETED
|
@@ -1,219 +0,0 @@
|
|
|
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
|
-
}
|