@apiquest/fracture 1.0.4 → 1.0.6
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 +3 -0
- package/dist/CollectionRunner.d.ts.map +1 -1
- package/dist/CollectionRunner.js +249 -154
- package/dist/CollectionRunner.js.map +1 -1
- package/dist/CollectionValidator.d.ts.map +1 -1
- package/dist/CollectionValidator.js +11 -0
- package/dist/CollectionValidator.js.map +1 -1
- package/dist/ConsoleReporter.d.ts.map +1 -1
- package/dist/ConsoleReporter.js +9 -6
- package/dist/ConsoleReporter.js.map +1 -1
- package/dist/DagScheduler.d.ts.map +1 -1
- package/dist/DagScheduler.js +11 -0
- package/dist/DagScheduler.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/PluginManager.d.ts.map +1 -1
- package/dist/PluginManager.js +11 -7
- package/dist/PluginManager.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/QuestAPI.d.ts.map +1 -1
- package/dist/QuestAPI.js +114 -217
- package/dist/QuestAPI.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/TaskGraph.d.ts +2 -1
- package/dist/TaskGraph.d.ts.map +1 -1
- package/dist/TaskGraph.js +28 -26
- package/dist/TaskGraph.js.map +1 -1
- package/dist/VariableResolver.d.ts +1 -1
- package/dist/VariableResolver.js +10 -10
- package/dist/VariableResolver.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/tsconfig.json +1 -0
- package/tsconfig.test.json +3 -0
- package/dist/QuestAPI.types.d.ts +0 -35
- package/dist/QuestAPI.types.d.ts.map +0 -1
- package/dist/QuestAPI.types.js +0 -3
- package/dist/QuestAPI.types.js.map +0 -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/CollectionRunner.ts
DELETED
|
@@ -1,1423 +0,0 @@
|
|
|
1
|
-
import { EventEmitter } from 'events';
|
|
2
|
-
import { randomUUID } from 'crypto';
|
|
3
|
-
import type {
|
|
4
|
-
Collection,
|
|
5
|
-
RunOptions,
|
|
6
|
-
RunResult,
|
|
7
|
-
Request,
|
|
8
|
-
RequestResult,
|
|
9
|
-
ExecutionContext,
|
|
10
|
-
RuntimeOptions,
|
|
11
|
-
ExecutionRecord,
|
|
12
|
-
TestResult,
|
|
13
|
-
ScriptResult,
|
|
14
|
-
CollectionRunnerOptions,
|
|
15
|
-
ScopeFrame,
|
|
16
|
-
Folder,
|
|
17
|
-
EventEnvelope,
|
|
18
|
-
CollectionInfo,
|
|
19
|
-
Cookie,
|
|
20
|
-
IProtocolPlugin,
|
|
21
|
-
IAuthPlugin
|
|
22
|
-
} from '@apiquest/types';
|
|
23
|
-
import { ScriptType, LogLevel } from '@apiquest/types';
|
|
24
|
-
import { VariableResolver } from './VariableResolver.js';
|
|
25
|
-
import { PluginManager } from './PluginManager.js';
|
|
26
|
-
import { PluginLoader } from './PluginLoader.js';
|
|
27
|
-
import { PluginResolver, type ResolvedPlugin } from './PluginResolver.js';
|
|
28
|
-
import { CollectionAnalyzer } from './CollectionAnalyzer.js';
|
|
29
|
-
import { CollectionValidator } from './CollectionValidator.js';
|
|
30
|
-
import { TestCounter } from './TestCounter.js';
|
|
31
|
-
import { ScriptEngine } from './ScriptEngine.js';
|
|
32
|
-
import { RequestFilter } from './RequestFilter.js';
|
|
33
|
-
import { Logger } from './Logger.js';
|
|
34
|
-
import { CookieJar } from './CookieJar.js';
|
|
35
|
-
import { isNullOrEmpty, isNullOrWhitespace, hasItems } from './utils.js';
|
|
36
|
-
import type { ErrorWithPhase } from './CollectionRunner.types.js';
|
|
37
|
-
import { TaskGraph, type TaskNode } from './TaskGraph.js';
|
|
38
|
-
import { DagScheduler, type DagExecutionCallbacks } from './DagScheduler.js';
|
|
39
|
-
|
|
40
|
-
export class CollectionRunner extends EventEmitter {
|
|
41
|
-
private variableResolver: VariableResolver;
|
|
42
|
-
private pluginManager: PluginManager;
|
|
43
|
-
private pluginResolver: PluginResolver;
|
|
44
|
-
private pluginLoader: PluginLoader;
|
|
45
|
-
private collectionAnalyzer: CollectionAnalyzer;
|
|
46
|
-
private collectionValidator: CollectionValidator;
|
|
47
|
-
private testCounter: TestCounter;
|
|
48
|
-
private scriptEngine: ScriptEngine;
|
|
49
|
-
private scriptQueue = Promise.resolve();
|
|
50
|
-
private pluginResolutionPromise: Promise<ResolvedPlugin[]>;
|
|
51
|
-
private resolvedPlugins: ResolvedPlugin[] = [];
|
|
52
|
-
private logger: Logger;
|
|
53
|
-
private abortController?: AbortController;
|
|
54
|
-
private ownsController = false;
|
|
55
|
-
private bailEnabled = false;
|
|
56
|
-
private abortReason?: string;
|
|
57
|
-
private shouldDelayNextRequest = false;
|
|
58
|
-
|
|
59
|
-
constructor(options?: CollectionRunnerOptions) {
|
|
60
|
-
super();
|
|
61
|
-
const logLevel = options?.logLevel ?? LogLevel.INFO;
|
|
62
|
-
|
|
63
|
-
this.logger = new Logger('CollectionRunner', logLevel, this);
|
|
64
|
-
|
|
65
|
-
this.variableResolver = new VariableResolver(this.logger);
|
|
66
|
-
this.pluginManager = new PluginManager(this.logger);
|
|
67
|
-
this.pluginResolver = new PluginResolver(this.logger);
|
|
68
|
-
this.pluginLoader = new PluginLoader(this.pluginManager, this.logger);
|
|
69
|
-
this.collectionAnalyzer = new CollectionAnalyzer(this.logger);
|
|
70
|
-
this.collectionValidator = new CollectionValidator(this.pluginManager, this.logger);
|
|
71
|
-
this.testCounter = new TestCounter(this.pluginManager, this.logger);
|
|
72
|
-
this.scriptEngine = new ScriptEngine(this.logger);
|
|
73
|
-
|
|
74
|
-
// Phase 1: Resolve plugins if directories provided (fast - just scans, no loading)
|
|
75
|
-
if (options?.pluginsDir !== undefined) {
|
|
76
|
-
const dirs = Array.isArray(options.pluginsDir)
|
|
77
|
-
? options.pluginsDir
|
|
78
|
-
: [options.pluginsDir];
|
|
79
|
-
|
|
80
|
-
// Start plugin resolution (but don't block constructor)
|
|
81
|
-
this.pluginResolutionPromise = this.pluginResolver.scanDirectories(dirs);
|
|
82
|
-
} else {
|
|
83
|
-
// No plugins to resolve
|
|
84
|
-
this.pluginResolutionPromise = Promise.resolve([]);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Queue script execution to ensure sequential execution (thread-safe)
|
|
90
|
-
*/
|
|
91
|
-
private async queueScript<T>(fn: () => Promise<T>): Promise<T> {
|
|
92
|
-
if (this.abortController?.signal.aborted === true) {
|
|
93
|
-
throw new Error('Script execution aborted');
|
|
94
|
-
}
|
|
95
|
-
const resultPromise = this.scriptQueue.then(fn);
|
|
96
|
-
this.scriptQueue = resultPromise.then(() => {}, () => {});
|
|
97
|
-
return resultPromise;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
registerPlugin(plugin: IProtocolPlugin): void {
|
|
101
|
-
this.pluginManager.registerPlugin(plugin);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
registerAuthPlugin(plugin: IAuthPlugin): void {
|
|
105
|
-
this.pluginManager.registerAuthPlugin(plugin);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
private emitConsoleOutput(messages?: string[]): void {
|
|
109
|
-
if (messages === undefined || messages.length === 0) return;
|
|
110
|
-
for (const message of messages) {
|
|
111
|
-
let level: 'log' | 'info' | 'warn' | 'error' = 'log';
|
|
112
|
-
let cleanMessage = message;
|
|
113
|
-
if (message.startsWith('[INFO] ')) {
|
|
114
|
-
level = 'info';
|
|
115
|
-
cleanMessage = message.replace('[INFO] ', '');
|
|
116
|
-
} else if (message.startsWith('[WARN] ')) {
|
|
117
|
-
level = 'warn';
|
|
118
|
-
cleanMessage = message.replace('[WARN] ', '');
|
|
119
|
-
} else if (message.startsWith('[ERROR] ')) {
|
|
120
|
-
level = 'error';
|
|
121
|
-
cleanMessage = message.replace('[ERROR] ', '');
|
|
122
|
-
}
|
|
123
|
-
this.emit('console', { id: randomUUID(), message: cleanMessage, level });
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Subscribe to ALL events emitted by the runner (including custom plugin events)
|
|
129
|
-
* Overrides emit to intercept all event emissions
|
|
130
|
-
*/
|
|
131
|
-
onAll(handler: (eventType: string, data: unknown) => void): () => void {
|
|
132
|
-
// Store original emit
|
|
133
|
-
const originalEmit = this.emit.bind(this);
|
|
134
|
-
|
|
135
|
-
// Override emit to intercept all events
|
|
136
|
-
this.emit = function(event: string | symbol, ...args: unknown[]): boolean {
|
|
137
|
-
// Call original handler first
|
|
138
|
-
const result = originalEmit(event, ...args);
|
|
139
|
-
|
|
140
|
-
// Skip internal EventEmitter events
|
|
141
|
-
if (event !== 'newListener' && event !== 'removeListener') {
|
|
142
|
-
handler(String(event), args[0]);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
return result;
|
|
146
|
-
} as typeof originalEmit;
|
|
147
|
-
|
|
148
|
-
// Return cleanup function
|
|
149
|
-
return () => {
|
|
150
|
-
// Restore original emit
|
|
151
|
-
this.emit = originalEmit;
|
|
152
|
-
};
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* Create event envelope for all events (except console)
|
|
157
|
-
*/
|
|
158
|
-
private createEventEnvelope(
|
|
159
|
-
collectionInfo: CollectionInfo,
|
|
160
|
-
path: EventEnvelope['path'],
|
|
161
|
-
context?: ExecutionContext,
|
|
162
|
-
request?: Request
|
|
163
|
-
): EventEnvelope {
|
|
164
|
-
const pathType = path === 'collection:/' ? 'collection' : path.startsWith('folder:/') ? 'folder' : 'request';
|
|
165
|
-
|
|
166
|
-
const envelope: EventEnvelope = {
|
|
167
|
-
id: randomUUID(),
|
|
168
|
-
path,
|
|
169
|
-
pathType,
|
|
170
|
-
collectionInfo
|
|
171
|
-
};
|
|
172
|
-
|
|
173
|
-
// Add iteration info if available
|
|
174
|
-
if (context !== undefined) {
|
|
175
|
-
const currentRow = context.iterationData?.[context.iterationCurrent - 1];
|
|
176
|
-
|
|
177
|
-
envelope.iteration = {
|
|
178
|
-
current: context.iterationCurrent,
|
|
179
|
-
total: context.iterationCount,
|
|
180
|
-
source: context.iterationSource,
|
|
181
|
-
rowIndex: currentRow !== undefined ? context.iterationCurrent - 1 : undefined,
|
|
182
|
-
rowKeys: currentRow !== undefined ? Object.keys(currentRow) : undefined,
|
|
183
|
-
row: currentRow
|
|
184
|
-
};
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// Add request if provided
|
|
188
|
-
if (request !== undefined) {
|
|
189
|
-
envelope.request = request;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
return envelope;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
private abort(reason: string): void {
|
|
196
|
-
this.abortReason ??= reason;
|
|
197
|
-
if (this.ownsController && this.abortController?.signal.aborted === false) {
|
|
198
|
-
this.abortController.abort(reason);
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
private isAborted(): boolean {
|
|
203
|
-
return this.abortController?.signal.aborted === true;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
private getAbortReason(): string | undefined {
|
|
207
|
-
if (this.abortReason !== undefined) {
|
|
208
|
-
return this.abortReason;
|
|
209
|
-
}
|
|
210
|
-
const signalReason = this.abortController?.signal.reason as unknown;
|
|
211
|
-
if (signalReason !== undefined) {
|
|
212
|
-
return String(signalReason);
|
|
213
|
-
}
|
|
214
|
-
return undefined;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
async run(collection: Collection, options: RunOptions = {}): Promise<RunResult> {
|
|
218
|
-
const startTime = new Date();
|
|
219
|
-
|
|
220
|
-
// Phase 1: Wait for plugin resolution
|
|
221
|
-
this.resolvedPlugins = await this.pluginResolutionPromise;
|
|
222
|
-
this.logger.debug(`Plugin resolution complete: ${this.resolvedPlugins.length} plugins available`);
|
|
223
|
-
|
|
224
|
-
// Analyze collection to determine required plugins
|
|
225
|
-
const requirements = this.collectionAnalyzer.analyzeRequirements(collection);
|
|
226
|
-
this.logger.debug(`Collection requires: protocols=[${Array.from(requirements.protocols)}], auth=[${Array.from(requirements.authTypes)}], providers=[${Array.from(requirements.valueProviders)}]`);
|
|
227
|
-
|
|
228
|
-
// Phase 2: Load ONLY required plugins
|
|
229
|
-
await this.pluginLoader.loadRequiredPlugins(this.resolvedPlugins, requirements);
|
|
230
|
-
this.logger.debug('Required plugins loaded');
|
|
231
|
-
|
|
232
|
-
this.logger.info(`Starting collection: ${collection.info.name}`);
|
|
233
|
-
this.logger.debug(`Collection ID: ${collection.info.id}, Protocol: ${collection.protocol}`);
|
|
234
|
-
|
|
235
|
-
// Validate and cache protocol plugin
|
|
236
|
-
const protocolPlugin = this.pluginManager.getPlugin(collection.protocol);
|
|
237
|
-
if (protocolPlugin === undefined) {
|
|
238
|
-
throw new Error(
|
|
239
|
-
`No plugin registered for protocol '${collection.protocol}'. ` +
|
|
240
|
-
`Available protocols: ${this.pluginManager.getAllPlugins().flatMap(p => p.protocols).join(', ')}`
|
|
241
|
-
);
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
// Merge runtime options (needed for validation)
|
|
245
|
-
const runtimeOptions = this.mergeOptions(collection.options, options);
|
|
246
|
-
|
|
247
|
-
if (options.signal !== undefined) {
|
|
248
|
-
this.ownsController = false;
|
|
249
|
-
this.abortController = {
|
|
250
|
-
signal: options.signal,
|
|
251
|
-
abort: () => {}
|
|
252
|
-
} as AbortController;
|
|
253
|
-
this.logger.info('Using external abort signal');
|
|
254
|
-
} else {
|
|
255
|
-
this.ownsController = true;
|
|
256
|
-
this.abortController = new AbortController();
|
|
257
|
-
this.logger.info('Created internal abort controller');
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
this.abortReason = undefined;
|
|
261
|
-
this.bailEnabled = runtimeOptions.execution?.bail === true;
|
|
262
|
-
|
|
263
|
-
// Get strict mode with precedence: RunOptions > Collection.options > Default (true)
|
|
264
|
-
const strictMode = options.strictMode ?? collection.options?.strictMode ?? true;
|
|
265
|
-
|
|
266
|
-
// PRE-RUN VALIDATION
|
|
267
|
-
const validationResult = await this.collectionValidator.validateCollection(collection, runtimeOptions, strictMode);
|
|
268
|
-
|
|
269
|
-
// Additional validation: cookie-jar-persist incompatible with parallel execution
|
|
270
|
-
if (runtimeOptions.execution?.allowParallel === true && runtimeOptions.jar?.persist === true) {
|
|
271
|
-
validationResult.valid = false;
|
|
272
|
-
validationResult.errors ??= [];
|
|
273
|
-
validationResult.errors.push({
|
|
274
|
-
location: '/options/execution',
|
|
275
|
-
message: 'Cookie jar persistence (jar.persist=true) is not allowed with parallel execution (allowParallel=true). ' +
|
|
276
|
-
'In parallel mode, cookies are cleared after each request to prevent race conditions.',
|
|
277
|
-
source: 'schema' // Using schema as this is a configuration validation error
|
|
278
|
-
});
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
// COUNT EXPECTED TESTS (only in strict mode for deterministic counting)
|
|
282
|
-
const expectedTestCount = strictMode ? this.testCounter.countTests(collection) : -1;
|
|
283
|
-
|
|
284
|
-
let collectionToRun = collection;
|
|
285
|
-
if (runtimeOptions.filter !== undefined) {
|
|
286
|
-
// Filter collection
|
|
287
|
-
collectionToRun = RequestFilter.filterCollection(collection, {
|
|
288
|
-
filter: runtimeOptions.filter,
|
|
289
|
-
excludeDeps: Boolean(runtimeOptions.excludeDeps)
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
if (collectionToRun !== collection) {
|
|
293
|
-
this.logger.debug('Collection filtered');
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
// Determine test data (CLI options override collection testData)
|
|
298
|
-
let iterationData = options.data ?? collectionToRun.testData ?? [];
|
|
299
|
-
|
|
300
|
-
// Apply --iterations limits
|
|
301
|
-
// 1. With data (CLI or collection): limit to first N rows
|
|
302
|
-
// 2. Without data: repeat collection N times
|
|
303
|
-
let iterationCount: number;
|
|
304
|
-
if (options.iterations !== undefined && options.iterations > 0) {
|
|
305
|
-
if (iterationData.length > 0) {
|
|
306
|
-
// With data: limit to first N rows
|
|
307
|
-
iterationData = iterationData.slice(0, options.iterations);
|
|
308
|
-
iterationCount = iterationData.length;
|
|
309
|
-
} else {
|
|
310
|
-
// Without data: repeat collection N times
|
|
311
|
-
iterationCount = options.iterations;
|
|
312
|
-
}
|
|
313
|
-
} else {
|
|
314
|
-
// No --iterations specified: use all data or run once
|
|
315
|
-
iterationCount = iterationData.length > 0 ? iterationData.length : 1;
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
// Determine iteration source for event envelopes
|
|
319
|
-
const iterationSource: 'collection' | 'cli' | 'none' =
|
|
320
|
-
options.data !== undefined ? 'cli' : (collection.testData !== undefined ? 'collection' : 'none');
|
|
321
|
-
|
|
322
|
-
// Emit beforeRun with validation results and expected test count
|
|
323
|
-
// Note: -1 means dynamic (can't determine), undefined means not calculated
|
|
324
|
-
this.emit('beforeRun', {
|
|
325
|
-
collectionInfo: {
|
|
326
|
-
id: collection.info.id,
|
|
327
|
-
name: collection.info.name,
|
|
328
|
-
version: collection.info.version,
|
|
329
|
-
description: collection.info.description
|
|
330
|
-
},
|
|
331
|
-
options,
|
|
332
|
-
validationResult,
|
|
333
|
-
expectedTestCount // Include -1 for dynamic tests
|
|
334
|
-
});
|
|
335
|
-
|
|
336
|
-
// STOP if validation failed
|
|
337
|
-
if (validationResult.valid === false) {
|
|
338
|
-
const endTime = new Date();
|
|
339
|
-
return {
|
|
340
|
-
collectionId: collection.info.id,
|
|
341
|
-
collectionName: collection.info.name,
|
|
342
|
-
startTime,
|
|
343
|
-
endTime,
|
|
344
|
-
duration: endTime.getTime() - startTime.getTime(),
|
|
345
|
-
requestResults: [],
|
|
346
|
-
totalTests: 0,
|
|
347
|
-
passedTests: 0,
|
|
348
|
-
failedTests: 0,
|
|
349
|
-
skippedTests: 0,
|
|
350
|
-
validationErrors: validationResult.errors,
|
|
351
|
-
aborted: this.isAborted(),
|
|
352
|
-
abortReason: this.getAbortReason()
|
|
353
|
-
};
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
const requestResults: RequestResult[] = [];
|
|
357
|
-
|
|
358
|
-
// Initialize cookie jar - always create one
|
|
359
|
-
// If jar.persist = true, cookies carry across requests
|
|
360
|
-
// If jar.persist = false (default), each request gets cookies from response only
|
|
361
|
-
const cookieJar = new CookieJar(runtimeOptions.jar ?? { persist: false });
|
|
362
|
-
|
|
363
|
-
// Inject initial cookies if provided in options
|
|
364
|
-
if (runtimeOptions.cookies !== null && runtimeOptions.cookies !== undefined && runtimeOptions.cookies.length > 0) {
|
|
365
|
-
for (const cookie of runtimeOptions.cookies) {
|
|
366
|
-
// Skip cookies without domain - domain is required for RFC 6265 compliance
|
|
367
|
-
if (cookie.domain === null || cookie.domain === undefined) {
|
|
368
|
-
continue;
|
|
369
|
-
}
|
|
370
|
-
cookieJar.set(cookie.name, cookie.value, {
|
|
371
|
-
domain: cookie.domain,
|
|
372
|
-
path: cookie.path,
|
|
373
|
-
expires: cookie.expires,
|
|
374
|
-
httpOnly: cookie.httpOnly,
|
|
375
|
-
secure: cookie.secure,
|
|
376
|
-
sameSite: cookie.sameSite
|
|
377
|
-
});
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
// Initialize scope stack with collection scope
|
|
382
|
-
const scopeStack: ScopeFrame[] = [{
|
|
383
|
-
level: 'collection',
|
|
384
|
-
id: collection.info.id,
|
|
385
|
-
vars: {}
|
|
386
|
-
}];
|
|
387
|
-
|
|
388
|
-
// Execute collection pre-request script once before all iterations
|
|
389
|
-
if (!isNullOrWhitespace(collection.collectionPreScript)) {
|
|
390
|
-
// Emit event before collection pre-script execution
|
|
391
|
-
const envelope = this.createEventEnvelope(collection.info, 'collection:/', undefined);
|
|
392
|
-
this.emit('beforeCollectionPreScript', {
|
|
393
|
-
...envelope,
|
|
394
|
-
path: 'collection:/' as const
|
|
395
|
-
});
|
|
396
|
-
|
|
397
|
-
const tempContext: ExecutionContext = {
|
|
398
|
-
collectionInfo: collection.info,
|
|
399
|
-
protocol: collection.protocol,
|
|
400
|
-
collectionVariables: collection.variables ?? {},
|
|
401
|
-
globalVariables: options.globalVariables ?? {},
|
|
402
|
-
scopeStack: [...scopeStack], // Clone scope stack
|
|
403
|
-
environment: options.environment,
|
|
404
|
-
iterationCurrent: 1,
|
|
405
|
-
iterationCount,
|
|
406
|
-
iterationData,
|
|
407
|
-
iterationSource,
|
|
408
|
-
executionHistory: [],
|
|
409
|
-
options: this.mergeOptions(collection.options, options),
|
|
410
|
-
cookieJar,
|
|
411
|
-
eventEmitter: this,
|
|
412
|
-
protocolPlugin,
|
|
413
|
-
abortSignal: this.abortController?.signal
|
|
414
|
-
};
|
|
415
|
-
|
|
416
|
-
const preScriptResult = await this.queueScript(() =>
|
|
417
|
-
this.scriptEngine.execute(
|
|
418
|
-
collection.collectionPreScript!,
|
|
419
|
-
tempContext,
|
|
420
|
-
ScriptType.CollectionPre,
|
|
421
|
-
() => {} // noop - collection pre-scripts cannot have tests
|
|
422
|
-
)
|
|
423
|
-
);
|
|
424
|
-
this.emitConsoleOutput(preScriptResult.consoleOutput);
|
|
425
|
-
|
|
426
|
-
// Emit event after collection pre-script completion
|
|
427
|
-
const afterEnvelope = this.createEventEnvelope(collection.info, 'collection:/', tempContext);
|
|
428
|
-
this.emit('afterCollectionPreScript', {
|
|
429
|
-
...afterEnvelope,
|
|
430
|
-
path: 'collection:/' as const,
|
|
431
|
-
result: preScriptResult
|
|
432
|
-
});
|
|
433
|
-
|
|
434
|
-
if (preScriptResult.success === false) {
|
|
435
|
-
throw new Error(`Collection pre-script error: ${preScriptResult.error}`);
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
// Update context variables for iterations
|
|
439
|
-
options.globalVariables = tempContext.globalVariables;
|
|
440
|
-
options.environment = tempContext.environment;
|
|
441
|
-
|
|
442
|
-
// Update collection scope with any changes from script
|
|
443
|
-
Object.assign(scopeStack[0].vars, tempContext.scopeStack[0].vars);
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
for (let i = 0; i < iterationCount; i++) {
|
|
447
|
-
if (this.isAborted()) {
|
|
448
|
-
this.logger.info('Run aborted - skipping remaining iterations');
|
|
449
|
-
break;
|
|
450
|
-
}
|
|
451
|
-
const iterationStart = Date.now();
|
|
452
|
-
const context: ExecutionContext = {
|
|
453
|
-
collectionInfo: collection.info,
|
|
454
|
-
protocol: collection.protocol,
|
|
455
|
-
collectionVariables: collection.variables ?? {},
|
|
456
|
-
globalVariables: options.globalVariables ?? {},
|
|
457
|
-
scopeStack: [...scopeStack], // Clone scope stack for each iteration
|
|
458
|
-
environment: options.environment,
|
|
459
|
-
iterationCurrent: i + 1,
|
|
460
|
-
iterationCount,
|
|
461
|
-
iterationData,
|
|
462
|
-
iterationSource,
|
|
463
|
-
executionHistory: [],
|
|
464
|
-
options: this.mergeOptions(collection.options, options),
|
|
465
|
-
cookieJar,
|
|
466
|
-
eventEmitter: this,
|
|
467
|
-
protocolPlugin,
|
|
468
|
-
abortSignal: this.abortController?.signal
|
|
469
|
-
};
|
|
470
|
-
|
|
471
|
-
// Emit beforeIteration event
|
|
472
|
-
const iterationEnvelope = this.createEventEnvelope(collection.info, 'collection:/', context);
|
|
473
|
-
this.emit('beforeIteration', {
|
|
474
|
-
...iterationEnvelope,
|
|
475
|
-
iteration: iterationEnvelope.iteration!
|
|
476
|
-
});
|
|
477
|
-
|
|
478
|
-
// Use DAG-based execution (ALWAYS - concurrency=1 for sequential mode)
|
|
479
|
-
await this.executeWithDAG(collectionToRun, context, requestResults);
|
|
480
|
-
|
|
481
|
-
// Emit afterIteration event
|
|
482
|
-
const iterationDuration = Date.now() - iterationStart;
|
|
483
|
-
const afterIterationEnvelope = this.createEventEnvelope(collection.info, 'collection:/', context);
|
|
484
|
-
this.emit('afterIteration', {
|
|
485
|
-
...afterIterationEnvelope,
|
|
486
|
-
iteration: afterIterationEnvelope.iteration!,
|
|
487
|
-
duration: iterationDuration
|
|
488
|
-
});
|
|
489
|
-
|
|
490
|
-
// Update collection scope with changes from iteration
|
|
491
|
-
Object.assign(scopeStack[0].vars, context.scopeStack[0].vars);
|
|
492
|
-
|
|
493
|
-
// Update global variables and environment for next iteration
|
|
494
|
-
options.globalVariables = context.globalVariables;
|
|
495
|
-
options.environment = context.environment;
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
// Execute collection post-request script once after all iterations
|
|
499
|
-
if (!isNullOrWhitespace(collection.collectionPostScript)) {
|
|
500
|
-
// Emit event before collection post-script execution
|
|
501
|
-
const beforePostEnvelope = this.createEventEnvelope(collection.info, 'collection:/', undefined);
|
|
502
|
-
this.emit('beforeCollectionPostScript', {
|
|
503
|
-
...beforePostEnvelope,
|
|
504
|
-
path: 'collection:/' as const
|
|
505
|
-
});
|
|
506
|
-
|
|
507
|
-
const tempContext: ExecutionContext = {
|
|
508
|
-
collectionInfo: collection.info,
|
|
509
|
-
protocol: collection.protocol,
|
|
510
|
-
collectionVariables: collection.variables ?? {},
|
|
511
|
-
globalVariables: options.globalVariables ?? {},
|
|
512
|
-
scopeStack: [...scopeStack], // Clone scope stack
|
|
513
|
-
environment: options.environment,
|
|
514
|
-
iterationCurrent: iterationCount,
|
|
515
|
-
iterationCount,
|
|
516
|
-
iterationData,
|
|
517
|
-
iterationSource,
|
|
518
|
-
executionHistory: [],
|
|
519
|
-
options: this.mergeOptions(collection.options, options),
|
|
520
|
-
cookieJar,
|
|
521
|
-
eventEmitter: this,
|
|
522
|
-
protocolPlugin,
|
|
523
|
-
abortSignal: this.abortController?.signal
|
|
524
|
-
};
|
|
525
|
-
|
|
526
|
-
const postScriptResult = await this.queueScript(() =>
|
|
527
|
-
this.scriptEngine.execute(
|
|
528
|
-
collection.collectionPostScript!,
|
|
529
|
-
tempContext,
|
|
530
|
-
ScriptType.CollectionPost,
|
|
531
|
-
() => {} // noop - collection post-scripts cannot have tests
|
|
532
|
-
)
|
|
533
|
-
);
|
|
534
|
-
this.emitConsoleOutput(postScriptResult.consoleOutput);
|
|
535
|
-
|
|
536
|
-
// Emit event for collection post-script completion
|
|
537
|
-
const afterPostEnvelope = this.createEventEnvelope(collection.info, 'collection:/', tempContext);
|
|
538
|
-
this.emit('afterCollectionPostScript', {
|
|
539
|
-
...afterPostEnvelope,
|
|
540
|
-
path: 'collection:/' as const,
|
|
541
|
-
result: postScriptResult
|
|
542
|
-
});
|
|
543
|
-
|
|
544
|
-
if (postScriptResult.success === false) {
|
|
545
|
-
throw new Error(`Collection post-script error: ${postScriptResult.error}`);
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
const endTime = new Date();
|
|
550
|
-
const duration = endTime.getTime() - startTime.getTime();
|
|
551
|
-
|
|
552
|
-
const totalTests = requestResults.reduce((sum, r) => sum + r.tests.length, 0);
|
|
553
|
-
const passedTests = requestResults.reduce(
|
|
554
|
-
(sum, r) => sum + r.tests.filter(t => t.passed && !t.skipped).length,
|
|
555
|
-
0
|
|
556
|
-
);
|
|
557
|
-
const failedTests = requestResults.reduce(
|
|
558
|
-
(sum, r) => sum + r.tests.filter(t => !t.passed && !t.skipped).length,
|
|
559
|
-
0
|
|
560
|
-
);
|
|
561
|
-
const skippedTests = requestResults.reduce(
|
|
562
|
-
(sum, r) => sum + r.tests.filter(t => t.skipped).length,
|
|
563
|
-
0
|
|
564
|
-
);
|
|
565
|
-
|
|
566
|
-
const result: RunResult = {
|
|
567
|
-
collectionId: collection.info.id,
|
|
568
|
-
collectionName: collection.info.name,
|
|
569
|
-
startTime,
|
|
570
|
-
endTime,
|
|
571
|
-
duration,
|
|
572
|
-
requestResults,
|
|
573
|
-
totalTests,
|
|
574
|
-
passedTests,
|
|
575
|
-
failedTests,
|
|
576
|
-
skippedTests,
|
|
577
|
-
aborted: this.isAborted(),
|
|
578
|
-
abortReason: this.getAbortReason()
|
|
579
|
-
};
|
|
580
|
-
|
|
581
|
-
this.emit('afterRun', {
|
|
582
|
-
collectionInfo: collection.info,
|
|
583
|
-
result
|
|
584
|
-
});
|
|
585
|
-
return result;
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
private resolveRequest(request: Request, context: ExecutionContext): void {
|
|
589
|
-
// Resolve variables in request data
|
|
590
|
-
request.data = this.variableResolver.resolveAll(request.data, context) as typeof request.data;
|
|
591
|
-
|
|
592
|
-
// Resolve variables in auth data
|
|
593
|
-
if (request.auth?.data !== undefined) {
|
|
594
|
-
request.auth.data = this.variableResolver.resolveAll(request.auth.data, context) as Record<string, unknown>;
|
|
595
|
-
}
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
private mergeOptions(
|
|
599
|
-
collectionOptions?: RuntimeOptions,
|
|
600
|
-
runOptions?: RunOptions
|
|
601
|
-
): RuntimeOptions {
|
|
602
|
-
// Since RunOptions extends RuntimeOptions, merge is straightforward
|
|
603
|
-
// RunOptions takes precedence over collectionOptions
|
|
604
|
-
const merged: RuntimeOptions = {
|
|
605
|
-
...collectionOptions,
|
|
606
|
-
...runOptions,
|
|
607
|
-
// Deep merge nested objects
|
|
608
|
-
execution: {
|
|
609
|
-
...(collectionOptions?.execution ?? {}),
|
|
610
|
-
...(runOptions?.execution ?? {})
|
|
611
|
-
},
|
|
612
|
-
// Ensure defaults
|
|
613
|
-
strictMode: runOptions?.strictMode ?? collectionOptions?.strictMode ?? true,
|
|
614
|
-
// Include filter options (type narrow from undefined)
|
|
615
|
-
filter: (runOptions?.filter ?? collectionOptions?.filter) !== undefined
|
|
616
|
-
? String(runOptions?.filter ?? collectionOptions?.filter)
|
|
617
|
-
: undefined,
|
|
618
|
-
excludeDeps: ((runOptions?.excludeDeps ?? collectionOptions?.excludeDeps) !== undefined)
|
|
619
|
-
? Boolean(runOptions?.excludeDeps ?? collectionOptions?.excludeDeps)
|
|
620
|
-
: undefined
|
|
621
|
-
};
|
|
622
|
-
|
|
623
|
-
// Conditionally merge optional nested objects
|
|
624
|
-
if (collectionOptions?.timeout !== undefined || runOptions?.timeout !== undefined) {
|
|
625
|
-
merged.timeout = {
|
|
626
|
-
...(collectionOptions?.timeout ?? {}),
|
|
627
|
-
...(runOptions?.timeout ?? {})
|
|
628
|
-
};
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
if (collectionOptions?.ssl !== undefined || runOptions?.ssl !== undefined) {
|
|
632
|
-
merged.ssl = {
|
|
633
|
-
...(collectionOptions?.ssl ?? {}),
|
|
634
|
-
...(runOptions?.ssl ?? {})
|
|
635
|
-
};
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
if (collectionOptions?.jar !== undefined || runOptions?.jar !== undefined) {
|
|
639
|
-
merged.jar = {
|
|
640
|
-
persist: runOptions?.jar?.persist ?? collectionOptions?.jar?.persist ?? false
|
|
641
|
-
};
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
if (runOptions?.proxy !== null && runOptions?.proxy !== undefined) {
|
|
645
|
-
merged.proxy = runOptions.proxy;
|
|
646
|
-
} else if (collectionOptions?.proxy !== null && collectionOptions?.proxy !== undefined) {
|
|
647
|
-
merged.proxy = collectionOptions.proxy;
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
// Merge cookies arrays (runOptions cookies + collectionOptions cookies)
|
|
651
|
-
const collectionCookies = collectionOptions?.cookies ?? [];
|
|
652
|
-
const runCookies = runOptions?.cookies ?? [];
|
|
653
|
-
if (collectionCookies.length > 0 || runCookies.length > 0) {
|
|
654
|
-
// RunOptions cookies override collection cookies with same name
|
|
655
|
-
const cookieMap = new Map<string, Cookie>();
|
|
656
|
-
|
|
657
|
-
// Add collection cookies first
|
|
658
|
-
for (const cookie of collectionCookies) {
|
|
659
|
-
cookieMap.set(cookie.name, cookie);
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
// Then add/override with run cookies
|
|
663
|
-
for (const cookie of runCookies) {
|
|
664
|
-
cookieMap.set(cookie.name, cookie);
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
merged.cookies = Array.from(cookieMap.values());
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
return merged;
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
/**
|
|
674
|
-
* Evaluate condition expression at runtime
|
|
675
|
-
* Returns true if condition passes, false if it fails
|
|
676
|
-
*/
|
|
677
|
-
private async evaluateCondition(condition: string, context: ExecutionContext): Promise<boolean> {
|
|
678
|
-
try {
|
|
679
|
-
// Use workaround: store result in global variable
|
|
680
|
-
const wrappedScript = `
|
|
681
|
-
const __conditionResult = (${condition});
|
|
682
|
-
quest.global.variables.set('__conditionResult', String(__conditionResult === true));
|
|
683
|
-
`;
|
|
684
|
-
|
|
685
|
-
const result = await this.scriptEngine.execute(
|
|
686
|
-
wrappedScript,
|
|
687
|
-
context,
|
|
688
|
-
ScriptType.PreRequest,
|
|
689
|
-
() => {}
|
|
690
|
-
);
|
|
691
|
-
|
|
692
|
-
if (result.success === false) {
|
|
693
|
-
this.logger.warn(`Condition evaluation error: ${result.error}`);
|
|
694
|
-
return false;
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
// Read result from global variables
|
|
698
|
-
const conditionResult = context.globalVariables.__conditionResult === 'true';
|
|
699
|
-
|
|
700
|
-
// Clean up temp variable
|
|
701
|
-
delete context.globalVariables.__conditionResult;
|
|
702
|
-
|
|
703
|
-
return conditionResult;
|
|
704
|
-
} catch (error) {
|
|
705
|
-
this.logger.error(`Failed to evaluate condition: ${condition}`, error);
|
|
706
|
-
return false;
|
|
707
|
-
}
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
// ========================================================================
|
|
711
|
-
// DAG Execution Methods
|
|
712
|
-
// ========================================================================
|
|
713
|
-
|
|
714
|
-
/**
|
|
715
|
-
* Execute collection using DAG-based execution
|
|
716
|
-
*/
|
|
717
|
-
private async executeWithDAG(
|
|
718
|
-
collection: Collection,
|
|
719
|
-
context: ExecutionContext,
|
|
720
|
-
results: RequestResult[]
|
|
721
|
-
): Promise<void> {
|
|
722
|
-
// Determine execution mode
|
|
723
|
-
const allowParallel = context.options.execution?.allowParallel === true;
|
|
724
|
-
|
|
725
|
-
// Build TaskGraph from collection (DAG structure depends on execution mode)
|
|
726
|
-
this.logger.debug(`Building TaskGraph from collection (parallel=${allowParallel})`);
|
|
727
|
-
const taskGraph = new TaskGraph(this.logger);
|
|
728
|
-
taskGraph.build(collection, allowParallel);
|
|
729
|
-
this.logger.debug(`TaskGraph built: ${taskGraph.getNodes().size} nodes, ${taskGraph.getEdges().length} edges`);
|
|
730
|
-
|
|
731
|
-
// Determine concurrency (0 defaults to 1)
|
|
732
|
-
let maxConcurrency = allowParallel
|
|
733
|
-
? (context.options.execution?.maxConcurrency ?? 5)
|
|
734
|
-
: 1; // Sequential mode uses concurrency=1
|
|
735
|
-
|
|
736
|
-
// Treat 0 as 1 (invalid value)
|
|
737
|
-
if (maxConcurrency === 0) {
|
|
738
|
-
this.logger.warn('maxConcurrency=0 is invalid, defaulting to 1');
|
|
739
|
-
maxConcurrency = 1;
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
this.logger.info(`DAG execution mode: ${maxConcurrency === 1 ? 'SEQUENTIAL' : `PARALLEL (concurrency=${maxConcurrency})`}`);
|
|
743
|
-
|
|
744
|
-
// Create scheduler with callbacks
|
|
745
|
-
const callbacks: DagExecutionCallbacks = {
|
|
746
|
-
executeScript: this.executeScriptForDAG.bind(this),
|
|
747
|
-
executeFolderEnter: this.executeFolderEnter.bind(this),
|
|
748
|
-
executeFolderExit: this.executeFolderExit.bind(this),
|
|
749
|
-
executeRequestIO: this.executeRequestIOForDAG.bind(this),
|
|
750
|
-
evaluateCondition: this.evaluateCondition.bind(this),
|
|
751
|
-
isAborted: this.isAborted.bind(this)
|
|
752
|
-
};
|
|
753
|
-
|
|
754
|
-
const scheduler = new DagScheduler(callbacks, maxConcurrency, this.logger);
|
|
755
|
-
|
|
756
|
-
// Execute DAG
|
|
757
|
-
this.logger.debug('Starting DAG execution');
|
|
758
|
-
const dagResults = await scheduler.execute(taskGraph, context);
|
|
759
|
-
this.logger.debug(`DAG execution complete: ${dagResults.length} requests executed`);
|
|
760
|
-
|
|
761
|
-
results.push(...dagResults);
|
|
762
|
-
|
|
763
|
-
// Check for script errors and stop execution if found
|
|
764
|
-
for (const result of dagResults) {
|
|
765
|
-
if (result.scriptError !== undefined && result.scriptError !== 'Skipped by condition' && result.scriptError !== 'Skipped by bail') {
|
|
766
|
-
throw new Error(result.scriptError);
|
|
767
|
-
}
|
|
768
|
-
}
|
|
769
|
-
}
|
|
770
|
-
|
|
771
|
-
/**
|
|
772
|
-
* Execute a script node (called by DagScheduler)
|
|
773
|
-
*/
|
|
774
|
-
private async executeScriptForDAG(
|
|
775
|
-
script: string,
|
|
776
|
-
scriptType: ScriptType,
|
|
777
|
-
context: ExecutionContext,
|
|
778
|
-
node: TaskNode,
|
|
779
|
-
flags: { skip: boolean; bail: boolean }
|
|
780
|
-
): Promise<ScriptResult> {
|
|
781
|
-
if (flags.bail) {
|
|
782
|
-
return { success: true, tests: [], consoleOutput: [] };
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
// Skip script execution but allow skipped assertions in post-request scripts
|
|
786
|
-
if (flags.skip) {
|
|
787
|
-
if (scriptType === ScriptType.FolderPre || scriptType === ScriptType.FolderPost) {
|
|
788
|
-
return { success: true, tests: [], consoleOutput: [] };
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
// Collection scripts should not run during skip propagation
|
|
792
|
-
if (scriptType === ScriptType.CollectionPre || scriptType === ScriptType.CollectionPost) {
|
|
793
|
-
return { success: true, tests: [], consoleOutput: [] };
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
// Plugin event scripts are skipped without assertions
|
|
797
|
-
if (scriptType === ScriptType.PluginEvent) {
|
|
798
|
-
return { success: true, tests: [], consoleOutput: [] };
|
|
799
|
-
}
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
// Handle different script types
|
|
803
|
-
switch (scriptType) {
|
|
804
|
-
case ScriptType.CollectionPre:
|
|
805
|
-
return await this.executeCollectionPreScript(script, context);
|
|
806
|
-
|
|
807
|
-
case ScriptType.CollectionPost:
|
|
808
|
-
return await this.executeCollectionPostScript(script, context);
|
|
809
|
-
|
|
810
|
-
case ScriptType.FolderPre:
|
|
811
|
-
return await this.executeFolderPreScript(script, context, node, flags);
|
|
812
|
-
|
|
813
|
-
case ScriptType.FolderPost:
|
|
814
|
-
return await this.executeFolderPostScript(script, context, node, flags);
|
|
815
|
-
|
|
816
|
-
case ScriptType.PluginEvent:
|
|
817
|
-
return await this.executePluginEventScript(script, context, node);
|
|
818
|
-
|
|
819
|
-
default:
|
|
820
|
-
this.logger.error(`Unknown script type: ${scriptType as string}`);
|
|
821
|
-
return {
|
|
822
|
-
success: false,
|
|
823
|
-
tests: [],
|
|
824
|
-
consoleOutput: [],
|
|
825
|
-
error: `Unknown script type: ${scriptType as string}`
|
|
826
|
-
};
|
|
827
|
-
}
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
private async executeCollectionPreScript(
|
|
831
|
-
script: string,
|
|
832
|
-
context: ExecutionContext
|
|
833
|
-
): Promise<ScriptResult> {
|
|
834
|
-
const envelope = this.createEventEnvelope(context.collectionInfo, 'collection:/', undefined);
|
|
835
|
-
this.emit('beforeCollectionPreScript', {
|
|
836
|
-
...envelope,
|
|
837
|
-
script
|
|
838
|
-
});
|
|
839
|
-
|
|
840
|
-
const result = await this.scriptEngine.execute(
|
|
841
|
-
script,
|
|
842
|
-
context,
|
|
843
|
-
ScriptType.CollectionPre,
|
|
844
|
-
() => {} // Collection pre-scripts cannot have tests
|
|
845
|
-
);
|
|
846
|
-
this.emitConsoleOutput(result.consoleOutput);
|
|
847
|
-
|
|
848
|
-
const afterEnvelope = this.createEventEnvelope(context.collectionInfo, 'collection:/', context);
|
|
849
|
-
this.emit('afterCollectionPreScript', {
|
|
850
|
-
...afterEnvelope,
|
|
851
|
-
result
|
|
852
|
-
});
|
|
853
|
-
|
|
854
|
-
return result;
|
|
855
|
-
}
|
|
856
|
-
|
|
857
|
-
private async executeCollectionPostScript(
|
|
858
|
-
script: string,
|
|
859
|
-
context: ExecutionContext
|
|
860
|
-
): Promise<ScriptResult> {
|
|
861
|
-
const beforeEnvelope = this.createEventEnvelope(context.collectionInfo, 'collection:/', undefined);
|
|
862
|
-
this.emit('beforeCollectionPostScript', {
|
|
863
|
-
...beforeEnvelope,
|
|
864
|
-
script
|
|
865
|
-
});
|
|
866
|
-
|
|
867
|
-
const result = await this.scriptEngine.execute(
|
|
868
|
-
script,
|
|
869
|
-
context,
|
|
870
|
-
ScriptType.CollectionPost,
|
|
871
|
-
() => {} // Collection post-scripts cannot have tests
|
|
872
|
-
);
|
|
873
|
-
this.emitConsoleOutput(result.consoleOutput);
|
|
874
|
-
|
|
875
|
-
const afterEnvelope = this.createEventEnvelope(context.collectionInfo, 'collection:/', context);
|
|
876
|
-
this.emit('afterCollectionPostScript', {
|
|
877
|
-
...afterEnvelope,
|
|
878
|
-
result
|
|
879
|
-
});
|
|
880
|
-
|
|
881
|
-
return result;
|
|
882
|
-
}
|
|
883
|
-
|
|
884
|
-
/**
|
|
885
|
-
* Execute folder enter lifecycle: PUSH scope + emit beforeFolder
|
|
886
|
-
* ALWAYS executes regardless of script existence
|
|
887
|
-
*/
|
|
888
|
-
private async executeFolderEnter(
|
|
889
|
-
node: TaskNode,
|
|
890
|
-
context: ExecutionContext,
|
|
891
|
-
flags: { skip: boolean; bail: boolean }
|
|
892
|
-
): Promise<void> {
|
|
893
|
-
if (flags.skip || flags.bail) {
|
|
894
|
-
return;
|
|
895
|
-
}
|
|
896
|
-
const folder = node.item as Folder;
|
|
897
|
-
|
|
898
|
-
// PUSH folder scope
|
|
899
|
-
context.scopeStack.push({
|
|
900
|
-
level: 'folder',
|
|
901
|
-
id: folder.id,
|
|
902
|
-
vars: {}
|
|
903
|
-
});
|
|
904
|
-
|
|
905
|
-
// Emit beforeFolder event
|
|
906
|
-
const beforeFolderEnvelope = this.createEventEnvelope(context.collectionInfo, node.path, context);
|
|
907
|
-
this.emit('beforeFolder', beforeFolderEnvelope);
|
|
908
|
-
}
|
|
909
|
-
|
|
910
|
-
/**
|
|
911
|
-
* Execute folder exit lifecycle: POP scope + emit afterFolder
|
|
912
|
-
* ALWAYS executes regardless of script existence
|
|
913
|
-
*
|
|
914
|
-
* Note: If folder-enter was skipped due to condition, the scope was never pushed.
|
|
915
|
-
* We only POP if the top of the stack matches this folder's ID.
|
|
916
|
-
*/
|
|
917
|
-
private async executeFolderExit(
|
|
918
|
-
node: TaskNode,
|
|
919
|
-
context: ExecutionContext,
|
|
920
|
-
flags: { skip: boolean; bail: boolean }
|
|
921
|
-
): Promise<void> {
|
|
922
|
-
if (flags.skip || flags.bail) {
|
|
923
|
-
return;
|
|
924
|
-
}
|
|
925
|
-
const folder = node.item as Folder;
|
|
926
|
-
|
|
927
|
-
// Check if folder scope exists on stack before popping
|
|
928
|
-
// (folder-enter may have been skipped due to condition)
|
|
929
|
-
const topScope = context.scopeStack[context.scopeStack.length - 1];
|
|
930
|
-
if (topScope?.level === 'folder' && topScope.id === folder.id) {
|
|
931
|
-
// POP folder scope
|
|
932
|
-
context.scopeStack.pop();
|
|
933
|
-
|
|
934
|
-
// Emit afterFolder event
|
|
935
|
-
const afterFolderEnvelope = this.createEventEnvelope(context.collectionInfo, node.path, context);
|
|
936
|
-
this.emit('afterFolder', {
|
|
937
|
-
...afterFolderEnvelope,
|
|
938
|
-
duration: 0
|
|
939
|
-
});
|
|
940
|
-
}
|
|
941
|
-
// If scope doesn't match, folder-enter was skipped - no POP or event needed
|
|
942
|
-
}
|
|
943
|
-
|
|
944
|
-
private async executeFolderPreScript(
|
|
945
|
-
script: string,
|
|
946
|
-
context: ExecutionContext,
|
|
947
|
-
node: TaskNode,
|
|
948
|
-
flags: { skip: boolean; bail: boolean }
|
|
949
|
-
): Promise<ScriptResult> {
|
|
950
|
-
if (flags.skip || flags.bail) {
|
|
951
|
-
return { success: true, tests: [], consoleOutput: [] };
|
|
952
|
-
}
|
|
953
|
-
// Emit beforeFolderPreScript event
|
|
954
|
-
const beforePreEnvelope = this.createEventEnvelope(context.collectionInfo, node.path, context);
|
|
955
|
-
this.emit('beforeFolderPreScript', beforePreEnvelope);
|
|
956
|
-
|
|
957
|
-
const result = await this.scriptEngine.execute(
|
|
958
|
-
script,
|
|
959
|
-
context,
|
|
960
|
-
ScriptType.FolderPre,
|
|
961
|
-
() => {} // Folder pre-scripts cannot have tests
|
|
962
|
-
);
|
|
963
|
-
this.emitConsoleOutput(result.consoleOutput);
|
|
964
|
-
|
|
965
|
-
// Emit afterFolderPreScript event
|
|
966
|
-
const afterPreEnvelope = this.createEventEnvelope(context.collectionInfo, node.path, context);
|
|
967
|
-
this.emit('afterFolderPreScript', {
|
|
968
|
-
...afterPreEnvelope,
|
|
969
|
-
result
|
|
970
|
-
});
|
|
971
|
-
|
|
972
|
-
return result;
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
private async executeFolderPostScript(
|
|
976
|
-
script: string,
|
|
977
|
-
context: ExecutionContext,
|
|
978
|
-
node: TaskNode,
|
|
979
|
-
flags: { skip: boolean; bail: boolean }
|
|
980
|
-
): Promise<ScriptResult> {
|
|
981
|
-
if (flags.skip || flags.bail) {
|
|
982
|
-
return { success: true, tests: [], consoleOutput: [] };
|
|
983
|
-
}
|
|
984
|
-
// Emit beforeFolderPostScript event
|
|
985
|
-
const beforePostEnvelope = this.createEventEnvelope(context.collectionInfo, node.path, context);
|
|
986
|
-
this.emit('beforeFolderPostScript', beforePostEnvelope);
|
|
987
|
-
|
|
988
|
-
const result = await this.scriptEngine.execute(
|
|
989
|
-
script,
|
|
990
|
-
context,
|
|
991
|
-
ScriptType.FolderPost,
|
|
992
|
-
() => {} // Folder post-scripts cannot have tests
|
|
993
|
-
);
|
|
994
|
-
this.emitConsoleOutput(result.consoleOutput);
|
|
995
|
-
|
|
996
|
-
// Emit afterFolderPostScript event
|
|
997
|
-
const afterPostEnvelope = this.createEventEnvelope(context.collectionInfo, node.path, context);
|
|
998
|
-
this.emit('afterFolderPostScript', {
|
|
999
|
-
...afterPostEnvelope,
|
|
1000
|
-
result
|
|
1001
|
-
});
|
|
1002
|
-
|
|
1003
|
-
return result;
|
|
1004
|
-
}
|
|
1005
|
-
|
|
1006
|
-
private async executePluginEventScript(
|
|
1007
|
-
script: string,
|
|
1008
|
-
context: ExecutionContext,
|
|
1009
|
-
node: TaskNode
|
|
1010
|
-
): Promise<ScriptResult> {
|
|
1011
|
-
// Plugin event scripts should already have context.currentEvent set by the plugin
|
|
1012
|
-
// We just need to execute the script through the queue (THIS FIXES THE BUG!)
|
|
1013
|
-
|
|
1014
|
-
const request = context.currentRequest!;
|
|
1015
|
-
const eventName = node.eventName!;
|
|
1016
|
-
|
|
1017
|
-
const result = await this.scriptEngine.execute(
|
|
1018
|
-
script,
|
|
1019
|
-
context,
|
|
1020
|
-
ScriptType.PluginEvent,
|
|
1021
|
-
(test: TestResult) => {
|
|
1022
|
-
// Emit assertion event for plugin event test
|
|
1023
|
-
const eventDef = context.protocolPlugin.events?.find(e => e.name === eventName);
|
|
1024
|
-
if (eventDef?.canHaveTests === true) {
|
|
1025
|
-
const envelope = this.createEventEnvelope(context.collectionInfo, node.path, context, request);
|
|
1026
|
-
this.emit('assertion', {
|
|
1027
|
-
...envelope,
|
|
1028
|
-
test,
|
|
1029
|
-
event: eventName
|
|
1030
|
-
});
|
|
1031
|
-
}
|
|
1032
|
-
|
|
1033
|
-
// Trigger bail on failed test if enabled
|
|
1034
|
-
if (this.bailEnabled && this.ownsController && !test.passed && !test.skipped) {
|
|
1035
|
-
this.abort('Test failure (--bail)');
|
|
1036
|
-
}
|
|
1037
|
-
}
|
|
1038
|
-
);
|
|
1039
|
-
|
|
1040
|
-
return result;
|
|
1041
|
-
}
|
|
1042
|
-
|
|
1043
|
-
/**
|
|
1044
|
-
* Execute request I/O (called by DagScheduler)
|
|
1045
|
-
* This handles the full request lifecycle: pre-scripts → I/O → post-scripts
|
|
1046
|
-
*/
|
|
1047
|
-
private async executeRequestIOForDAG(
|
|
1048
|
-
node: TaskNode,
|
|
1049
|
-
context: ExecutionContext,
|
|
1050
|
-
flags: { skip: boolean; bail: boolean }
|
|
1051
|
-
): Promise<RequestResult> {
|
|
1052
|
-
const request = node.item as Request;
|
|
1053
|
-
|
|
1054
|
-
// NOTE: Do NOT set context.currentRequest here in parallel mode!
|
|
1055
|
-
// It will be overwritten by other parallel requests before scripts execute.
|
|
1056
|
-
// Instead, set it inside each queued script function.
|
|
1057
|
-
context.currentPath = node.path;
|
|
1058
|
-
|
|
1059
|
-
// Apply execution.delay between requests (not before first, not if parallel, not if skipped)
|
|
1060
|
-
if (!flags.skip && !flags.bail) {
|
|
1061
|
-
const delay = context.options?.execution?.delay ?? 0;
|
|
1062
|
-
const isParallel = context.options?.execution?.allowParallel ?? false;
|
|
1063
|
-
|
|
1064
|
-
if (this.shouldDelayNextRequest && delay > 0 && !isParallel) {
|
|
1065
|
-
this.logger.debug(`Delaying ${delay}ms before request`);
|
|
1066
|
-
await new Promise(resolve => setTimeout(resolve, delay));
|
|
1067
|
-
}
|
|
1068
|
-
|
|
1069
|
-
// Mark that subsequent requests should be delayed
|
|
1070
|
-
this.shouldDelayNextRequest = true;
|
|
1071
|
-
}
|
|
1072
|
-
|
|
1073
|
-
if (flags.bail) {
|
|
1074
|
-
const skippedResult: RequestResult = {
|
|
1075
|
-
requestId: request.id,
|
|
1076
|
-
requestName: request.name,
|
|
1077
|
-
path: node.path,
|
|
1078
|
-
success: true,
|
|
1079
|
-
tests: [],
|
|
1080
|
-
duration: 0,
|
|
1081
|
-
iteration: context.iterationCurrent,
|
|
1082
|
-
scriptError: 'Skipped by bail'
|
|
1083
|
-
};
|
|
1084
|
-
return skippedResult;
|
|
1085
|
-
}
|
|
1086
|
-
|
|
1087
|
-
if (flags.skip) {
|
|
1088
|
-
const skippedResult: RequestResult = {
|
|
1089
|
-
requestId: request.id,
|
|
1090
|
-
requestName: request.name,
|
|
1091
|
-
path: node.path,
|
|
1092
|
-
success: true,
|
|
1093
|
-
tests: [],
|
|
1094
|
-
duration: 0,
|
|
1095
|
-
iteration: context.iterationCurrent,
|
|
1096
|
-
scriptError: 'Skipped by condition'
|
|
1097
|
-
};
|
|
1098
|
-
return skippedResult;
|
|
1099
|
-
}
|
|
1100
|
-
|
|
1101
|
-
// Emit beforeItem event
|
|
1102
|
-
const beforeItemEnvelope = this.createEventEnvelope(context.collectionInfo, node.path, context, request);
|
|
1103
|
-
this.emit('beforeItem', {
|
|
1104
|
-
...beforeItemEnvelope,
|
|
1105
|
-
request,
|
|
1106
|
-
path: node.path
|
|
1107
|
-
});
|
|
1108
|
-
|
|
1109
|
-
// PUSH request scope
|
|
1110
|
-
context.scopeStack.push({
|
|
1111
|
-
level: 'request',
|
|
1112
|
-
id: request.id,
|
|
1113
|
-
vars: {}
|
|
1114
|
-
});
|
|
1115
|
-
|
|
1116
|
-
try {
|
|
1117
|
-
// Execute all pre-request scripts (inherited + request-level) through queue
|
|
1118
|
-
if (node.inheritedPreScripts !== undefined && node.inheritedPreScripts.length > 0) {
|
|
1119
|
-
await this.queueScript(async () => {
|
|
1120
|
-
// Set currentRequest inside the queued function to avoid race conditions in parallel execution
|
|
1121
|
-
context.currentRequest = request;
|
|
1122
|
-
this.logger.debug(`Executing pre-script for request: id=${request.id}, name=${request.name}`);
|
|
1123
|
-
for (const script of node.inheritedPreScripts!) {
|
|
1124
|
-
// Emit beforePreScript event
|
|
1125
|
-
const beforePreEnvelope = this.createEventEnvelope(context.collectionInfo, node.path, context, request);
|
|
1126
|
-
this.emit('beforePreScript', {
|
|
1127
|
-
...beforePreEnvelope,
|
|
1128
|
-
request,
|
|
1129
|
-
path: node.path
|
|
1130
|
-
});
|
|
1131
|
-
|
|
1132
|
-
const preScriptResult = await this.scriptEngine.execute(
|
|
1133
|
-
script,
|
|
1134
|
-
context,
|
|
1135
|
-
ScriptType.PreRequest,
|
|
1136
|
-
() => {} // Pre-request scripts cannot have tests
|
|
1137
|
-
);
|
|
1138
|
-
this.emitConsoleOutput(preScriptResult.consoleOutput);
|
|
1139
|
-
|
|
1140
|
-
// Emit afterPreScript event
|
|
1141
|
-
const afterPreEnvelope = this.createEventEnvelope(context.collectionInfo, node.path, context, request);
|
|
1142
|
-
this.emit('afterPreScript', {
|
|
1143
|
-
...afterPreEnvelope,
|
|
1144
|
-
request,
|
|
1145
|
-
path: node.path,
|
|
1146
|
-
result: preScriptResult
|
|
1147
|
-
});
|
|
1148
|
-
|
|
1149
|
-
if (preScriptResult.success === false) {
|
|
1150
|
-
const error = new Error(
|
|
1151
|
-
`Pre-request script error: ${preScriptResult.error}`
|
|
1152
|
-
) as ErrorWithPhase;
|
|
1153
|
-
error.phase = 'prerequest';
|
|
1154
|
-
throw error;
|
|
1155
|
-
}
|
|
1156
|
-
}
|
|
1157
|
-
});
|
|
1158
|
-
}
|
|
1159
|
-
|
|
1160
|
-
// HTTP execution NOT queued - runs in parallel
|
|
1161
|
-
// Set currentRequest before I/O phase
|
|
1162
|
-
context.currentRequest = request;
|
|
1163
|
-
|
|
1164
|
-
// Apply effective auth from node (collection/folder auth inheritance)
|
|
1165
|
-
// Request auth > Folder auth > Collection auth
|
|
1166
|
-
if (node.effectiveAuth !== undefined) {
|
|
1167
|
-
request.auth = node.effectiveAuth;
|
|
1168
|
-
}
|
|
1169
|
-
|
|
1170
|
-
this.resolveRequest(request, context);
|
|
1171
|
-
|
|
1172
|
-
// Track plugin event tests and indices
|
|
1173
|
-
const pluginEventTests: TestResult[] = [];
|
|
1174
|
-
const eventIndices = new Map<string, number>();
|
|
1175
|
-
|
|
1176
|
-
// Create emitEvent callback for plugin event execution during I/O
|
|
1177
|
-
// Plugin events fire DURING request execution (e.g., WebSocket onMessage)
|
|
1178
|
-
// They must be queued/serialized and complete before request finishes
|
|
1179
|
-
const emitEvent = async (eventName: string, eventData: unknown): Promise<void> => {
|
|
1180
|
-
this.logger.trace(`Plugin event emitted: ${eventName}`, { hasScripts: request.data.scripts !== undefined });
|
|
1181
|
-
|
|
1182
|
-
// Find matching script (validation ensures at most one per event type)
|
|
1183
|
-
const eventScript = request.data.scripts?.find(s => s.event === eventName);
|
|
1184
|
-
this.logger.trace(`Event script found: ${eventScript !== undefined}`);
|
|
1185
|
-
if (eventScript === undefined) return;
|
|
1186
|
-
|
|
1187
|
-
// Get current event index (starts at 0, increments per event type)
|
|
1188
|
-
const currentIndex = eventIndices.get(eventName) ?? 0;
|
|
1189
|
-
|
|
1190
|
-
// Set event context (wrapped in try/finally to prevent state leak)
|
|
1191
|
-
try {
|
|
1192
|
-
context.currentEvent = {
|
|
1193
|
-
eventName: eventName,
|
|
1194
|
-
requestId: request.id,
|
|
1195
|
-
timestamp: new Date(),
|
|
1196
|
-
data: eventData,
|
|
1197
|
-
index: currentIndex
|
|
1198
|
-
};
|
|
1199
|
-
|
|
1200
|
-
// Execute plugin event script through the queue (serialized)
|
|
1201
|
-
const result = await this.queueScript(async () => {
|
|
1202
|
-
return await this.scriptEngine.execute(
|
|
1203
|
-
eventScript.script,
|
|
1204
|
-
context,
|
|
1205
|
-
ScriptType.PluginEvent,
|
|
1206
|
-
(test: TestResult) => {
|
|
1207
|
-
// Emit assertion event for plugin event test
|
|
1208
|
-
const eventDef = context.protocolPlugin.events?.find(e => e.name === eventName);
|
|
1209
|
-
if (eventDef?.canHaveTests === true) {
|
|
1210
|
-
const envelope = this.createEventEnvelope(context.collectionInfo, node.path, context, request);
|
|
1211
|
-
this.emit('assertion', {
|
|
1212
|
-
...envelope,
|
|
1213
|
-
test,
|
|
1214
|
-
event: eventName
|
|
1215
|
-
});
|
|
1216
|
-
}
|
|
1217
|
-
|
|
1218
|
-
// Trigger bail on failed test if enabled
|
|
1219
|
-
if (this.bailEnabled && this.ownsController && !test.passed && !test.skipped) {
|
|
1220
|
-
this.abort('Test failure (--bail)');
|
|
1221
|
-
}
|
|
1222
|
-
}
|
|
1223
|
-
);
|
|
1224
|
-
});
|
|
1225
|
-
|
|
1226
|
-
this.emitConsoleOutput(result.consoleOutput);
|
|
1227
|
-
|
|
1228
|
-
// Collect test results - add directly to pluginEventTests array
|
|
1229
|
-
if (result.tests !== undefined && result.tests.length > 0) {
|
|
1230
|
-
this.logger.trace(`Plugin event tests collected: ${result.tests.length}`);
|
|
1231
|
-
pluginEventTests.push(...result.tests);
|
|
1232
|
-
} else {
|
|
1233
|
-
this.logger.trace('No tests in plugin event result');
|
|
1234
|
-
}
|
|
1235
|
-
|
|
1236
|
-
// Handle script errors (log but don't throw - allow other events to continue)
|
|
1237
|
-
if (!isNullOrEmpty(result.error)) {
|
|
1238
|
-
this.logger.error(`Plugin event script error (${eventName}):`, result.error);
|
|
1239
|
-
}
|
|
1240
|
-
} finally {
|
|
1241
|
-
// Always reset event context to prevent state leak
|
|
1242
|
-
context.currentEvent = undefined;
|
|
1243
|
-
|
|
1244
|
-
// Increment event index for next event of same type
|
|
1245
|
-
eventIndices.set(eventName, currentIndex + 1);
|
|
1246
|
-
}
|
|
1247
|
-
};
|
|
1248
|
-
|
|
1249
|
-
// Emit beforeRequest event
|
|
1250
|
-
const beforeRequestEnvelope = this.createEventEnvelope(context.collectionInfo, node.path, context, request);
|
|
1251
|
-
this.emit('beforeRequest', {
|
|
1252
|
-
...beforeRequestEnvelope,
|
|
1253
|
-
request,
|
|
1254
|
-
path: node.path
|
|
1255
|
-
});
|
|
1256
|
-
|
|
1257
|
-
const response = await this.pluginManager.execute(
|
|
1258
|
-
context.protocol,
|
|
1259
|
-
request, // Use request directly instead of context.currentRequest
|
|
1260
|
-
context,
|
|
1261
|
-
context.options,
|
|
1262
|
-
emitEvent
|
|
1263
|
-
);
|
|
1264
|
-
context.currentResponse = response;
|
|
1265
|
-
|
|
1266
|
-
// Emit afterRequest event with duration from plugin (excludes delay)
|
|
1267
|
-
const afterRequestEnvelope = this.createEventEnvelope(context.collectionInfo, node.path, context, request);
|
|
1268
|
-
this.emit('afterRequest', {
|
|
1269
|
-
...afterRequestEnvelope,
|
|
1270
|
-
request,
|
|
1271
|
-
response,
|
|
1272
|
-
duration: response.duration
|
|
1273
|
-
});
|
|
1274
|
-
|
|
1275
|
-
// Add preliminary execution record to history
|
|
1276
|
-
const executionRecord: ExecutionRecord = {
|
|
1277
|
-
id: request.id,
|
|
1278
|
-
name: request.name,
|
|
1279
|
-
path: node.path,
|
|
1280
|
-
iteration: context.iterationCurrent,
|
|
1281
|
-
response,
|
|
1282
|
-
tests: [...pluginEventTests],
|
|
1283
|
-
timestamp: new Date().toISOString()
|
|
1284
|
-
};
|
|
1285
|
-
|
|
1286
|
-
context.executionHistory.push(executionRecord);
|
|
1287
|
-
|
|
1288
|
-
// Execute all post-request scripts (request-level + inherited) through queue
|
|
1289
|
-
let scriptResult: ScriptResult = { success: true, tests: [], consoleOutput: [] };
|
|
1290
|
-
|
|
1291
|
-
this.logger.debug(`Post-scripts for ${request.id}: ${node.inheritedPostScripts?.length ?? 0}`);
|
|
1292
|
-
|
|
1293
|
-
if (node.inheritedPostScripts !== undefined && node.inheritedPostScripts.length > 0) {
|
|
1294
|
-
scriptResult = await this.queueScript(async () => {
|
|
1295
|
-
// Set currentRequest inside the queued function to avoid race conditions in parallel execution
|
|
1296
|
-
context.currentRequest = request;
|
|
1297
|
-
const combinedResult: ScriptResult = { success: true, tests: [], consoleOutput: [] };
|
|
1298
|
-
|
|
1299
|
-
for (const script of node.inheritedPostScripts!) {
|
|
1300
|
-
// Emit beforePostScript event
|
|
1301
|
-
const beforePostEnvelope = this.createEventEnvelope(context.collectionInfo, node.path, context, request);
|
|
1302
|
-
this.emit('beforePostScript', {
|
|
1303
|
-
...beforePostEnvelope,
|
|
1304
|
-
request,
|
|
1305
|
-
path: node.path,
|
|
1306
|
-
response
|
|
1307
|
-
});
|
|
1308
|
-
|
|
1309
|
-
const postScriptResult = await this.scriptEngine.execute(
|
|
1310
|
-
script,
|
|
1311
|
-
context,
|
|
1312
|
-
ScriptType.PostRequest,
|
|
1313
|
-
(test: TestResult) => {
|
|
1314
|
-
// Emit assertion event
|
|
1315
|
-
const envelope = this.createEventEnvelope(context.collectionInfo, node.path, context, request);
|
|
1316
|
-
this.emit('assertion', {
|
|
1317
|
-
...envelope,
|
|
1318
|
-
test,
|
|
1319
|
-
response
|
|
1320
|
-
});
|
|
1321
|
-
|
|
1322
|
-
// Trigger bail on failed test
|
|
1323
|
-
if (this.bailEnabled && this.ownsController && !test.passed && !test.skipped) {
|
|
1324
|
-
this.abort('Test failure (--bail)');
|
|
1325
|
-
}
|
|
1326
|
-
}
|
|
1327
|
-
);
|
|
1328
|
-
this.emitConsoleOutput(postScriptResult.consoleOutput);
|
|
1329
|
-
|
|
1330
|
-
// Emit afterPostScript event
|
|
1331
|
-
const afterPostEnvelope = this.createEventEnvelope(context.collectionInfo, node.path, context, request);
|
|
1332
|
-
this.emit('afterPostScript', {
|
|
1333
|
-
...afterPostEnvelope,
|
|
1334
|
-
request,
|
|
1335
|
-
path: node.path,
|
|
1336
|
-
response,
|
|
1337
|
-
result: postScriptResult
|
|
1338
|
-
});
|
|
1339
|
-
|
|
1340
|
-
if (postScriptResult.success === false) {
|
|
1341
|
-
const error = new Error(
|
|
1342
|
-
`Post-request script error: ${postScriptResult.error}`
|
|
1343
|
-
) as ErrorWithPhase;
|
|
1344
|
-
error.phase = 'postrequest';
|
|
1345
|
-
throw error;
|
|
1346
|
-
}
|
|
1347
|
-
|
|
1348
|
-
combinedResult.tests.push(...postScriptResult.tests);
|
|
1349
|
-
combinedResult.consoleOutput.push(...postScriptResult.consoleOutput);
|
|
1350
|
-
}
|
|
1351
|
-
|
|
1352
|
-
return combinedResult;
|
|
1353
|
-
});
|
|
1354
|
-
}
|
|
1355
|
-
|
|
1356
|
-
// Update execution record with all tests
|
|
1357
|
-
const allTests = [...pluginEventTests, ...scriptResult.tests];
|
|
1358
|
-
executionRecord.tests = allTests;
|
|
1359
|
-
|
|
1360
|
-
const result: RequestResult = {
|
|
1361
|
-
requestId: request.id,
|
|
1362
|
-
requestName: request.name,
|
|
1363
|
-
path: node.path,
|
|
1364
|
-
success: isNullOrEmpty(response.error),
|
|
1365
|
-
response,
|
|
1366
|
-
tests: allTests,
|
|
1367
|
-
duration: response.duration,
|
|
1368
|
-
iteration: context.iterationCurrent
|
|
1369
|
-
};
|
|
1370
|
-
|
|
1371
|
-
// Emit afterItem event
|
|
1372
|
-
const afterItemEnvelope = this.createEventEnvelope(context.collectionInfo, node.path, context, request);
|
|
1373
|
-
this.emit('afterItem', {
|
|
1374
|
-
...afterItemEnvelope,
|
|
1375
|
-
request,
|
|
1376
|
-
path: node.path,
|
|
1377
|
-
response,
|
|
1378
|
-
result
|
|
1379
|
-
});
|
|
1380
|
-
|
|
1381
|
-
// Clear cookies if persist is false
|
|
1382
|
-
if (context.options.jar?.persist !== true) {
|
|
1383
|
-
context.cookieJar.clear();
|
|
1384
|
-
}
|
|
1385
|
-
|
|
1386
|
-
// POP request scope
|
|
1387
|
-
context.scopeStack.pop();
|
|
1388
|
-
|
|
1389
|
-
return result;
|
|
1390
|
-
} catch (error) {
|
|
1391
|
-
const err = error as ErrorWithPhase & { message?: string };
|
|
1392
|
-
|
|
1393
|
-
const result: RequestResult = {
|
|
1394
|
-
requestId: request.id,
|
|
1395
|
-
requestName: request.name,
|
|
1396
|
-
path: node.path,
|
|
1397
|
-
success: false,
|
|
1398
|
-
tests: [],
|
|
1399
|
-
duration: context.currentResponse?.duration ?? 0,
|
|
1400
|
-
scriptError: err.message ?? String(error),
|
|
1401
|
-
iteration: context.iterationCurrent
|
|
1402
|
-
};
|
|
1403
|
-
|
|
1404
|
-
const phase = err.phase ?? 'request';
|
|
1405
|
-
this.emit('exception', {
|
|
1406
|
-
id: randomUUID(),
|
|
1407
|
-
error,
|
|
1408
|
-
phase,
|
|
1409
|
-
request,
|
|
1410
|
-
path: node.path,
|
|
1411
|
-
response: context.currentResponse
|
|
1412
|
-
});
|
|
1413
|
-
|
|
1414
|
-
// POP request scope even on error
|
|
1415
|
-
context.scopeStack.pop();
|
|
1416
|
-
|
|
1417
|
-
// Abort execution to prevent further requests from running (fail-fast)
|
|
1418
|
-
this.abort(`Script error in ${phase}: ${result.scriptError}`);
|
|
1419
|
-
|
|
1420
|
-
return result;
|
|
1421
|
-
}
|
|
1422
|
-
}
|
|
1423
|
-
}
|