@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/dist/CollectionRunner.js
CHANGED
|
@@ -15,6 +15,7 @@ import { CookieJar } from './CookieJar.js';
|
|
|
15
15
|
import { isNullOrEmpty, isNullOrWhitespace } from './utils.js';
|
|
16
16
|
import { TaskGraph } from './TaskGraph.js';
|
|
17
17
|
import { DagScheduler } from './DagScheduler.js';
|
|
18
|
+
import { LibraryLoader } from './LibraryLoader.js';
|
|
18
19
|
export class CollectionRunner extends EventEmitter {
|
|
19
20
|
variableResolver;
|
|
20
21
|
pluginManager;
|
|
@@ -33,6 +34,9 @@ export class CollectionRunner extends EventEmitter {
|
|
|
33
34
|
bailEnabled = false;
|
|
34
35
|
abortReason;
|
|
35
36
|
shouldDelayNextRequest = false;
|
|
37
|
+
libraryLoader;
|
|
38
|
+
loadedLibraries = new Map();
|
|
39
|
+
folderScopeById = new Map();
|
|
36
40
|
constructor(options) {
|
|
37
41
|
super();
|
|
38
42
|
const logLevel = options?.logLevel ?? LogLevel.INFO;
|
|
@@ -45,6 +49,7 @@ export class CollectionRunner extends EventEmitter {
|
|
|
45
49
|
this.collectionValidator = new CollectionValidator(this.pluginManager, this.logger);
|
|
46
50
|
this.testCounter = new TestCounter(this.pluginManager, this.logger);
|
|
47
51
|
this.scriptEngine = new ScriptEngine(this.logger);
|
|
52
|
+
this.libraryLoader = new LibraryLoader(this.logger);
|
|
48
53
|
// Phase 1: Resolve plugins if directories provided (fast - just scans, no loading)
|
|
49
54
|
if (options?.pluginsDir !== undefined) {
|
|
50
55
|
const dirs = Array.isArray(options.pluginsDir)
|
|
@@ -178,7 +183,7 @@ export class CollectionRunner extends EventEmitter {
|
|
|
178
183
|
// Phase 2: Load ONLY required plugins
|
|
179
184
|
await this.pluginLoader.loadRequiredPlugins(this.resolvedPlugins, requirements);
|
|
180
185
|
this.logger.debug('Required plugins loaded');
|
|
181
|
-
this.logger.
|
|
186
|
+
this.logger.debug(`Starting collection: ${collection.info.name}`);
|
|
182
187
|
this.logger.debug(`Collection ID: ${collection.info.id}, Protocol: ${collection.protocol}`);
|
|
183
188
|
// Validate and cache protocol plugin
|
|
184
189
|
const protocolPlugin = this.pluginManager.getPlugin(collection.protocol);
|
|
@@ -187,7 +192,21 @@ export class CollectionRunner extends EventEmitter {
|
|
|
187
192
|
`Available protocols: ${this.pluginManager.getAllPlugins().flatMap(p => p.protocols).join(', ')}`);
|
|
188
193
|
}
|
|
189
194
|
// Merge runtime options (needed for validation)
|
|
190
|
-
const runtimeOptions = this.mergeOptions(collection.options, options);
|
|
195
|
+
const runtimeOptions = this.mergeOptions(collection.options, options, { includeMeta: true, includeProxy: true });
|
|
196
|
+
// Validate external libraries flag
|
|
197
|
+
if (runtimeOptions.libraries !== undefined && runtimeOptions.libraries.length > 0) {
|
|
198
|
+
if (options.allowExternalLibraries !== true) {
|
|
199
|
+
throw new Error(`Collection defines external libraries but --allow-external-libraries flag is not enabled. ` +
|
|
200
|
+
`External libraries (npm/file/cdn) pose security risks and must be explicitly allowed. ` +
|
|
201
|
+
`Use --allow-external-libraries to enable this feature.`);
|
|
202
|
+
}
|
|
203
|
+
this.logger.debug(`External libraries enabled: ${runtimeOptions.libraries.length} libraries to load`);
|
|
204
|
+
// Load external libraries
|
|
205
|
+
this.loadedLibraries = await this.libraryLoader.loadLibraries(runtimeOptions.libraries);
|
|
206
|
+
// Recreate script engine with loaded libraries
|
|
207
|
+
this.scriptEngine = new ScriptEngine(this.logger, this.loadedLibraries);
|
|
208
|
+
this.logger.debug(`Loaded ${this.loadedLibraries.size} external libraries`);
|
|
209
|
+
}
|
|
191
210
|
if (options.signal !== undefined) {
|
|
192
211
|
this.ownsController = false;
|
|
193
212
|
this.abortController = {
|
|
@@ -199,7 +218,7 @@ export class CollectionRunner extends EventEmitter {
|
|
|
199
218
|
else {
|
|
200
219
|
this.ownsController = true;
|
|
201
220
|
this.abortController = new AbortController();
|
|
202
|
-
this.logger.
|
|
221
|
+
this.logger.debug('Created internal abort controller');
|
|
203
222
|
}
|
|
204
223
|
this.abortReason = undefined;
|
|
205
224
|
this.bailEnabled = runtimeOptions.execution?.bail === true;
|
|
@@ -308,12 +327,13 @@ export class CollectionRunner extends EventEmitter {
|
|
|
308
327
|
});
|
|
309
328
|
}
|
|
310
329
|
}
|
|
311
|
-
// Initialize scope
|
|
312
|
-
const
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
330
|
+
// Initialize collection scope (root of scope chain)
|
|
331
|
+
const collectionScope = {
|
|
332
|
+
level: 'collection',
|
|
333
|
+
id: collection.info.id,
|
|
334
|
+
vars: {}
|
|
335
|
+
};
|
|
336
|
+
this.folderScopeById = new Map();
|
|
317
337
|
// Execute collection pre-request script once before all iterations
|
|
318
338
|
if (!isNullOrWhitespace(collection.collectionPreScript)) {
|
|
319
339
|
// Emit event before collection pre-script execution
|
|
@@ -327,14 +347,14 @@ export class CollectionRunner extends EventEmitter {
|
|
|
327
347
|
protocol: collection.protocol,
|
|
328
348
|
collectionVariables: collection.variables ?? {},
|
|
329
349
|
globalVariables: options.globalVariables ?? {},
|
|
330
|
-
|
|
350
|
+
scope: collectionScope,
|
|
331
351
|
environment: options.environment,
|
|
332
352
|
iterationCurrent: 1,
|
|
333
353
|
iterationCount,
|
|
334
354
|
iterationData,
|
|
335
355
|
iterationSource,
|
|
336
356
|
executionHistory: [],
|
|
337
|
-
options: this.mergeOptions(collection.options, options),
|
|
357
|
+
options: this.mergeOptions(collection.options, options, { includeMeta: true, includeProxy: true }),
|
|
338
358
|
cookieJar,
|
|
339
359
|
eventEmitter: this,
|
|
340
360
|
protocolPlugin,
|
|
@@ -357,7 +377,7 @@ export class CollectionRunner extends EventEmitter {
|
|
|
357
377
|
options.globalVariables = tempContext.globalVariables;
|
|
358
378
|
options.environment = tempContext.environment;
|
|
359
379
|
// Update collection scope with any changes from script
|
|
360
|
-
Object.assign(
|
|
380
|
+
Object.assign(collectionScope.vars, tempContext.scope.vars);
|
|
361
381
|
}
|
|
362
382
|
for (let i = 0; i < iterationCount; i++) {
|
|
363
383
|
if (this.isAborted()) {
|
|
@@ -370,14 +390,18 @@ export class CollectionRunner extends EventEmitter {
|
|
|
370
390
|
protocol: collection.protocol,
|
|
371
391
|
collectionVariables: collection.variables ?? {},
|
|
372
392
|
globalVariables: options.globalVariables ?? {},
|
|
373
|
-
|
|
393
|
+
scope: {
|
|
394
|
+
level: 'collection',
|
|
395
|
+
id: collectionScope.id,
|
|
396
|
+
vars: { ...collectionScope.vars }
|
|
397
|
+
},
|
|
374
398
|
environment: options.environment,
|
|
375
399
|
iterationCurrent: i + 1,
|
|
376
400
|
iterationCount,
|
|
377
401
|
iterationData,
|
|
378
402
|
iterationSource,
|
|
379
403
|
executionHistory: [],
|
|
380
|
-
options: this.mergeOptions(collection.options, options),
|
|
404
|
+
options: this.mergeOptions(collection.options, options, { includeMeta: true, includeProxy: true }),
|
|
381
405
|
cookieJar,
|
|
382
406
|
eventEmitter: this,
|
|
383
407
|
protocolPlugin,
|
|
@@ -400,49 +424,54 @@ export class CollectionRunner extends EventEmitter {
|
|
|
400
424
|
duration: iterationDuration
|
|
401
425
|
});
|
|
402
426
|
// Update collection scope with changes from iteration
|
|
403
|
-
Object.assign(
|
|
427
|
+
Object.assign(collectionScope.vars, context.scope.vars);
|
|
404
428
|
// Update global variables and environment for next iteration
|
|
405
429
|
options.globalVariables = context.globalVariables;
|
|
406
430
|
options.environment = context.environment;
|
|
407
431
|
}
|
|
408
432
|
// Execute collection post-request script once after all iterations
|
|
409
433
|
if (!isNullOrWhitespace(collection.collectionPostScript)) {
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
434
|
+
if (this.isAborted()) {
|
|
435
|
+
this.logger.warn('Skipping collection post-script due to abort');
|
|
436
|
+
}
|
|
437
|
+
else {
|
|
438
|
+
// Emit event before collection post-script execution
|
|
439
|
+
const beforePostEnvelope = this.createEventEnvelope(collection.info, 'collection:/', undefined);
|
|
440
|
+
this.emit('beforeCollectionPostScript', {
|
|
441
|
+
...beforePostEnvelope,
|
|
442
|
+
path: 'collection:/'
|
|
443
|
+
});
|
|
444
|
+
const tempContext = {
|
|
445
|
+
collectionInfo: collection.info,
|
|
446
|
+
protocol: collection.protocol,
|
|
447
|
+
collectionVariables: collection.variables ?? {},
|
|
448
|
+
globalVariables: options.globalVariables ?? {},
|
|
449
|
+
scope: collectionScope,
|
|
450
|
+
environment: options.environment,
|
|
451
|
+
iterationCurrent: iterationCount,
|
|
452
|
+
iterationCount,
|
|
453
|
+
iterationData,
|
|
454
|
+
iterationSource,
|
|
455
|
+
executionHistory: [],
|
|
456
|
+
options: this.mergeOptions(collection.options, options, { includeMeta: true, includeProxy: true }),
|
|
457
|
+
cookieJar,
|
|
458
|
+
eventEmitter: this,
|
|
459
|
+
protocolPlugin,
|
|
460
|
+
abortSignal: this.abortController?.signal
|
|
461
|
+
};
|
|
462
|
+
const postScriptResult = await this.queueScript(() => this.scriptEngine.execute(collection.collectionPostScript, tempContext, ScriptType.CollectionPost, () => { } // noop - collection post-scripts cannot have tests
|
|
463
|
+
));
|
|
464
|
+
this.emitConsoleOutput(postScriptResult.consoleOutput);
|
|
465
|
+
// Emit event for collection post-script completion
|
|
466
|
+
const afterPostEnvelope = this.createEventEnvelope(collection.info, 'collection:/', tempContext);
|
|
467
|
+
this.emit('afterCollectionPostScript', {
|
|
468
|
+
...afterPostEnvelope,
|
|
469
|
+
path: 'collection:/',
|
|
470
|
+
result: postScriptResult
|
|
471
|
+
});
|
|
472
|
+
if (postScriptResult.success === false) {
|
|
473
|
+
throw new Error(`Collection post-script error: ${postScriptResult.error}`);
|
|
474
|
+
}
|
|
446
475
|
}
|
|
447
476
|
}
|
|
448
477
|
const endTime = new Date();
|
|
@@ -479,63 +508,75 @@ export class CollectionRunner extends EventEmitter {
|
|
|
479
508
|
request.auth.data = this.variableResolver.resolveAll(request.auth.data, context);
|
|
480
509
|
}
|
|
481
510
|
}
|
|
482
|
-
mergeOptions(
|
|
483
|
-
|
|
484
|
-
|
|
511
|
+
mergeOptions(baseOptions, overrideOptions, options) {
|
|
512
|
+
const includeMeta = options?.includeMeta ?? true;
|
|
513
|
+
const includeProxy = options?.includeProxy ?? true;
|
|
514
|
+
// overrideOptions takes precedence over baseOptions
|
|
485
515
|
const merged = {
|
|
486
|
-
...
|
|
487
|
-
...
|
|
516
|
+
...baseOptions,
|
|
517
|
+
...overrideOptions,
|
|
488
518
|
// Deep merge nested objects
|
|
489
519
|
execution: {
|
|
490
|
-
...(
|
|
491
|
-
...(
|
|
492
|
-
}
|
|
493
|
-
// Ensure defaults
|
|
494
|
-
strictMode: runOptions?.strictMode ?? collectionOptions?.strictMode ?? true,
|
|
495
|
-
// Include filter options (type narrow from undefined)
|
|
496
|
-
filter: (runOptions?.filter ?? collectionOptions?.filter) !== undefined
|
|
497
|
-
? String(runOptions?.filter ?? collectionOptions?.filter)
|
|
498
|
-
: undefined,
|
|
499
|
-
excludeDeps: ((runOptions?.excludeDeps ?? collectionOptions?.excludeDeps) !== undefined)
|
|
500
|
-
? Boolean(runOptions?.excludeDeps ?? collectionOptions?.excludeDeps)
|
|
501
|
-
: undefined
|
|
520
|
+
...(baseOptions?.execution ?? {}),
|
|
521
|
+
...(overrideOptions?.execution ?? {})
|
|
522
|
+
}
|
|
502
523
|
};
|
|
524
|
+
if (includeMeta) {
|
|
525
|
+
merged.strictMode = overrideOptions?.strictMode ?? baseOptions?.strictMode ?? true;
|
|
526
|
+
merged.filter = (overrideOptions?.filter ?? baseOptions?.filter) !== undefined
|
|
527
|
+
? String(overrideOptions?.filter ?? baseOptions?.filter)
|
|
528
|
+
: undefined;
|
|
529
|
+
merged.excludeDeps = ((overrideOptions?.excludeDeps ?? baseOptions?.excludeDeps) !== undefined)
|
|
530
|
+
? Boolean(overrideOptions?.excludeDeps ?? baseOptions?.excludeDeps)
|
|
531
|
+
: undefined;
|
|
532
|
+
}
|
|
503
533
|
// Conditionally merge optional nested objects
|
|
504
|
-
if (
|
|
534
|
+
if (baseOptions?.timeout !== undefined || overrideOptions?.timeout !== undefined) {
|
|
505
535
|
merged.timeout = {
|
|
506
|
-
...(
|
|
507
|
-
...(
|
|
536
|
+
...(baseOptions?.timeout ?? {}),
|
|
537
|
+
...(overrideOptions?.timeout ?? {})
|
|
508
538
|
};
|
|
509
539
|
}
|
|
510
|
-
if (
|
|
540
|
+
if (baseOptions?.ssl !== undefined || overrideOptions?.ssl !== undefined) {
|
|
511
541
|
merged.ssl = {
|
|
512
|
-
...(
|
|
513
|
-
...(
|
|
542
|
+
...(baseOptions?.ssl ?? {}),
|
|
543
|
+
...(overrideOptions?.ssl ?? {})
|
|
514
544
|
};
|
|
515
545
|
}
|
|
516
|
-
if (
|
|
546
|
+
if (baseOptions?.jar !== undefined || overrideOptions?.jar !== undefined) {
|
|
517
547
|
merged.jar = {
|
|
518
|
-
persist:
|
|
548
|
+
persist: overrideOptions?.jar?.persist ?? baseOptions?.jar?.persist ?? false
|
|
519
549
|
};
|
|
520
550
|
}
|
|
521
|
-
if (
|
|
522
|
-
|
|
551
|
+
if (includeProxy) {
|
|
552
|
+
if (overrideOptions?.proxy !== null && overrideOptions?.proxy !== undefined) {
|
|
553
|
+
merged.proxy = overrideOptions.proxy;
|
|
554
|
+
}
|
|
555
|
+
else if (baseOptions?.proxy !== null && baseOptions?.proxy !== undefined) {
|
|
556
|
+
merged.proxy = baseOptions.proxy;
|
|
557
|
+
}
|
|
523
558
|
}
|
|
524
|
-
|
|
525
|
-
merged.
|
|
559
|
+
if (includeMeta) {
|
|
560
|
+
merged.strictMode = overrideOptions?.strictMode ?? baseOptions?.strictMode ?? true;
|
|
561
|
+
merged.filter = (overrideOptions?.filter ?? baseOptions?.filter) !== undefined
|
|
562
|
+
? String(overrideOptions?.filter ?? baseOptions?.filter)
|
|
563
|
+
: undefined;
|
|
564
|
+
merged.excludeDeps = ((overrideOptions?.excludeDeps ?? baseOptions?.excludeDeps) !== undefined)
|
|
565
|
+
? Boolean(overrideOptions?.excludeDeps ?? baseOptions?.excludeDeps)
|
|
566
|
+
: undefined;
|
|
526
567
|
}
|
|
527
|
-
// Merge cookies arrays (
|
|
528
|
-
const
|
|
529
|
-
const
|
|
530
|
-
if (
|
|
531
|
-
//
|
|
568
|
+
// Merge cookies arrays (override cookies + base cookies)
|
|
569
|
+
const baseCookies = baseOptions?.cookies ?? [];
|
|
570
|
+
const overrideCookies = overrideOptions?.cookies ?? [];
|
|
571
|
+
if (baseCookies.length > 0 || overrideCookies.length > 0) {
|
|
572
|
+
// Override cookies override base cookies with same name
|
|
532
573
|
const cookieMap = new Map();
|
|
533
|
-
// Add
|
|
534
|
-
for (const cookie of
|
|
574
|
+
// Add base cookies first
|
|
575
|
+
for (const cookie of baseCookies) {
|
|
535
576
|
cookieMap.set(cookie.name, cookie);
|
|
536
577
|
}
|
|
537
|
-
// Then add/override with
|
|
538
|
-
for (const cookie of
|
|
578
|
+
// Then add/override with override cookies
|
|
579
|
+
for (const cookie of overrideCookies) {
|
|
539
580
|
cookieMap.set(cookie.name, cookie);
|
|
540
581
|
}
|
|
541
582
|
merged.cookies = Array.from(cookieMap.values());
|
|
@@ -548,12 +589,14 @@ export class CollectionRunner extends EventEmitter {
|
|
|
548
589
|
*/
|
|
549
590
|
async evaluateCondition(condition, context) {
|
|
550
591
|
try {
|
|
551
|
-
// Use workaround: store result in global variable
|
|
552
|
-
const
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
592
|
+
// Use workaround: store result in global variable (serialized)
|
|
593
|
+
const result = await this.queueScript(async () => {
|
|
594
|
+
const wrappedScript = `
|
|
595
|
+
const __conditionResult = (${condition});
|
|
596
|
+
quest.global.variables.set('__conditionResult', String(__conditionResult === true));
|
|
597
|
+
`;
|
|
598
|
+
return await this.scriptEngine.execute(wrappedScript, context, ScriptType.PreRequest, () => { });
|
|
599
|
+
});
|
|
557
600
|
if (result.success === false) {
|
|
558
601
|
this.logger.warn(`Condition evaluation error: ${result.error}`);
|
|
559
602
|
return false;
|
|
@@ -699,12 +742,19 @@ export class CollectionRunner extends EventEmitter {
|
|
|
699
742
|
return;
|
|
700
743
|
}
|
|
701
744
|
const folder = node.item;
|
|
702
|
-
|
|
703
|
-
|
|
745
|
+
const parentScope = node.parentFolderItemId !== undefined
|
|
746
|
+
? this.folderScopeById.get(node.parentFolderItemId)
|
|
747
|
+
: context.scope;
|
|
748
|
+
if (parentScope === undefined) {
|
|
749
|
+
throw new Error(`Missing parent scope for folder '${folder.id}'`);
|
|
750
|
+
}
|
|
751
|
+
const folderScope = {
|
|
704
752
|
level: 'folder',
|
|
705
753
|
id: folder.id,
|
|
706
|
-
vars: {}
|
|
707
|
-
|
|
754
|
+
vars: {},
|
|
755
|
+
parent: parentScope
|
|
756
|
+
};
|
|
757
|
+
this.folderScopeById.set(folder.id, folderScope);
|
|
708
758
|
// Emit beforeFolder event
|
|
709
759
|
const beforeFolderEnvelope = this.createEventEnvelope(context.collectionInfo, node.path, context);
|
|
710
760
|
this.emit('beforeFolder', beforeFolderEnvelope);
|
|
@@ -721,12 +771,9 @@ export class CollectionRunner extends EventEmitter {
|
|
|
721
771
|
return;
|
|
722
772
|
}
|
|
723
773
|
const folder = node.item;
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
if (topScope?.level === 'folder' && topScope.id === folder.id) {
|
|
728
|
-
// POP folder scope
|
|
729
|
-
context.scopeStack.pop();
|
|
774
|
+
const folderScope = this.folderScopeById.get(folder.id);
|
|
775
|
+
if (folderScope !== undefined) {
|
|
776
|
+
this.folderScopeById.delete(folder.id);
|
|
730
777
|
// Emit afterFolder event
|
|
731
778
|
const afterFolderEnvelope = this.createEventEnvelope(context.collectionInfo, node.path, context);
|
|
732
779
|
this.emit('afterFolder', {
|
|
@@ -734,7 +781,6 @@ export class CollectionRunner extends EventEmitter {
|
|
|
734
781
|
duration: 0
|
|
735
782
|
});
|
|
736
783
|
}
|
|
737
|
-
// If scope doesn't match, folder-enter was skipped - no POP or event needed
|
|
738
784
|
}
|
|
739
785
|
async executeFolderPreScript(script, context, node, flags) {
|
|
740
786
|
if (flags.skip || flags.bail) {
|
|
@@ -824,6 +870,13 @@ export class CollectionRunner extends EventEmitter {
|
|
|
824
870
|
success: true,
|
|
825
871
|
tests: [],
|
|
826
872
|
duration: 0,
|
|
873
|
+
summary: {
|
|
874
|
+
outcome: 'success',
|
|
875
|
+
code: 'skipped',
|
|
876
|
+
label: 'Skipped',
|
|
877
|
+
message: 'Skipped by bail',
|
|
878
|
+
duration: 0
|
|
879
|
+
},
|
|
827
880
|
iteration: context.iterationCurrent,
|
|
828
881
|
scriptError: 'Skipped by bail'
|
|
829
882
|
};
|
|
@@ -837,6 +890,13 @@ export class CollectionRunner extends EventEmitter {
|
|
|
837
890
|
success: true,
|
|
838
891
|
tests: [],
|
|
839
892
|
duration: 0,
|
|
893
|
+
summary: {
|
|
894
|
+
outcome: 'success',
|
|
895
|
+
code: 'skipped',
|
|
896
|
+
label: 'Skipped',
|
|
897
|
+
message: 'Skipped by condition',
|
|
898
|
+
duration: 0
|
|
899
|
+
},
|
|
840
900
|
iteration: context.iterationCurrent,
|
|
841
901
|
scriptError: 'Skipped by condition'
|
|
842
902
|
};
|
|
@@ -849,32 +909,62 @@ export class CollectionRunner extends EventEmitter {
|
|
|
849
909
|
request,
|
|
850
910
|
path: node.path
|
|
851
911
|
});
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
}
|
|
912
|
+
const parentScope = node.parentFolderItemId !== undefined
|
|
913
|
+
? this.folderScopeById.get(node.parentFolderItemId)
|
|
914
|
+
: context.scope;
|
|
915
|
+
if (parentScope === undefined) {
|
|
916
|
+
throw new Error(`Missing parent scope for request '${request.id}'`);
|
|
917
|
+
}
|
|
918
|
+
const requestOptions = this.mergeOptions(context.options, request.options, { includeMeta: false, includeProxy: false });
|
|
919
|
+
const requestContext = {
|
|
920
|
+
...context,
|
|
921
|
+
scope: {
|
|
922
|
+
level: 'request',
|
|
923
|
+
id: request.id,
|
|
924
|
+
vars: {},
|
|
925
|
+
parent: parentScope
|
|
926
|
+
},
|
|
927
|
+
options: requestOptions
|
|
928
|
+
};
|
|
929
|
+
// For persist=false, use a per-request cookie jar to avoid cross-request leakage
|
|
930
|
+
if (requestContext.options.jar?.persist !== true) {
|
|
931
|
+
requestContext.cookieJar = new CookieJar({ persist: false });
|
|
932
|
+
// Inject initial cookies for this request (merged from collection/run/request options)
|
|
933
|
+
const initialCookies = requestContext.options.cookies ?? [];
|
|
934
|
+
for (const cookie of initialCookies) {
|
|
935
|
+
if (cookie.domain === null || cookie.domain === undefined) {
|
|
936
|
+
continue;
|
|
937
|
+
}
|
|
938
|
+
requestContext.cookieJar.set(cookie.name, cookie.value, {
|
|
939
|
+
domain: cookie.domain,
|
|
940
|
+
path: cookie.path,
|
|
941
|
+
expires: cookie.expires,
|
|
942
|
+
httpOnly: cookie.httpOnly,
|
|
943
|
+
secure: cookie.secure,
|
|
944
|
+
sameSite: cookie.sameSite
|
|
945
|
+
});
|
|
946
|
+
}
|
|
947
|
+
}
|
|
858
948
|
try {
|
|
859
949
|
// Execute all pre-request scripts (inherited + request-level) through queue
|
|
860
950
|
if (node.inheritedPreScripts !== undefined && node.inheritedPreScripts.length > 0) {
|
|
861
951
|
await this.queueScript(async () => {
|
|
862
952
|
// Set currentRequest inside the queued function to avoid race conditions in parallel execution
|
|
863
|
-
|
|
953
|
+
requestContext.currentRequest = request;
|
|
864
954
|
this.logger.debug(`Executing pre-script for request: id=${request.id}, name=${request.name}`);
|
|
865
955
|
for (const script of node.inheritedPreScripts) {
|
|
866
956
|
// Emit beforePreScript event
|
|
867
|
-
const beforePreEnvelope = this.createEventEnvelope(
|
|
957
|
+
const beforePreEnvelope = this.createEventEnvelope(requestContext.collectionInfo, node.path, requestContext, request);
|
|
868
958
|
this.emit('beforePreScript', {
|
|
869
959
|
...beforePreEnvelope,
|
|
870
960
|
request,
|
|
871
961
|
path: node.path
|
|
872
962
|
});
|
|
873
|
-
const preScriptResult = await this.scriptEngine.execute(script,
|
|
963
|
+
const preScriptResult = await this.scriptEngine.execute(script, requestContext, ScriptType.PreRequest, () => { } // Pre-request scripts cannot have tests
|
|
874
964
|
);
|
|
875
965
|
this.emitConsoleOutput(preScriptResult.consoleOutput);
|
|
876
966
|
// Emit afterPreScript event
|
|
877
|
-
const afterPreEnvelope = this.createEventEnvelope(
|
|
967
|
+
const afterPreEnvelope = this.createEventEnvelope(requestContext.collectionInfo, node.path, requestContext, request);
|
|
878
968
|
this.emit('afterPreScript', {
|
|
879
969
|
...afterPreEnvelope,
|
|
880
970
|
request,
|
|
@@ -891,13 +981,13 @@ export class CollectionRunner extends EventEmitter {
|
|
|
891
981
|
}
|
|
892
982
|
// HTTP execution NOT queued - runs in parallel
|
|
893
983
|
// Set currentRequest before I/O phase
|
|
894
|
-
|
|
984
|
+
requestContext.currentRequest = request;
|
|
895
985
|
// Apply effective auth from node (collection/folder auth inheritance)
|
|
896
986
|
// Request auth > Folder auth > Collection auth
|
|
897
987
|
if (node.effectiveAuth !== undefined) {
|
|
898
988
|
request.auth = node.effectiveAuth;
|
|
899
989
|
}
|
|
900
|
-
this.resolveRequest(request,
|
|
990
|
+
this.resolveRequest(request, requestContext);
|
|
901
991
|
// Track plugin event tests and indices
|
|
902
992
|
const pluginEventTests = [];
|
|
903
993
|
const eventIndices = new Map();
|
|
@@ -915,20 +1005,21 @@ export class CollectionRunner extends EventEmitter {
|
|
|
915
1005
|
const currentIndex = eventIndices.get(eventName) ?? 0;
|
|
916
1006
|
// Set event context (wrapped in try/finally to prevent state leak)
|
|
917
1007
|
try {
|
|
918
|
-
context.currentEvent = {
|
|
919
|
-
eventName: eventName,
|
|
920
|
-
requestId: request.id,
|
|
921
|
-
timestamp: new Date(),
|
|
922
|
-
data: eventData,
|
|
923
|
-
index: currentIndex
|
|
924
|
-
};
|
|
925
1008
|
// Execute plugin event script through the queue (serialized)
|
|
926
1009
|
const result = await this.queueScript(async () => {
|
|
927
|
-
|
|
1010
|
+
requestContext.currentEvent = {
|
|
1011
|
+
eventName: eventName,
|
|
1012
|
+
requestId: request.id,
|
|
1013
|
+
timestamp: new Date(),
|
|
1014
|
+
data: eventData,
|
|
1015
|
+
index: currentIndex
|
|
1016
|
+
};
|
|
1017
|
+
requestContext.currentRequest = request;
|
|
1018
|
+
return await this.scriptEngine.execute(eventScript.script, requestContext, ScriptType.PluginEvent, (test) => {
|
|
928
1019
|
// Emit assertion event for plugin event test
|
|
929
|
-
const eventDef =
|
|
1020
|
+
const eventDef = requestContext.protocolPlugin.events?.find(e => e.name === eventName);
|
|
930
1021
|
if (eventDef?.canHaveTests === true) {
|
|
931
|
-
const envelope = this.createEventEnvelope(
|
|
1022
|
+
const envelope = this.createEventEnvelope(requestContext.collectionInfo, node.path, requestContext, request);
|
|
932
1023
|
this.emit('assertion', {
|
|
933
1024
|
...envelope,
|
|
934
1025
|
test,
|
|
@@ -957,60 +1048,60 @@ export class CollectionRunner extends EventEmitter {
|
|
|
957
1048
|
}
|
|
958
1049
|
finally {
|
|
959
1050
|
// Always reset event context to prevent state leak
|
|
960
|
-
|
|
1051
|
+
requestContext.currentEvent = undefined;
|
|
961
1052
|
// Increment event index for next event of same type
|
|
962
1053
|
eventIndices.set(eventName, currentIndex + 1);
|
|
963
1054
|
}
|
|
964
1055
|
};
|
|
965
1056
|
// Emit beforeRequest event
|
|
966
|
-
const beforeRequestEnvelope = this.createEventEnvelope(
|
|
1057
|
+
const beforeRequestEnvelope = this.createEventEnvelope(requestContext.collectionInfo, node.path, requestContext, request);
|
|
967
1058
|
this.emit('beforeRequest', {
|
|
968
1059
|
...beforeRequestEnvelope,
|
|
969
1060
|
request,
|
|
970
1061
|
path: node.path
|
|
971
1062
|
});
|
|
972
1063
|
const response = await this.pluginManager.execute(context.protocol, request, // Use request directly instead of context.currentRequest
|
|
973
|
-
|
|
974
|
-
|
|
1064
|
+
requestContext, requestContext.options, emitEvent);
|
|
1065
|
+
requestContext.currentResponse = response;
|
|
975
1066
|
// Emit afterRequest event with duration from plugin (excludes delay)
|
|
976
|
-
const afterRequestEnvelope = this.createEventEnvelope(
|
|
1067
|
+
const afterRequestEnvelope = this.createEventEnvelope(requestContext.collectionInfo, node.path, requestContext, request);
|
|
977
1068
|
this.emit('afterRequest', {
|
|
978
1069
|
...afterRequestEnvelope,
|
|
979
1070
|
request,
|
|
980
1071
|
response,
|
|
981
|
-
duration: response.duration
|
|
1072
|
+
duration: response.summary.duration
|
|
982
1073
|
});
|
|
983
1074
|
// Add preliminary execution record to history
|
|
984
1075
|
const executionRecord = {
|
|
985
1076
|
id: request.id,
|
|
986
1077
|
name: request.name,
|
|
987
1078
|
path: node.path,
|
|
988
|
-
iteration:
|
|
1079
|
+
iteration: requestContext.iterationCurrent,
|
|
989
1080
|
response,
|
|
990
1081
|
tests: [...pluginEventTests],
|
|
991
1082
|
timestamp: new Date().toISOString()
|
|
992
1083
|
};
|
|
993
|
-
|
|
1084
|
+
requestContext.executionHistory.push(executionRecord);
|
|
994
1085
|
// Execute all post-request scripts (request-level + inherited) through queue
|
|
995
1086
|
let scriptResult = { success: true, tests: [], consoleOutput: [] };
|
|
996
1087
|
this.logger.debug(`Post-scripts for ${request.id}: ${node.inheritedPostScripts?.length ?? 0}`);
|
|
997
1088
|
if (node.inheritedPostScripts !== undefined && node.inheritedPostScripts.length > 0) {
|
|
998
1089
|
scriptResult = await this.queueScript(async () => {
|
|
999
1090
|
// Set currentRequest inside the queued function to avoid race conditions in parallel execution
|
|
1000
|
-
|
|
1091
|
+
requestContext.currentRequest = request;
|
|
1001
1092
|
const combinedResult = { success: true, tests: [], consoleOutput: [] };
|
|
1002
1093
|
for (const script of node.inheritedPostScripts) {
|
|
1003
1094
|
// Emit beforePostScript event
|
|
1004
|
-
const beforePostEnvelope = this.createEventEnvelope(
|
|
1095
|
+
const beforePostEnvelope = this.createEventEnvelope(requestContext.collectionInfo, node.path, requestContext, request);
|
|
1005
1096
|
this.emit('beforePostScript', {
|
|
1006
1097
|
...beforePostEnvelope,
|
|
1007
1098
|
request,
|
|
1008
1099
|
path: node.path,
|
|
1009
1100
|
response
|
|
1010
1101
|
});
|
|
1011
|
-
const postScriptResult = await this.scriptEngine.execute(script,
|
|
1102
|
+
const postScriptResult = await this.scriptEngine.execute(script, requestContext, ScriptType.PostRequest, (test) => {
|
|
1012
1103
|
// Emit assertion event
|
|
1013
|
-
const envelope = this.createEventEnvelope(
|
|
1104
|
+
const envelope = this.createEventEnvelope(requestContext.collectionInfo, node.path, requestContext, request);
|
|
1014
1105
|
this.emit('assertion', {
|
|
1015
1106
|
...envelope,
|
|
1016
1107
|
test,
|
|
@@ -1023,7 +1114,7 @@ export class CollectionRunner extends EventEmitter {
|
|
|
1023
1114
|
});
|
|
1024
1115
|
this.emitConsoleOutput(postScriptResult.consoleOutput);
|
|
1025
1116
|
// Emit afterPostScript event
|
|
1026
|
-
const afterPostEnvelope = this.createEventEnvelope(
|
|
1117
|
+
const afterPostEnvelope = this.createEventEnvelope(requestContext.collectionInfo, node.path, requestContext, request);
|
|
1027
1118
|
this.emit('afterPostScript', {
|
|
1028
1119
|
...afterPostEnvelope,
|
|
1029
1120
|
request,
|
|
@@ -1049,14 +1140,15 @@ export class CollectionRunner extends EventEmitter {
|
|
|
1049
1140
|
requestId: request.id,
|
|
1050
1141
|
requestName: request.name,
|
|
1051
1142
|
path: node.path,
|
|
1052
|
-
success:
|
|
1143
|
+
success: response.summary.outcome === 'success',
|
|
1053
1144
|
response,
|
|
1054
1145
|
tests: allTests,
|
|
1055
|
-
duration: response.duration,
|
|
1056
|
-
|
|
1146
|
+
duration: response.summary.duration ?? 0,
|
|
1147
|
+
summary: response.summary,
|
|
1148
|
+
iteration: requestContext.iterationCurrent
|
|
1057
1149
|
};
|
|
1058
1150
|
// Emit afterItem event
|
|
1059
|
-
const afterItemEnvelope = this.createEventEnvelope(
|
|
1151
|
+
const afterItemEnvelope = this.createEventEnvelope(requestContext.collectionInfo, node.path, requestContext, request);
|
|
1060
1152
|
this.emit('afterItem', {
|
|
1061
1153
|
...afterItemEnvelope,
|
|
1062
1154
|
request,
|
|
@@ -1065,11 +1157,9 @@ export class CollectionRunner extends EventEmitter {
|
|
|
1065
1157
|
result
|
|
1066
1158
|
});
|
|
1067
1159
|
// Clear cookies if persist is false
|
|
1068
|
-
if (
|
|
1069
|
-
|
|
1160
|
+
if (requestContext.options.jar?.persist !== true) {
|
|
1161
|
+
requestContext.cookieJar.clear();
|
|
1070
1162
|
}
|
|
1071
|
-
// POP request scope
|
|
1072
|
-
context.scopeStack.pop();
|
|
1073
1163
|
return result;
|
|
1074
1164
|
}
|
|
1075
1165
|
catch (error) {
|
|
@@ -1080,9 +1170,16 @@ export class CollectionRunner extends EventEmitter {
|
|
|
1080
1170
|
path: node.path,
|
|
1081
1171
|
success: false,
|
|
1082
1172
|
tests: [],
|
|
1083
|
-
duration:
|
|
1173
|
+
duration: requestContext.currentResponse?.summary?.duration ?? 0,
|
|
1174
|
+
summary: requestContext.currentResponse?.summary ?? {
|
|
1175
|
+
outcome: 'error',
|
|
1176
|
+
code: 'script',
|
|
1177
|
+
label: 'Script Error',
|
|
1178
|
+
message: err.message ?? String(error),
|
|
1179
|
+
duration: requestContext.currentResponse?.summary?.duration ?? 0
|
|
1180
|
+
},
|
|
1084
1181
|
scriptError: err.message ?? String(error),
|
|
1085
|
-
iteration:
|
|
1182
|
+
iteration: requestContext.iterationCurrent
|
|
1086
1183
|
};
|
|
1087
1184
|
const phase = err.phase ?? 'request';
|
|
1088
1185
|
this.emit('exception', {
|
|
@@ -1091,10 +1188,8 @@ export class CollectionRunner extends EventEmitter {
|
|
|
1091
1188
|
phase,
|
|
1092
1189
|
request,
|
|
1093
1190
|
path: node.path,
|
|
1094
|
-
response:
|
|
1191
|
+
response: requestContext.currentResponse
|
|
1095
1192
|
});
|
|
1096
|
-
// POP request scope even on error
|
|
1097
|
-
context.scopeStack.pop();
|
|
1098
1193
|
// Abort execution to prevent further requests from running (fail-fast)
|
|
1099
1194
|
this.abort(`Script error in ${phase}: ${result.scriptError}`);
|
|
1100
1195
|
return result;
|