@apiquest/fracture 1.0.2 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (195) hide show
  1. package/README.md +207 -0
  2. package/bin/cli.js +2 -2
  3. package/dist/CollectionRunner.d.ts +2 -0
  4. package/dist/CollectionRunner.d.ts.map +1 -1
  5. package/dist/CollectionRunner.js +23 -5
  6. package/dist/CollectionRunner.js.map +1 -1
  7. package/dist/LibraryLoader.d.ts +49 -0
  8. package/dist/LibraryLoader.d.ts.map +1 -0
  9. package/dist/LibraryLoader.js +198 -0
  10. package/dist/LibraryLoader.js.map +1 -0
  11. package/dist/PluginLoader.d.ts.map +1 -1
  12. package/dist/PluginLoader.js +9 -6
  13. package/dist/PluginLoader.js.map +1 -1
  14. package/dist/PluginResolver.d.ts +1 -1
  15. package/dist/PluginResolver.d.ts.map +1 -1
  16. package/dist/PluginResolver.js +1 -1
  17. package/dist/PluginResolver.js.map +1 -1
  18. package/dist/ScriptEngine.d.ts +2 -1
  19. package/dist/ScriptEngine.d.ts.map +1 -1
  20. package/dist/ScriptEngine.js +19 -12
  21. package/dist/ScriptEngine.js.map +1 -1
  22. package/dist/cli/index.js +35 -3
  23. package/dist/cli/index.js.map +1 -1
  24. package/dist/cli/plugin-commands.d.ts.map +1 -1
  25. package/dist/cli/plugin-commands.js +48 -81
  26. package/dist/cli/plugin-commands.js.map +1 -1
  27. package/dist/cli/plugin-installer.d.ts +48 -0
  28. package/dist/cli/plugin-installer.d.ts.map +1 -0
  29. package/dist/cli/plugin-installer.js +136 -0
  30. package/dist/cli/plugin-installer.js.map +1 -0
  31. package/dist/cli/plugin-registry.d.ts +17 -0
  32. package/dist/cli/plugin-registry.d.ts.map +1 -0
  33. package/dist/cli/plugin-registry.js +77 -0
  34. package/dist/cli/plugin-registry.js.map +1 -0
  35. package/package.json +55 -50
  36. package/tsconfig.json +20 -20
  37. package/tsconfig.test.json +5 -5
  38. package/vitest.config.ts +22 -22
  39. package/dist/ExecutionTree.d.ts +0 -77
  40. package/dist/ExecutionTree.d.ts.map +0 -1
  41. package/dist/ExecutionTree.js +0 -265
  42. package/dist/ExecutionTree.js.map +0 -1
  43. package/dist/fracture/src/CollectionAnalyzer.d.ts +0 -17
  44. package/dist/fracture/src/CollectionAnalyzer.d.ts.map +0 -1
  45. package/dist/fracture/src/CollectionAnalyzer.js +0 -70
  46. package/dist/fracture/src/CollectionAnalyzer.js.map +0 -1
  47. package/dist/fracture/src/CollectionRunner.d.ts +0 -39
  48. package/dist/fracture/src/CollectionRunner.d.ts.map +0 -1
  49. package/dist/fracture/src/CollectionRunner.js +0 -802
  50. package/dist/fracture/src/CollectionRunner.js.map +0 -1
  51. package/dist/fracture/src/CollectionRunner.types.d.ts +0 -8
  52. package/dist/fracture/src/CollectionRunner.types.d.ts.map +0 -1
  53. package/dist/fracture/src/CollectionRunner.types.js +0 -2
  54. package/dist/fracture/src/CollectionRunner.types.js.map +0 -1
  55. package/dist/fracture/src/CollectionValidator.d.ts +0 -14
  56. package/dist/fracture/src/CollectionValidator.d.ts.map +0 -1
  57. package/dist/fracture/src/CollectionValidator.js +0 -145
  58. package/dist/fracture/src/CollectionValidator.js.map +0 -1
  59. package/dist/fracture/src/ConsoleReporter.d.ts +0 -24
  60. package/dist/fracture/src/ConsoleReporter.d.ts.map +0 -1
  61. package/dist/fracture/src/ConsoleReporter.js +0 -123
  62. package/dist/fracture/src/ConsoleReporter.js.map +0 -1
  63. package/dist/fracture/src/CookieJar.d.ts +0 -70
  64. package/dist/fracture/src/CookieJar.d.ts.map +0 -1
  65. package/dist/fracture/src/CookieJar.js +0 -233
  66. package/dist/fracture/src/CookieJar.js.map +0 -1
  67. package/dist/fracture/src/ExecutionTree.d.ts +0 -77
  68. package/dist/fracture/src/ExecutionTree.d.ts.map +0 -1
  69. package/dist/fracture/src/ExecutionTree.js +0 -258
  70. package/dist/fracture/src/ExecutionTree.js.map +0 -1
  71. package/dist/fracture/src/Logger.d.ts +0 -25
  72. package/dist/fracture/src/Logger.d.ts.map +0 -1
  73. package/dist/fracture/src/Logger.js +0 -78
  74. package/dist/fracture/src/Logger.js.map +0 -1
  75. package/dist/fracture/src/PluginLoader.d.ts +0 -23
  76. package/dist/fracture/src/PluginLoader.d.ts.map +0 -1
  77. package/dist/fracture/src/PluginLoader.js +0 -102
  78. package/dist/fracture/src/PluginLoader.js.map +0 -1
  79. package/dist/fracture/src/PluginManager.d.ts +0 -64
  80. package/dist/fracture/src/PluginManager.d.ts.map +0 -1
  81. package/dist/fracture/src/PluginManager.js +0 -162
  82. package/dist/fracture/src/PluginManager.js.map +0 -1
  83. package/dist/fracture/src/PluginResolver.d.ts +0 -35
  84. package/dist/fracture/src/PluginResolver.d.ts.map +0 -1
  85. package/dist/fracture/src/PluginResolver.js +0 -128
  86. package/dist/fracture/src/PluginResolver.js.map +0 -1
  87. package/dist/fracture/src/QuestAPI.d.ts +0 -9
  88. package/dist/fracture/src/QuestAPI.d.ts.map +0 -1
  89. package/dist/fracture/src/QuestAPI.js +0 -679
  90. package/dist/fracture/src/QuestAPI.js.map +0 -1
  91. package/dist/fracture/src/QuestAPI.types.d.ts +0 -35
  92. package/dist/fracture/src/QuestAPI.types.d.ts.map +0 -1
  93. package/dist/fracture/src/QuestAPI.types.js +0 -3
  94. package/dist/fracture/src/QuestAPI.types.js.map +0 -1
  95. package/dist/fracture/src/QuestTestAPI.d.ts +0 -12
  96. package/dist/fracture/src/QuestTestAPI.d.ts.map +0 -1
  97. package/dist/fracture/src/QuestTestAPI.js +0 -133
  98. package/dist/fracture/src/QuestTestAPI.js.map +0 -1
  99. package/dist/fracture/src/ScriptEngine.d.ts +0 -21
  100. package/dist/fracture/src/ScriptEngine.d.ts.map +0 -1
  101. package/dist/fracture/src/ScriptEngine.js +0 -183
  102. package/dist/fracture/src/ScriptEngine.js.map +0 -1
  103. package/dist/fracture/src/ScriptValidator.d.ts +0 -68
  104. package/dist/fracture/src/ScriptValidator.d.ts.map +0 -1
  105. package/dist/fracture/src/ScriptValidator.js +0 -351
  106. package/dist/fracture/src/ScriptValidator.js.map +0 -1
  107. package/dist/fracture/src/TestCounter.d.ts +0 -18
  108. package/dist/fracture/src/TestCounter.d.ts.map +0 -1
  109. package/dist/fracture/src/TestCounter.js +0 -82
  110. package/dist/fracture/src/TestCounter.js.map +0 -1
  111. package/dist/fracture/src/VariableResolver.d.ts +0 -20
  112. package/dist/fracture/src/VariableResolver.d.ts.map +0 -1
  113. package/dist/fracture/src/VariableResolver.js +0 -100
  114. package/dist/fracture/src/VariableResolver.js.map +0 -1
  115. package/dist/fracture/src/cli/index.d.ts +0 -3
  116. package/dist/fracture/src/cli/index.d.ts.map +0 -1
  117. package/dist/fracture/src/cli/index.js +0 -347
  118. package/dist/fracture/src/cli/index.js.map +0 -1
  119. package/dist/fracture/src/cli/plugin-commands.d.ts +0 -6
  120. package/dist/fracture/src/cli/plugin-commands.d.ts.map +0 -1
  121. package/dist/fracture/src/cli/plugin-commands.js +0 -263
  122. package/dist/fracture/src/cli/plugin-commands.js.map +0 -1
  123. package/dist/fracture/src/cli/plugin-discovery.d.ts +0 -11
  124. package/dist/fracture/src/cli/plugin-discovery.d.ts.map +0 -1
  125. package/dist/fracture/src/cli/plugin-discovery.js +0 -64
  126. package/dist/fracture/src/cli/plugin-discovery.js.map +0 -1
  127. package/dist/fracture/src/index.d.ts +0 -13
  128. package/dist/fracture/src/index.d.ts.map +0 -1
  129. package/dist/fracture/src/index.js +0 -17
  130. package/dist/fracture/src/index.js.map +0 -1
  131. package/dist/fracture/src/utils.d.ts +0 -28
  132. package/dist/fracture/src/utils.d.ts.map +0 -1
  133. package/dist/fracture/src/utils.js +0 -48
  134. package/dist/fracture/src/utils.js.map +0 -1
  135. package/dist/plugin-auth/src/apikey-auth.d.ts +0 -3
  136. package/dist/plugin-auth/src/apikey-auth.d.ts.map +0 -1
  137. package/dist/plugin-auth/src/apikey-auth.js +0 -73
  138. package/dist/plugin-auth/src/apikey-auth.js.map +0 -1
  139. package/dist/plugin-auth/src/basic-auth.d.ts +0 -3
  140. package/dist/plugin-auth/src/basic-auth.d.ts.map +0 -1
  141. package/dist/plugin-auth/src/basic-auth.js +0 -61
  142. package/dist/plugin-auth/src/basic-auth.js.map +0 -1
  143. package/dist/plugin-auth/src/bearer-auth.d.ts +0 -3
  144. package/dist/plugin-auth/src/bearer-auth.d.ts.map +0 -1
  145. package/dist/plugin-auth/src/bearer-auth.js +0 -49
  146. package/dist/plugin-auth/src/bearer-auth.js.map +0 -1
  147. package/dist/plugin-auth/src/helpers.d.ts +0 -3
  148. package/dist/plugin-auth/src/helpers.d.ts.map +0 -1
  149. package/dist/plugin-auth/src/helpers.js +0 -8
  150. package/dist/plugin-auth/src/helpers.js.map +0 -1
  151. package/dist/plugin-auth/src/index.d.ts +0 -10
  152. package/dist/plugin-auth/src/index.d.ts.map +0 -1
  153. package/dist/plugin-auth/src/index.js +0 -25
  154. package/dist/plugin-auth/src/index.js.map +0 -1
  155. package/dist/plugin-auth/src/oauth2-auth.d.ts +0 -35
  156. package/dist/plugin-auth/src/oauth2-auth.d.ts.map +0 -1
  157. package/dist/plugin-auth/src/oauth2-auth.js +0 -266
  158. package/dist/plugin-auth/src/oauth2-auth.js.map +0 -1
  159. package/dist/plugin-http/src/index.d.ts +0 -4
  160. package/dist/plugin-http/src/index.d.ts.map +0 -1
  161. package/dist/plugin-http/src/index.js +0 -266
  162. package/dist/plugin-http/src/index.js.map +0 -1
  163. package/dist/plugin-vault-file/src/index.d.ts +0 -67
  164. package/dist/plugin-vault-file/src/index.d.ts.map +0 -1
  165. package/dist/plugin-vault-file/src/index.js +0 -171
  166. package/dist/plugin-vault-file/src/index.js.map +0 -1
  167. package/dist/types.d.ts +0 -374
  168. package/dist/types.d.ts.map +0 -1
  169. package/dist/types.js +0 -13
  170. package/dist/types.js.map +0 -1
  171. package/src/CollectionAnalyzer.ts +0 -102
  172. package/src/CollectionRunner.ts +0 -1423
  173. package/src/CollectionRunner.types.ts +0 -9
  174. package/src/CollectionValidator.ts +0 -289
  175. package/src/ConsoleReporter.ts +0 -143
  176. package/src/CookieJar.ts +0 -258
  177. package/src/DagScheduler.ts +0 -439
  178. package/src/Logger.ts +0 -85
  179. package/src/PluginLoader.ts +0 -126
  180. package/src/PluginManager.ts +0 -208
  181. package/src/PluginResolver.ts +0 -154
  182. package/src/QuestAPI.ts +0 -764
  183. package/src/QuestAPI.types.ts +0 -33
  184. package/src/QuestTestAPI.ts +0 -164
  185. package/src/RequestFilter.ts +0 -224
  186. package/src/ScriptEngine.ts +0 -219
  187. package/src/ScriptValidator.ts +0 -428
  188. package/src/TaskGraph.ts +0 -598
  189. package/src/TestCounter.ts +0 -109
  190. package/src/VariableResolver.ts +0 -114
  191. package/src/cli/index.ts +0 -480
  192. package/src/cli/plugin-commands.ts +0 -341
  193. package/src/cli/plugin-discovery.ts +0 -44
  194. package/src/index.ts +0 -24
  195. package/src/utils.ts +0 -52
@@ -1,802 +0,0 @@
1
- import { EventEmitter } from 'events';
2
- import { ScriptType, LogLevel } from '@apiquest/types';
3
- import { VariableResolver } from './VariableResolver.js';
4
- import { PluginManager } from './PluginManager.js';
5
- import { PluginLoader } from './PluginLoader.js';
6
- import { PluginResolver } from './PluginResolver.js';
7
- import { CollectionAnalyzer } from './CollectionAnalyzer.js';
8
- import { CollectionValidator } from './CollectionValidator.js';
9
- import { TestCounter } from './TestCounter.js';
10
- import { ScriptEngine } from './ScriptEngine.js';
11
- import { TreeBuilder } from './ExecutionTree.js';
12
- import { Logger } from './Logger.js';
13
- import { CookieJar } from './CookieJar.js';
14
- import { isNullOrEmpty, isNullOrWhitespace } from './utils.js';
15
- export class CollectionRunner extends EventEmitter {
16
- variableResolver;
17
- pluginManager;
18
- pluginResolver;
19
- pluginLoader;
20
- collectionAnalyzer;
21
- collectionValidator;
22
- testCounter;
23
- scriptEngine;
24
- scriptQueue = Promise.resolve(); // Script execution mutex
25
- pluginResolutionPromise; // Track plugin resolution (Phase 1)
26
- resolvedPlugins = []; // Available plugins
27
- logger;
28
- constructor(options) {
29
- super();
30
- const logLevel = options?.logLevel ?? LogLevel.INFO;
31
- // Create logger first so it can be passed to other components
32
- this.logger = new Logger('CollectionRunner', logLevel, this);
33
- this.variableResolver = new VariableResolver(this.logger);
34
- this.pluginManager = new PluginManager(this, logLevel);
35
- this.pluginResolver = new PluginResolver(this.logger);
36
- this.pluginLoader = new PluginLoader(this.pluginManager, this.logger);
37
- this.collectionAnalyzer = new CollectionAnalyzer();
38
- this.collectionValidator = new CollectionValidator(this.pluginManager);
39
- this.testCounter = new TestCounter(this.pluginManager);
40
- this.scriptEngine = new ScriptEngine(this.logger);
41
- // Phase 1: Resolve plugins if directories provided (fast - just scans, no loading)
42
- if (options?.pluginsDir !== undefined) {
43
- const dirs = Array.isArray(options.pluginsDir)
44
- ? options.pluginsDir
45
- : [options.pluginsDir];
46
- // Start plugin resolution (but don't block constructor)
47
- this.pluginResolutionPromise = this.pluginResolver.scanDirectories(dirs);
48
- }
49
- else {
50
- // No plugins to resolve
51
- this.pluginResolutionPromise = Promise.resolve([]);
52
- }
53
- }
54
- /**
55
- * Queue script execution to ensure sequential execution (thread-safe)
56
- */
57
- async queueScript(fn) {
58
- const resultPromise = this.scriptQueue.then(fn);
59
- this.scriptQueue = resultPromise.then(() => { }, () => { });
60
- return resultPromise;
61
- }
62
- registerPlugin(plugin) {
63
- this.pluginManager.registerPlugin(plugin);
64
- }
65
- registerAuthPlugin(plugin) {
66
- this.pluginManager.registerAuthPlugin(plugin);
67
- }
68
- emitConsoleOutput(messages) {
69
- if (messages === undefined || messages.length === 0)
70
- return;
71
- for (const message of messages) {
72
- let level = 'log';
73
- let cleanMessage = message;
74
- if (message.startsWith('[INFO] ')) {
75
- level = 'info';
76
- cleanMessage = message.replace('[INFO] ', '');
77
- }
78
- else if (message.startsWith('[WARN] ')) {
79
- level = 'warn';
80
- cleanMessage = message.replace('[WARN] ', '');
81
- }
82
- else if (message.startsWith('[ERROR] ')) {
83
- level = 'error';
84
- cleanMessage = message.replace('[ERROR] ', '');
85
- }
86
- this.emit('console', { message: cleanMessage, level });
87
- }
88
- }
89
- /**
90
- * Subscribe to ALL events emitted by the runner (including custom plugin events)
91
- * Overrides emit to intercept all event emissions
92
- */
93
- onAll(handler) {
94
- // Store original emit
95
- const originalEmit = this.emit.bind(this);
96
- // Override emit to intercept all events
97
- this.emit = function (event, ...args) {
98
- // Call original handler first
99
- const result = originalEmit(event, ...args);
100
- // Skip internal EventEmitter events
101
- if (event !== 'newListener' && event !== 'removeListener') {
102
- handler(String(event), args[0]);
103
- }
104
- return result;
105
- };
106
- // Return cleanup function
107
- return () => {
108
- // Restore original emit
109
- this.emit = originalEmit;
110
- };
111
- }
112
- /**
113
- * Create event envelope for all events (except console)
114
- */
115
- createEventEnvelope(collectionInfo, path, context, request) {
116
- const pathType = path === 'collection:/' ? 'collection' : path.startsWith('folder:/') ? 'folder' : 'request';
117
- const envelope = {
118
- path,
119
- pathType,
120
- collectionInfo
121
- };
122
- // Add iteration info if available
123
- if (context !== undefined) {
124
- const currentRow = context.currentIterationData;
125
- envelope.iteration = {
126
- current: context.iterationCurrent,
127
- total: context.iterationCount,
128
- source: context.iterationSource,
129
- rowIndex: currentRow !== undefined ? context.iterationCurrent - 1 : undefined,
130
- rowKeys: currentRow !== undefined ? Object.keys(currentRow) : undefined,
131
- row: currentRow
132
- };
133
- }
134
- // Add request if provided
135
- if (request !== undefined) {
136
- envelope.request = request;
137
- }
138
- return envelope;
139
- }
140
- async run(collection, options = {}) {
141
- const startTime = new Date();
142
- // Set log level from options (if provided, overrides constructor level)
143
- if (options.logLevel !== undefined) {
144
- this.logger.setLevel(options.logLevel);
145
- this.pluginManager.setLogLevel(options.logLevel);
146
- }
147
- // Phase 1: Wait for plugin resolution
148
- this.resolvedPlugins = await this.pluginResolutionPromise;
149
- this.logger.debug(`Plugin resolution complete: ${this.resolvedPlugins.length} plugins available`);
150
- // Analyze collection to determine required plugins
151
- const requirements = this.collectionAnalyzer.analyzeRequirements(collection);
152
- this.logger.debug(`Collection requires: protocols=[${Array.from(requirements.protocols)}], auth=[${Array.from(requirements.authTypes)}], providers=[${Array.from(requirements.valueProviders)}]`);
153
- // Phase 2: Load ONLY required plugins
154
- await this.pluginLoader.loadRequiredPlugins(this.resolvedPlugins, requirements);
155
- this.logger.debug('Required plugins loaded');
156
- this.logger.info(`Starting collection: ${collection.info.name}`);
157
- this.logger.debug(`Collection ID: ${collection.info.id}, Protocol: ${collection.protocol}`);
158
- // Validate and cache protocol plugin
159
- const protocolPlugin = this.pluginManager.getPlugin(collection.protocol);
160
- if (protocolPlugin === undefined) {
161
- throw new Error(`No plugin registered for protocol '${collection.protocol}'. ` +
162
- `Available protocols: ${this.pluginManager.getAllPlugins().flatMap(p => p.protocols).join(', ')}`);
163
- }
164
- // Merge runtime options (needed for validation)
165
- const runtimeOptions = this.mergeOptions(collection.options, options);
166
- // Get strict mode with precedence: RunOptions > Collection.options > Default (true)
167
- const strictMode = options.strictMode ?? collection.options?.strictMode ?? true;
168
- // PRE-RUN VALIDATION
169
- const validationResult = await this.collectionValidator.validateCollection(collection, runtimeOptions, strictMode);
170
- // COUNT EXPECTED TESTS (only in strict mode for deterministic counting)
171
- const expectedTestCount = strictMode ? this.testCounter.countTests(collection) : -1;
172
- const treeBuilder = new TreeBuilder(options.data, this.logger);
173
- const tree = treeBuilder.build(collection);
174
- const collectionItem = tree.item;
175
- const iterationData = collectionItem.testData ?? [];
176
- const iterationCount = iterationData.length > 0 ? iterationData.length : 1;
177
- // Determine iteration source for event envelopes
178
- const iterationSource = options.data !== undefined ? 'cli' : (collection.testData !== undefined ? 'collection' : 'none');
179
- // Emit beforeRun with validation results and expected test count
180
- // Note: -1 means dynamic (can't determine), undefined means not calculated
181
- this.emit('beforeRun', {
182
- collectionInfo: {
183
- id: collection.info.id,
184
- name: collection.info.name,
185
- version: collection.info.version,
186
- description: collection.info.description
187
- },
188
- options,
189
- validationResult,
190
- expectedTestCount // Include -1 for dynamic tests
191
- });
192
- // STOP if validation failed
193
- if (validationResult.valid === false) {
194
- const endTime = new Date();
195
- return {
196
- collectionId: collection.info.id,
197
- collectionName: collection.info.name,
198
- startTime,
199
- endTime,
200
- duration: endTime.getTime() - startTime.getTime(),
201
- requestResults: [],
202
- totalTests: 0,
203
- passedTests: 0,
204
- failedTests: 0,
205
- skippedTests: 0,
206
- validationErrors: validationResult.errors
207
- };
208
- }
209
- const requestResults = [];
210
- // Initialize cookie jar - always create one
211
- // If jar.persist = true, cookies carry across requests
212
- // If jar.persist = false (default), each request gets cookies from response only
213
- const cookieJar = new CookieJar(runtimeOptions.jar ?? { persist: false });
214
- // Inject initial cookies if provided in options
215
- if (runtimeOptions.cookies !== null && runtimeOptions.cookies !== undefined && runtimeOptions.cookies.length > 0) {
216
- for (const cookie of runtimeOptions.cookies) {
217
- // Skip cookies without domain - domain is required for RFC 6265 compliance
218
- if (cookie.domain === null || cookie.domain === undefined) {
219
- continue;
220
- }
221
- cookieJar.set(cookie.name, cookie.value, {
222
- domain: cookie.domain,
223
- path: cookie.path,
224
- expires: cookie.expires,
225
- httpOnly: cookie.httpOnly,
226
- secure: cookie.secure,
227
- sameSite: cookie.sameSite
228
- });
229
- }
230
- }
231
- // Initialize scope stack with collection scope
232
- const scopeStack = [{
233
- level: 'collection',
234
- id: collection.info.id,
235
- vars: {}
236
- }];
237
- // Execute collection pre-request script once before all iterations
238
- if (!isNullOrWhitespace(collection.collectionPreScript)) {
239
- // Emit event before collection pre-script execution
240
- const envelope = this.createEventEnvelope(collection.info, 'collection:/', undefined);
241
- this.emit('beforeCollectionPreScript', {
242
- ...envelope,
243
- path: 'collection:/'
244
- });
245
- const tempContext = {
246
- collectionInfo: collection.info,
247
- protocol: collection.protocol,
248
- collectionVariables: collection.variables ?? {},
249
- globalVariables: options.globalVariables ?? {},
250
- scopeStack: [...scopeStack], // Clone scope stack
251
- environment: options.environment,
252
- iterationCurrent: 1,
253
- iterationCount,
254
- iterationData,
255
- currentIterationData: iterationData[0],
256
- iterationSource,
257
- executionHistory: [],
258
- options: this.mergeOptions(collection.options, options),
259
- cookieJar,
260
- eventEmitter: this,
261
- protocolPlugin
262
- };
263
- const preScriptResult = await this.queueScript(() => this.scriptEngine.execute(collection.collectionPreScript, tempContext, ScriptType.CollectionPre, () => { } // noop - collection pre-scripts cannot have tests
264
- ));
265
- this.emitConsoleOutput(preScriptResult.consoleOutput);
266
- // Emit event after collection pre-script completion
267
- const afterEnvelope = this.createEventEnvelope(collection.info, 'collection:/', tempContext);
268
- this.emit('afterCollectionPreScript', {
269
- ...afterEnvelope,
270
- path: 'collection:/',
271
- result: preScriptResult
272
- });
273
- if (preScriptResult.success === false) {
274
- throw new Error(`Collection pre-script error: ${preScriptResult.error}`);
275
- }
276
- // Update context variables for iterations
277
- options.globalVariables = tempContext.globalVariables;
278
- options.environment = tempContext.environment;
279
- // Update collection scope with any changes from script
280
- Object.assign(scopeStack[0].vars, tempContext.scopeStack[0].vars);
281
- }
282
- for (let i = 0; i < iterationCount; i++) {
283
- const iterationStart = Date.now();
284
- const context = {
285
- collectionInfo: collection.info,
286
- protocol: collection.protocol,
287
- collectionVariables: collection.variables ?? {},
288
- globalVariables: options.globalVariables ?? {},
289
- scopeStack: [...scopeStack], // Clone scope stack for each iteration
290
- environment: options.environment,
291
- iterationCurrent: i + 1,
292
- iterationCount,
293
- iterationData,
294
- currentIterationData: iterationData[i],
295
- iterationSource,
296
- executionHistory: [],
297
- options: this.mergeOptions(collection.options, options),
298
- cookieJar,
299
- eventEmitter: this,
300
- protocolPlugin
301
- };
302
- // Emit beforeIteration event
303
- const iterationEnvelope = this.createEventEnvelope(collection.info, 'collection:/', context);
304
- this.emit('beforeIteration', {
305
- ...iterationEnvelope,
306
- iteration: iterationEnvelope.iteration
307
- });
308
- await this.executeTree(tree, context, requestResults);
309
- // Emit afterIteration event
310
- const iterationDuration = Date.now() - iterationStart;
311
- const afterIterationEnvelope = this.createEventEnvelope(collection.info, 'collection:/', context);
312
- this.emit('afterIteration', {
313
- ...afterIterationEnvelope,
314
- iteration: afterIterationEnvelope.iteration,
315
- duration: iterationDuration
316
- });
317
- // Update collection scope with changes from iteration
318
- Object.assign(scopeStack[0].vars, context.scopeStack[0].vars);
319
- // Update global variables and environment for next iteration
320
- options.globalVariables = context.globalVariables;
321
- options.environment = context.environment;
322
- }
323
- // Execute collection post-request script once after all iterations
324
- if (!isNullOrWhitespace(collection.collectionPostScript)) {
325
- // Emit event before collection post-script execution
326
- const beforePostEnvelope = this.createEventEnvelope(collection.info, 'collection:/', undefined);
327
- this.emit('beforeCollectionPostScript', {
328
- ...beforePostEnvelope,
329
- path: 'collection:/'
330
- });
331
- const tempContext = {
332
- collectionInfo: collection.info,
333
- protocol: collection.protocol,
334
- collectionVariables: collection.variables ?? {},
335
- globalVariables: options.globalVariables ?? {},
336
- scopeStack: [...scopeStack], // Clone scope stack
337
- environment: options.environment,
338
- iterationCurrent: iterationCount,
339
- iterationCount,
340
- iterationData,
341
- currentIterationData: iterationData[iterationCount - 1],
342
- iterationSource,
343
- executionHistory: [],
344
- options: this.mergeOptions(collection.options, options),
345
- cookieJar,
346
- eventEmitter: this,
347
- protocolPlugin
348
- };
349
- const postScriptResult = await this.queueScript(() => this.scriptEngine.execute(collection.collectionPostScript, tempContext, ScriptType.CollectionPost, () => { } // noop - collection post-scripts cannot have tests
350
- ));
351
- this.emitConsoleOutput(postScriptResult.consoleOutput);
352
- // Emit event for collection post-script completion
353
- const afterPostEnvelope = this.createEventEnvelope(collection.info, 'collection:/', tempContext);
354
- this.emit('afterCollectionPostScript', {
355
- ...afterPostEnvelope,
356
- path: 'collection:/',
357
- result: postScriptResult
358
- });
359
- if (postScriptResult.success === false) {
360
- throw new Error(`Collection post-script error: ${postScriptResult.error}`);
361
- }
362
- }
363
- const endTime = new Date();
364
- const duration = endTime.getTime() - startTime.getTime();
365
- const totalTests = requestResults.reduce((sum, r) => sum + r.tests.length, 0);
366
- const passedTests = requestResults.reduce((sum, r) => sum + r.tests.filter(t => t.passed && !t.skipped).length, 0);
367
- const failedTests = requestResults.reduce((sum, r) => sum + r.tests.filter(t => !t.passed && !t.skipped).length, 0);
368
- const skippedTests = requestResults.reduce((sum, r) => sum + r.tests.filter(t => t.skipped).length, 0);
369
- const result = {
370
- collectionId: collection.info.id,
371
- collectionName: collection.info.name,
372
- startTime,
373
- endTime,
374
- duration,
375
- requestResults,
376
- totalTests,
377
- passedTests,
378
- failedTests,
379
- skippedTests
380
- };
381
- this.emit('afterRun', {
382
- collectionInfo: collection.info,
383
- result
384
- });
385
- return result;
386
- }
387
- async executeTree(node, context, results) {
388
- if (node.type === 'request') {
389
- const result = await this.executeRequest(node, context);
390
- results.push(result);
391
- // If there was a script error, stop execution immediately
392
- if (!isNullOrEmpty(result.scriptError)) {
393
- throw new Error(`${result.scriptError}`);
394
- }
395
- return;
396
- }
397
- // Handle folder node - execute folder scripts
398
- if (node.type === 'folder') {
399
- const folder = node.item;
400
- const folderStart = Date.now();
401
- // Emit beforeFolder event
402
- const beforeFolderEnvelope = this.createEventEnvelope(context.collectionInfo, node.path, context);
403
- this.emit('beforeFolder', beforeFolderEnvelope);
404
- // PUSH folder scope
405
- context.scopeStack.push({
406
- level: 'folder',
407
- id: folder.id,
408
- vars: {}
409
- });
410
- // Execute folder pre-script
411
- if (!isNullOrWhitespace(folder.folderPreScript)) {
412
- // Emit event before folder pre-script execution
413
- const beforePreEnvelope = this.createEventEnvelope(context.collectionInfo, node.path, context);
414
- this.emit('beforeFolderPreScript', beforePreEnvelope);
415
- const preScriptResult = await this.queueScript(() => this.scriptEngine.execute(folder.folderPreScript, context, ScriptType.FolderPre, () => { } // noop - folder pre-scripts cannot have tests
416
- ));
417
- this.emitConsoleOutput(preScriptResult.consoleOutput);
418
- // Emit event after folder pre-script completion
419
- const afterPreEnvelope = this.createEventEnvelope(context.collectionInfo, node.path, context);
420
- this.emit('afterFolderPreScript', {
421
- ...afterPreEnvelope,
422
- result: preScriptResult
423
- });
424
- if (preScriptResult.success === false) {
425
- throw new Error(`Folder pre-script error (${folder.name}): ${preScriptResult.error}`);
426
- }
427
- }
428
- // Execute children
429
- for (const child of node.children) {
430
- await this.executeTree(child, context, results);
431
- }
432
- // Execute folder post-script
433
- if (!isNullOrWhitespace(folder.folderPostScript)) {
434
- // Emit event before folder post-script execution
435
- const beforePostEnvelope = this.createEventEnvelope(context.collectionInfo, node.path, context);
436
- this.emit('beforeFolderPostScript', beforePostEnvelope);
437
- const postScriptResult = await this.queueScript(() => this.scriptEngine.execute(folder.folderPostScript, context, ScriptType.FolderPost, () => { } // noop - folder post-scripts cannot have tests
438
- ));
439
- this.emitConsoleOutput(postScriptResult.consoleOutput);
440
- // Emit event for folder post-script completion
441
- const afterPostEnvelope = this.createEventEnvelope(context.collectionInfo, node.path, context);
442
- this.emit('afterFolderPostScript', {
443
- ...afterPostEnvelope,
444
- result: postScriptResult
445
- });
446
- if (postScriptResult.success === false) {
447
- throw new Error(`Folder post-script error (${folder.name}): ${postScriptResult.error}`);
448
- }
449
- }
450
- // POP folder scope (folder variables don't persist)
451
- context.scopeStack.pop();
452
- // Emit afterFolder event
453
- const folderDuration = Date.now() - folderStart;
454
- const afterFolderEnvelope = this.createEventEnvelope(context.collectionInfo, node.path, context);
455
- this.emit('afterFolder', {
456
- ...afterFolderEnvelope,
457
- duration: folderDuration
458
- });
459
- return;
460
- }
461
- // Default: execute children
462
- for (const child of node.children) {
463
- await this.executeTree(child, context, results);
464
- }
465
- }
466
- async executeRequest(node, context) {
467
- const request = node.item;
468
- const requestStart = Date.now();
469
- context.currentRequest = request;
470
- context.currentPath = node.path;
471
- // Emit beforeItem event
472
- const beforeItemEnvelope = this.createEventEnvelope(context.collectionInfo, node.path, context, request);
473
- this.emit('beforeItem', {
474
- ...beforeItemEnvelope,
475
- request,
476
- path: node.path
477
- });
478
- // PUSH request scope
479
- context.scopeStack.push({
480
- level: 'request',
481
- id: request.id,
482
- vars: {}
483
- });
484
- try {
485
- // Queue all pre-request scripts
486
- const preScripts = [
487
- ...(node.inheritedScripts.preRequest ?? []).map(script => ({
488
- script,
489
- source: 'inherited'
490
- })),
491
- ...(!isNullOrWhitespace(request.preRequestScript)
492
- ? [{ script: request.preRequestScript, source: 'request' }]
493
- : [])
494
- ];
495
- if (preScripts.length > 0) {
496
- await this.queueScript(async () => {
497
- for (const { script, source } of preScripts) {
498
- // Emit beforePreScript event
499
- const beforePreEnvelope = this.createEventEnvelope(context.collectionInfo, node.path, context, request);
500
- this.emit('beforePreScript', {
501
- ...beforePreEnvelope,
502
- request,
503
- path: node.path
504
- });
505
- const preScriptResult = await this.scriptEngine.execute(script, context, ScriptType.PreRequest, () => { } // noop - pre-request scripts cannot have tests
506
- );
507
- this.emitConsoleOutput(preScriptResult.consoleOutput);
508
- // Emit afterPreScript event
509
- const afterPreEnvelope = this.createEventEnvelope(context.collectionInfo, node.path, context, request);
510
- this.emit('afterPreScript', {
511
- ...afterPreEnvelope,
512
- request,
513
- path: node.path,
514
- result: preScriptResult
515
- });
516
- if (preScriptResult.success === false) {
517
- const error = new Error(`${source === 'inherited' ? 'Inherited pre-request' : 'Pre-request'} script error: ${preScriptResult.error}`);
518
- error.phase = 'prerequest';
519
- throw error;
520
- }
521
- }
522
- });
523
- }
524
- // HTTP execution NOT queued - runs in parallel
525
- const resolvedRequest = this.resolveRequest(request, context);
526
- let resolvedAuth = node.effectiveAuth;
527
- if (resolvedAuth?.data !== undefined) {
528
- resolvedAuth = {
529
- ...resolvedAuth,
530
- data: this.variableResolver.resolveAll(resolvedAuth.data, context)
531
- };
532
- }
533
- const requestWithAuth = { ...resolvedRequest, auth: resolvedAuth };
534
- // Track event indices and tests for plugin events (per request)
535
- const eventIndices = new Map();
536
- const pluginEventTests = []; // Collect tests from plugin events
537
- // Create emitEvent callback for plugin event execution
538
- const emitEvent = async (eventName, eventData) => {
539
- this.logger.trace(`Plugin event emitted: ${eventName}`, { hasScripts: request.data.scripts !== undefined });
540
- // Find matching script (validation ensures at most one per event type)
541
- const eventScript = request.data.scripts?.find(s => s.event === eventName);
542
- this.logger.trace(`Event script found: ${eventScript !== undefined}`);
543
- if (eventScript === undefined)
544
- return;
545
- // Get current event index (starts at 0)
546
- const currentIndex = eventIndices.get(eventName) ?? 0;
547
- // Set event context (wrapped in try/finally to prevent state leak)
548
- try {
549
- context.currentEvent = {
550
- eventName: eventName,
551
- requestId: request.id,
552
- timestamp: new Date(),
553
- data: eventData,
554
- index: currentIndex
555
- };
556
- // Execute plugin event script
557
- const result = await this.scriptEngine.execute(eventScript.script, context, ScriptType.PluginEvent, (test) => {
558
- // Emit assertion event for plugin event test
559
- const eventDef = context.protocolPlugin.events?.find(e => e.name === eventName);
560
- if (eventDef?.canHaveTests === true) {
561
- const envelope = this.createEventEnvelope(context.collectionInfo, node.path, context, request);
562
- this.emit('assertion', {
563
- ...envelope,
564
- test,
565
- event: eventName
566
- });
567
- }
568
- });
569
- // Handle test results - add directly to collection
570
- if (result.tests !== undefined && result.tests.length > 0) {
571
- this.logger.trace(`Plugin event tests collected: ${result.tests.length}`);
572
- pluginEventTests.push(...result.tests);
573
- }
574
- else {
575
- this.logger.trace('No tests in plugin event result');
576
- }
577
- // Handle script errors (log but don't throw - allow other events to continue)
578
- if (!isNullOrEmpty(result.error)) {
579
- this.logger.error(`Plugin event script error (${eventName}):`, result.error);
580
- }
581
- }
582
- finally {
583
- // CRITICAL: Always reset event context to prevent state leak
584
- context.currentEvent = undefined;
585
- // Increment event index for next event of same type
586
- eventIndices.set(eventName, currentIndex + 1);
587
- }
588
- };
589
- // Emit beforeRequest event
590
- const beforeRequestEnvelope = this.createEventEnvelope(context.collectionInfo, node.path, context, request);
591
- this.emit('beforeRequest', {
592
- ...beforeRequestEnvelope,
593
- request,
594
- path: node.path
595
- });
596
- const response = await this.pluginManager.execute(context.protocol, requestWithAuth, context, context.options, emitEvent // Pass event emitter to plugin
597
- );
598
- context.currentResponse = response;
599
- // Emit afterRequest event (after HTTP execution, before post-scripts)
600
- const requestDuration = Date.now() - requestStart;
601
- const afterRequestEnvelope = this.createEventEnvelope(context.collectionInfo, node.path, context, request);
602
- this.emit('afterRequest', {
603
- ...afterRequestEnvelope,
604
- request,
605
- response,
606
- duration: requestDuration
607
- });
608
- // Add preliminary execution record to history
609
- this.logger.trace(`Creating execution record with ${pluginEventTests.length} plugin event tests`);
610
- const executionRecord = {
611
- id: request.id,
612
- name: request.name,
613
- path: node.path,
614
- iteration: context.iterationCurrent,
615
- response,
616
- tests: [...pluginEventTests], // Add plugin event tests first
617
- timestamp: new Date().toISOString()
618
- };
619
- this.logger.trace(`Execution record has ${executionRecord.tests.length} total tests`);
620
- context.executionHistory.push(executionRecord);
621
- // Queue all post-request scripts
622
- const postScripts = [
623
- ...(!isNullOrWhitespace(request.postRequestScript)
624
- ? [{ script: request.postRequestScript, source: 'request' }]
625
- : []),
626
- ...(node.inheritedScripts.postRequest ?? []).map(script => ({
627
- script,
628
- source: 'inherited'
629
- }))
630
- ];
631
- const scriptResult = postScripts.length > 0
632
- ? await this.queueScript(async () => {
633
- const combinedResult = { success: true, tests: [], consoleOutput: [] };
634
- for (const { script, source } of postScripts) {
635
- // Emit beforePostScript event
636
- const beforePostEnvelope = this.createEventEnvelope(context.collectionInfo, node.path, context, request);
637
- this.emit('beforePostScript', {
638
- ...beforePostEnvelope,
639
- request,
640
- path: node.path,
641
- response
642
- });
643
- const postScriptResult = await this.scriptEngine.execute(script, context, ScriptType.PostRequest, (test) => {
644
- // Emit assertion event for post-request test
645
- const envelope = this.createEventEnvelope(context.collectionInfo, node.path, context, request);
646
- this.emit('assertion', {
647
- ...envelope,
648
- test,
649
- response
650
- });
651
- });
652
- this.emitConsoleOutput(postScriptResult.consoleOutput);
653
- // Emit afterPostScript event
654
- const afterPostEnvelope = this.createEventEnvelope(context.collectionInfo, node.path, context, request);
655
- this.emit('afterPostScript', {
656
- ...afterPostEnvelope,
657
- request,
658
- path: node.path,
659
- response,
660
- result: postScriptResult
661
- });
662
- if (postScriptResult.success === false) {
663
- const error = new Error(`${source === 'inherited' ? 'Inherited post-request' : 'Post-request'} script error: ${postScriptResult.error}`);
664
- error.phase = 'postrequest';
665
- throw error;
666
- }
667
- combinedResult.tests.push(...postScriptResult.tests);
668
- combinedResult.consoleOutput.push(...postScriptResult.consoleOutput);
669
- }
670
- return combinedResult;
671
- })
672
- : { success: true, tests: [], consoleOutput: [] };
673
- // Update the execution record with all tests (plugin events + post-request scripts)
674
- const allTests = [...pluginEventTests, ...scriptResult.tests];
675
- executionRecord.tests = allTests;
676
- const duration = Date.now() - requestStart;
677
- const result = {
678
- requestId: request.id,
679
- requestName: request.name,
680
- path: node.path,
681
- success: isNullOrEmpty(response.error),
682
- response,
683
- tests: allTests,
684
- duration,
685
- iteration: context.iterationCurrent
686
- };
687
- // Emit afterItem event
688
- const afterItemEnvelope = this.createEventEnvelope(context.collectionInfo, node.path, context, request);
689
- this.emit('afterItem', {
690
- ...afterItemEnvelope,
691
- request,
692
- path: node.path,
693
- response,
694
- result
695
- });
696
- // Clear cookies if persist is false (default behavior)
697
- if (context.options.jar?.persist !== true) {
698
- context.cookieJar.clear();
699
- }
700
- // POP request scope
701
- context.scopeStack.pop();
702
- return result;
703
- }
704
- catch (error) {
705
- const duration = Date.now() - requestStart;
706
- const err = error;
707
- const result = {
708
- requestId: request.id,
709
- requestName: request.name,
710
- path: node.path,
711
- success: false,
712
- tests: [],
713
- duration,
714
- scriptError: err.message ?? String(error),
715
- iteration: context.iterationCurrent
716
- };
717
- const phase = err.phase ?? 'request';
718
- this.emit('exception', {
719
- error,
720
- phase,
721
- request,
722
- path: node.path,
723
- response: context.currentResponse
724
- });
725
- // POP request scope even on error
726
- context.scopeStack.pop();
727
- return result;
728
- }
729
- }
730
- resolveRequest(request, context) {
731
- const resolved = {
732
- ...request,
733
- data: this.variableResolver.resolveAll(request.data, context)
734
- };
735
- // Also resolve variables in auth configuration
736
- if (resolved.auth?.data !== undefined) {
737
- resolved.auth = {
738
- ...resolved.auth,
739
- data: this.variableResolver.resolveAll(resolved.auth.data, context)
740
- };
741
- }
742
- return resolved;
743
- }
744
- mergeOptions(collectionOptions, runOptions) {
745
- // Since RunOptions extends RuntimeOptions, merge is straightforward
746
- // RunOptions takes precedence over collectionOptions
747
- const merged = {
748
- ...collectionOptions,
749
- ...runOptions,
750
- // Deep merge nested objects
751
- execution: {
752
- ...(collectionOptions?.execution ?? {}),
753
- ...(runOptions?.execution ?? {})
754
- },
755
- // Ensure defaults
756
- logLevel: runOptions?.logLevel ?? collectionOptions?.logLevel ?? LogLevel.INFO,
757
- strictMode: runOptions?.strictMode ?? collectionOptions?.strictMode ?? true
758
- };
759
- // Conditionally merge optional nested objects
760
- if (collectionOptions?.timeout !== undefined || runOptions?.timeout !== undefined) {
761
- merged.timeout = {
762
- ...(collectionOptions?.timeout ?? {}),
763
- ...(runOptions?.timeout ?? {})
764
- };
765
- }
766
- if (collectionOptions?.ssl !== undefined || runOptions?.ssl !== undefined) {
767
- merged.ssl = {
768
- ...(collectionOptions?.ssl ?? {}),
769
- ...(runOptions?.ssl ?? {})
770
- };
771
- }
772
- if (collectionOptions?.jar !== undefined || runOptions?.jar !== undefined) {
773
- merged.jar = {
774
- persist: runOptions?.jar?.persist ?? collectionOptions?.jar?.persist ?? false
775
- };
776
- }
777
- if (runOptions?.proxy !== null && runOptions?.proxy !== undefined) {
778
- merged.proxy = runOptions.proxy;
779
- }
780
- else if (collectionOptions?.proxy !== null && collectionOptions?.proxy !== undefined) {
781
- merged.proxy = collectionOptions.proxy;
782
- }
783
- // Merge cookies arrays (runOptions cookies + collectionOptions cookies)
784
- const collectionCookies = collectionOptions?.cookies ?? [];
785
- const runCookies = runOptions?.cookies ?? [];
786
- if (collectionCookies.length > 0 || runCookies.length > 0) {
787
- // RunOptions cookies override collection cookies with same name
788
- const cookieMap = new Map();
789
- // Add collection cookies first
790
- for (const cookie of collectionCookies) {
791
- cookieMap.set(cookie.name, cookie);
792
- }
793
- // Then add/override with run cookies
794
- for (const cookie of runCookies) {
795
- cookieMap.set(cookie.name, cookie);
796
- }
797
- merged.cookies = Array.from(cookieMap.values());
798
- }
799
- return merged;
800
- }
801
- }
802
- //# sourceMappingURL=CollectionRunner.js.map