@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.
Files changed (87) hide show
  1. package/README.md +90 -2
  2. package/dist/CollectionRunner.d.ts +3 -0
  3. package/dist/CollectionRunner.d.ts.map +1 -1
  4. package/dist/CollectionRunner.js +249 -154
  5. package/dist/CollectionRunner.js.map +1 -1
  6. package/dist/CollectionValidator.d.ts.map +1 -1
  7. package/dist/CollectionValidator.js +11 -0
  8. package/dist/CollectionValidator.js.map +1 -1
  9. package/dist/ConsoleReporter.d.ts.map +1 -1
  10. package/dist/ConsoleReporter.js +9 -6
  11. package/dist/ConsoleReporter.js.map +1 -1
  12. package/dist/DagScheduler.d.ts.map +1 -1
  13. package/dist/DagScheduler.js +11 -0
  14. package/dist/DagScheduler.js.map +1 -1
  15. package/dist/LibraryLoader.d.ts +49 -0
  16. package/dist/LibraryLoader.d.ts.map +1 -0
  17. package/dist/LibraryLoader.js +198 -0
  18. package/dist/LibraryLoader.js.map +1 -0
  19. package/dist/PluginLoader.d.ts.map +1 -1
  20. package/dist/PluginLoader.js +9 -6
  21. package/dist/PluginLoader.js.map +1 -1
  22. package/dist/PluginManager.d.ts.map +1 -1
  23. package/dist/PluginManager.js +11 -7
  24. package/dist/PluginManager.js.map +1 -1
  25. package/dist/PluginResolver.d.ts +1 -1
  26. package/dist/PluginResolver.d.ts.map +1 -1
  27. package/dist/PluginResolver.js +1 -1
  28. package/dist/PluginResolver.js.map +1 -1
  29. package/dist/QuestAPI.d.ts.map +1 -1
  30. package/dist/QuestAPI.js +114 -217
  31. package/dist/QuestAPI.js.map +1 -1
  32. package/dist/ScriptEngine.d.ts +2 -1
  33. package/dist/ScriptEngine.d.ts.map +1 -1
  34. package/dist/ScriptEngine.js +15 -8
  35. package/dist/ScriptEngine.js.map +1 -1
  36. package/dist/TaskGraph.d.ts +2 -1
  37. package/dist/TaskGraph.d.ts.map +1 -1
  38. package/dist/TaskGraph.js +28 -26
  39. package/dist/TaskGraph.js.map +1 -1
  40. package/dist/VariableResolver.d.ts +1 -1
  41. package/dist/VariableResolver.js +10 -10
  42. package/dist/VariableResolver.js.map +1 -1
  43. package/dist/cli/index.js +35 -3
  44. package/dist/cli/index.js.map +1 -1
  45. package/dist/cli/plugin-commands.d.ts.map +1 -1
  46. package/dist/cli/plugin-commands.js +47 -81
  47. package/dist/cli/plugin-commands.js.map +1 -1
  48. package/dist/cli/plugin-installer.d.ts +48 -0
  49. package/dist/cli/plugin-installer.d.ts.map +1 -0
  50. package/dist/cli/plugin-installer.js +136 -0
  51. package/dist/cli/plugin-installer.js.map +1 -0
  52. package/dist/cli/plugin-registry.d.ts +17 -0
  53. package/dist/cli/plugin-registry.d.ts.map +1 -0
  54. package/dist/cli/plugin-registry.js +77 -0
  55. package/dist/cli/plugin-registry.js.map +1 -0
  56. package/package.json +1 -1
  57. package/tsconfig.json +1 -0
  58. package/tsconfig.test.json +3 -0
  59. package/dist/QuestAPI.types.d.ts +0 -35
  60. package/dist/QuestAPI.types.d.ts.map +0 -1
  61. package/dist/QuestAPI.types.js +0 -3
  62. package/dist/QuestAPI.types.js.map +0 -1
  63. package/src/CollectionAnalyzer.ts +0 -102
  64. package/src/CollectionRunner.ts +0 -1423
  65. package/src/CollectionRunner.types.ts +0 -9
  66. package/src/CollectionValidator.ts +0 -289
  67. package/src/ConsoleReporter.ts +0 -143
  68. package/src/CookieJar.ts +0 -258
  69. package/src/DagScheduler.ts +0 -439
  70. package/src/Logger.ts +0 -85
  71. package/src/PluginLoader.ts +0 -126
  72. package/src/PluginManager.ts +0 -208
  73. package/src/PluginResolver.ts +0 -154
  74. package/src/QuestAPI.ts +0 -764
  75. package/src/QuestAPI.types.ts +0 -33
  76. package/src/QuestTestAPI.ts +0 -164
  77. package/src/RequestFilter.ts +0 -224
  78. package/src/ScriptEngine.ts +0 -219
  79. package/src/ScriptValidator.ts +0 -428
  80. package/src/TaskGraph.ts +0 -598
  81. package/src/TestCounter.ts +0 -109
  82. package/src/VariableResolver.ts +0 -114
  83. package/src/cli/index.ts +0 -480
  84. package/src/cli/plugin-commands.ts +0 -342
  85. package/src/cli/plugin-discovery.ts +0 -44
  86. package/src/index.ts +0 -24
  87. package/src/utils.ts +0 -52
package/src/TaskGraph.ts DELETED
@@ -1,598 +0,0 @@
1
- /**
2
- * TaskGraph - DAG Builder for Parallel Execution
3
- *
4
- * Transforms a collection tree into a directed acyclic graph (DAG) where:
5
- * - Nodes represent atomic tasks (script execution or request I/O)
6
- * - Edges represent ordering dependencies (structural + explicit dependsOn)
7
- *
8
- * Design:
9
- * - Script nodes: Serial execution through script queue (collection-pre/post, folder-pre/post, plugin events)
10
- * - Request nodes: Parallel execution via request pool
11
- * - Inherited pre/post scripts execute INSIDE request through script queue
12
- * - These are stored in node metadata, not as separate DAG nodes
13
- *
14
- * Edge types:
15
- * - Structural: Parent-child hierarchy (folder-pre → children → folder-post)
16
- * - DependsOn: Explicit dependencies from request/folder dependsOn fields
17
- * - Event: Request → plugin event scripts → parent folder-post
18
- */
19
-
20
- import type {
21
- Collection,
22
- CollectionItem,
23
- Folder,
24
- Request,
25
- ProtocolScript,
26
- ScriptType,
27
- PathType,
28
- Auth
29
- } from '@apiquest/types';
30
- import { isNullOrWhitespace } from './utils.js';
31
- import { Logger } from './Logger.js';
32
-
33
- /**
34
- * TaskNode represents a node in the DAG
35
- * Can be either a script node or a request node
36
- */
37
- export interface TaskNode {
38
- // Identity
39
- id: string;
40
- name: string;
41
- type: 'script' | 'request' | 'folder-enter' | 'folder-exit';
42
- path: PathType;
43
- parentFolderId?: PathType;
44
-
45
- // Script node fields
46
- scriptType?: ScriptType;
47
- script?: string;
48
-
49
- // Request/Folder condition
50
- condition?: string;
51
-
52
- // Request node: inherited scripts (executed inside request through script queue)
53
- // Array maintains order: outermost to innermost
54
- inheritedPreScripts?: string[]; // Collection → Folders → Request
55
- inheritedPostScripts?: string[]; // Request → Folders → Collection (LIFO)
56
-
57
- // Auth inheritance: Request > Folder > Parent Folder > Collection
58
- effectiveAuth?: Auth;
59
-
60
- // Original item (for request/folder execution)
61
- item?: Request | Folder;
62
-
63
- // Plugin event metadata (for script nodes of type plugin-event)
64
- eventName?: string;
65
- parentRequestId?: string;
66
- }
67
-
68
- /**
69
- * TaskEdge represents a dependency between two nodes
70
- */
71
- export interface TaskEdge {
72
- from: string;
73
- to: string;
74
- type: 'structural' | 'dependsOn' | 'event';
75
- }
76
-
77
- /**
78
- * Builds DAG from collection structure
79
- */
80
- export class TaskGraph {
81
- private nodes: Map<string, TaskNode> = new Map();
82
- private edges: TaskEdge[] = [];
83
- private dependentsMap: Map<string, string[]> = new Map();
84
- private inDegreeMap: Map<string, number> = new Map();
85
- private parentByNodeId: Map<string, PathType> = new Map();
86
- private childrenByFolderId: Map<PathType, string[]> = new Map();
87
- private logger: Logger;
88
- private allowParallel: boolean = false; // Default to sequential
89
-
90
- // Mappings for dependsOn resolution
91
- private startNodeByItemId: Map<string, string> = new Map();
92
- private completionNodeByItemId: Map<string, string> = new Map();
93
- private pendingDependsOn: Array<{ depId: string; targetItemId: string }> = [];
94
-
95
- constructor(baseLogger?: Logger) {
96
- this.logger = baseLogger?.createLogger('TaskGraph') ?? new Logger('TaskGraph');
97
- }
98
-
99
- /**
100
- * Build DAG from collection
101
- * @param collection - The collection to build
102
- * @param allowParallel - Whether parallel execution is enabled (affects DAG structure)
103
- */
104
- public build(collection: Collection, allowParallel: boolean = false): void {
105
- this.allowParallel = allowParallel;
106
- this.logger.debug(`Building DAG: allowParallel=${allowParallel}, collection="${collection.info.name}"`);
107
-
108
- const collectionPreId = 'script:collection-pre';
109
- const collectionPostId = 'script:collection-post';
110
-
111
- // Add collection-level script nodes
112
- this.addNode({
113
- id: collectionPreId,
114
- name: 'collection-pre',
115
- type: 'script',
116
- scriptType: 'collection-pre' as ScriptType,
117
- script: collection.collectionPreScript,
118
- path: 'collection:/'
119
- });
120
-
121
- this.addNode({
122
- id: collectionPostId,
123
- name: 'collection-post',
124
- type: 'script',
125
- scriptType: 'collection-post' as ScriptType,
126
- script: collection.collectionPostScript,
127
- path: 'collection:/'
128
- });
129
-
130
- // Build inherited script arrays
131
- const collectionPreScripts = this.toArray(collection.preRequestScript);
132
- const collectionPostScripts = this.toArray(collection.postRequestScript);
133
-
134
- // Start with collection-level auth (if present)
135
- const collectionAuth = collection.auth;
136
-
137
- // Build child nodes
138
- // In sequential mode: add explicit edges to enforce declaration order
139
- // In parallel mode: siblings can execute concurrently (no sequential edges)
140
- let previousCompletionId: string = collectionPreId;
141
-
142
- for (const item of collection.items) {
143
- this.logger.trace(`Building item: ${item.type}:${item.name}, previousCompletionId=${previousCompletionId}`);
144
- const { startId, endId } = this.buildItem(
145
- item,
146
- 'collection:/',
147
- previousCompletionId, // Sequential: this item waits for previous
148
- collectionPostId,
149
- collectionPreScripts,
150
- collectionPostScripts,
151
- collectionAuth
152
- );
153
-
154
- this.logger.trace(`Built item: startId=${startId}, endId=${endId}`);
155
-
156
- // Update previousCompletionId for next sibling
157
- // Sequential mode: endId (strict ordering)
158
- // Parallel mode: collectionPreId (all siblings start after collection-pre)
159
- previousCompletionId = this.allowParallel ? collectionPreId : endId;
160
- }
161
-
162
- // Final barrier: last item → collection-post
163
- if (!this.allowParallel) {
164
- this.logger.trace(`Adding final sequential barrier: ${previousCompletionId} → ${collectionPostId}`);
165
- this.addEdge(previousCompletionId, collectionPostId, 'structural');
166
- }
167
-
168
- this.logger.debug(`DAG built: ${this.nodes.size} nodes, ${this.edges.length} edges`);
169
-
170
- // Resolve dependsOn edges
171
- this.resolveDependsOnEdges();
172
- }
173
-
174
- /**
175
- * Get all nodes
176
- */
177
- public getNodes(): Map<string, TaskNode> {
178
- return this.nodes;
179
- }
180
-
181
- /**
182
- * Get all edges
183
- */
184
- public getEdges(): TaskEdge[] {
185
- return this.edges;
186
- }
187
-
188
- public getChildrenByFolderId(): Map<PathType, string[]> {
189
- return this.childrenByFolderId;
190
- }
191
-
192
- public getParentByNodeId(): Map<string, PathType> {
193
- return this.parentByNodeId;
194
- }
195
-
196
- /**
197
- * Get dependents for a node
198
- */
199
- public getDependents(nodeId: string): string[] {
200
- return this.dependentsMap.get(nodeId) ?? [];
201
- }
202
-
203
- /**
204
- * Get in-degree for a node
205
- */
206
- public getInDegree(nodeId: string): number {
207
- return this.inDegreeMap.get(nodeId) ?? 0;
208
- }
209
-
210
- /**
211
- * Get nodes with zero in-degree (ready for execution)
212
- */
213
- public getReadyNodes(): TaskNode[] {
214
- const ready: TaskNode[] = [];
215
- for (const [nodeId, degree] of this.inDegreeMap) {
216
- if (degree === 0) {
217
- const node = this.nodes.get(nodeId);
218
- if (node !== undefined) {
219
- ready.push(node);
220
- }
221
- }
222
- }
223
-
224
- this.logger.trace(`getReadyNodes() found ${ready.length} nodes: ${ready.map(n => `${n.type}:${n.name ?? n.id}`).join(', ')}`);
225
-
226
- return ready;
227
- }
228
-
229
- /**
230
- * Mark node as complete and return newly ready nodes
231
- */
232
- public completeNode(nodeId: string): TaskNode[] {
233
- const nowReady: TaskNode[] = [];
234
- const deps = this.getDependents(nodeId);
235
-
236
- for (const depId of deps) {
237
- const currentDegree = this.inDegreeMap.get(depId) ?? 0;
238
- const newDegree = currentDegree - 1;
239
- this.inDegreeMap.set(depId, newDegree);
240
-
241
- if (newDegree === 0) {
242
- const node = this.nodes.get(depId);
243
- if (node !== undefined) {
244
- nowReady.push(node);
245
- }
246
- }
247
- }
248
-
249
- this.logger.trace(`completeNode(${nodeId}) made ${nowReady.length} nodes ready: ${nowReady.map(n => `${n.type}:${n.name ?? n.id}`).join(', ')}`);
250
-
251
- return nowReady;
252
- }
253
-
254
- private addNode(node: TaskNode): void {
255
- this.nodes.set(node.id, node);
256
- if (node.parentFolderId !== undefined) {
257
- this.parentByNodeId.set(node.id, node.parentFolderId);
258
- const children = this.childrenByFolderId.get(node.parentFolderId) ?? [];
259
- children.push(node.id);
260
- this.childrenByFolderId.set(node.parentFolderId, children);
261
- }
262
- if (!this.dependentsMap.has(node.id)) {
263
- this.dependentsMap.set(node.id, []);
264
- }
265
- if (!this.inDegreeMap.has(node.id)) {
266
- this.inDegreeMap.set(node.id, 0);
267
- }
268
- }
269
-
270
- private addEdge(from: string, to: string, type: 'structural' | 'dependsOn' | 'event'): void {
271
- this.edges.push({ from, to, type });
272
-
273
- // Update dependents
274
- const deps = this.dependentsMap.get(from) ?? [];
275
- deps.push(to);
276
- this.dependentsMap.set(from, deps);
277
-
278
- // Increment in-degree
279
- const degree = this.inDegreeMap.get(to) ?? 0;
280
- this.inDegreeMap.set(to, degree + 1);
281
- }
282
-
283
- private buildItem(
284
- item: CollectionItem,
285
- parentPath: PathType,
286
- parentPreId: string,
287
- parentPostId: string,
288
- inheritedPreScripts: string[],
289
- inheritedPostScripts: string[],
290
- parentAuth?: Auth
291
- ): { startId: string; endId: string } {
292
- if (item.type === 'folder') {
293
- return this.buildFolder(
294
- item,
295
- parentPath,
296
- parentPreId,
297
- parentPostId,
298
- inheritedPreScripts,
299
- inheritedPostScripts,
300
- parentAuth
301
- );
302
- } else {
303
- return this.buildRequest(
304
- item,
305
- parentPath,
306
- parentPreId,
307
- parentPostId,
308
- inheritedPreScripts,
309
- inheritedPostScripts,
310
- parentAuth
311
- );
312
- }
313
- }
314
-
315
- private buildFolder(
316
- folder: Folder,
317
- parentPath: PathType,
318
- parentPreId: string,
319
- parentPostId: string,
320
- inheritedPreScripts: string[],
321
- inheritedPostScripts: string[],
322
- parentAuth?: Auth
323
- ): { startId: string; endId: string } {
324
- const folderPath = this.buildPath(parentPath, folder.name, 'folder');
325
- const folderEnterId = `folder-enter:${folderPath}`;
326
- const folderPreId = `script:folder-pre:${folderPath}`;
327
- const folderPostId = `script:folder-post:${folderPath}`;
328
- const folderExitId = `folder-exit:${folderPath}`;
329
-
330
- this.logger.trace(`buildFolder: ${folder.name} (enter=${folderEnterId}, exit=${folderExitId})`);
331
-
332
- // Add folder-enter node (lifecycle: PUSH scope + beforeFolder event)
333
- // ALWAYS executes regardless of script existence
334
- this.addNode({
335
- id: folderEnterId,
336
- name: `${folder.name}-enter`,
337
- type: 'folder-enter',
338
- parentFolderId: parentPath.startsWith('folder:/') ? parentPath : undefined,
339
- condition: folder.condition,
340
- path: folderPath,
341
- item: folder
342
- });
343
-
344
- // Add folder-exit node (lifecycle: POP scope + afterFolder event)
345
- // ALWAYS executes regardless of script existence
346
- this.addNode({
347
- id: folderExitId,
348
- name: `${folder.name}-exit`,
349
- type: 'folder-exit',
350
- parentFolderId: parentPath.startsWith('folder:/') ? parentPath : undefined,
351
- path: folderPath,
352
- item: folder
353
- });
354
-
355
- // Add folder-pre script node ONLY if script exists
356
- if (!isNullOrWhitespace(folder.folderPreScript)) {
357
- this.addNode({
358
- id: folderPreId,
359
- name: `${folder.name}-pre`,
360
- type: 'script',
361
- scriptType: 'folder-pre' as ScriptType,
362
- script: folder.folderPreScript,
363
- parentFolderId: folderPath,
364
- path: folderPath,
365
- item: folder
366
- });
367
- }
368
-
369
- // Add folder-post script node ONLY if script exists
370
- if (!isNullOrWhitespace(folder.folderPostScript)) {
371
- this.addNode({
372
- id: folderPostId,
373
- name: `${folder.name}-post`,
374
- type: 'script',
375
- scriptType: 'folder-post' as ScriptType,
376
- script: folder.folderPostScript,
377
- parentFolderId: folderPath,
378
- path: folderPath,
379
- item: folder
380
- });
381
- }
382
-
383
- // Structural edges: parent → folder-enter
384
- this.addEdge(parentPreId, folderEnterId, 'structural');
385
-
386
- // folder-enter → folder-pre (if pre-script exists)
387
- if (!isNullOrWhitespace(folder.folderPreScript)) {
388
- this.addEdge(folderEnterId, folderPreId, 'structural');
389
- }
390
-
391
- // folder-post → folder-exit (if post-script exists)
392
- if (!isNullOrWhitespace(folder.folderPostScript)) {
393
- this.addEdge(folderPostId, folderExitId, 'structural');
394
- }
395
-
396
- // folder-exit → parent
397
- this.addEdge(folderExitId, parentPostId, 'structural');
398
-
399
- // Register for dependsOn resolution
400
- // Start node is folder-enter (lifecycle), completion is folder-exit (lifecycle)
401
- this.startNodeByItemId.set(folder.id, folderEnterId);
402
- this.completionNodeByItemId.set(folder.id, folderExitId);
403
-
404
- const folderDeps = folder.dependsOn;
405
- if (folderDeps !== undefined && folderDeps.length > 0) {
406
- for (const depId of folderDeps) {
407
- this.pendingDependsOn.push({ depId, targetItemId: folder.id });
408
- }
409
- }
410
-
411
- // Build inherited scripts for children
412
- const folderPreScripts = [...inheritedPreScripts];
413
- if (!isNullOrWhitespace(folder.preRequestScript)) {
414
- folderPreScripts.push(folder.preRequestScript!);
415
- }
416
-
417
- const folderPostScripts = [...inheritedPostScripts];
418
- if (!isNullOrWhitespace(folder.postRequestScript)) {
419
- // Use unshift for LIFO (inner runs before outer)
420
- folderPostScripts.unshift(folder.postRequestScript!);
421
- }
422
-
423
- // Determine which node children connect to (enter or pre-script)
424
- const childrenParentPreId = !isNullOrWhitespace(folder.folderPreScript) ? folderPreId : folderEnterId;
425
- // Determine which node receives children completion (post-script or exit)
426
- const childrenParentPostId = !isNullOrWhitespace(folder.folderPostScript) ? folderPostId : folderExitId;
427
-
428
- // If condition is statically false, skip children
429
- if (this.isConditionFalse(folder.condition)) {
430
- // Direct edge: folder-enter → folder-exit (bypassing children and scripts)
431
- this.addEdge(folderEnterId, folderExitId, 'structural');
432
- return { startId: folderEnterId, endId: folderExitId };
433
- }
434
-
435
- // Compute effective auth (folder auth overrides parent)
436
- const folderAuth = folder.auth ?? parentAuth;
437
-
438
- // Build children (preserve declaration order)
439
- for (const child of folder.items) {
440
- this.buildItem(
441
- child,
442
- folderPath,
443
- childrenParentPreId,
444
- childrenParentPostId,
445
- folderPreScripts,
446
- folderPostScripts,
447
- folderAuth // Pass folder's effective auth to children
448
- );
449
- }
450
-
451
- // Barrier edge (ensures children completion point waits for all children)
452
- this.addEdge(childrenParentPreId, childrenParentPostId, 'structural');
453
-
454
- return { startId: folderEnterId, endId: folderExitId };
455
- }
456
-
457
- private buildRequest(
458
- request: Request,
459
- parentPath: PathType,
460
- parentPreId: string,
461
- parentPostId: string,
462
- inheritedPreScripts: string[],
463
- inheritedPostScripts: string[],
464
- parentAuth?: Auth
465
- ): { startId: string; endId: string } {
466
- const requestPath = this.buildPath(parentPath, request.name, 'request');
467
- const requestId = `request:${requestPath}`;
468
-
469
- this.logger.trace(`buildRequest: ${request.name} (id=${requestId})`);
470
-
471
- // Build final script arrays for this request
472
- const requestPreScripts = [...inheritedPreScripts];
473
- if (!isNullOrWhitespace(request.preRequestScript)) {
474
- requestPreScripts.push(request.preRequestScript!);
475
- }
476
-
477
- const requestPostScripts = [...inheritedPostScripts];
478
- if (!isNullOrWhitespace(request.postRequestScript)) {
479
- // Use unshift for LIFO (request runs before inherited)
480
- requestPostScripts.unshift(request.postRequestScript!);
481
- }
482
-
483
- // Compute effective auth (request auth overrides folder/collection auth)
484
- const effectiveAuth = request.auth ?? parentAuth;
485
-
486
- // Add request node
487
- this.addNode({
488
- id: requestId,
489
- name: request.name,
490
- type: 'request',
491
- condition: request.condition,
492
- inheritedPreScripts: requestPreScripts,
493
- inheritedPostScripts: requestPostScripts,
494
- effectiveAuth, // Store computed effectiveAuth for request execution
495
- parentFolderId: parentPath.startsWith('folder:/') ? parentPath : undefined,
496
- path: requestPath,
497
- item: request
498
- });
499
-
500
- // Structural edges
501
- this.addEdge(parentPreId, requestId, 'structural');
502
- this.addEdge(requestId, parentPostId, 'structural');
503
-
504
- // Plugin event scripts are NOT DAG nodes - they execute via emitEvent() callback
505
- // during plugin I/O phase. This ensures proper serialization and lifecycle.
506
- // Plugin events fire DURING request execution (e.g., WebSocket onMessage), not as separate tasks.
507
- // Removed: this.addEventScriptNodes(request, requestId, parentPostId, parentPath);
508
-
509
- // Register for dependsOn resolution
510
- this.startNodeByItemId.set(request.id, requestId);
511
- this.completionNodeByItemId.set(request.id, requestId);
512
-
513
- if (request.dependsOn !== undefined && request.dependsOn.length > 0) {
514
- for (const depId of request.dependsOn) {
515
- this.pendingDependsOn.push({ depId, targetItemId: request.id });
516
- }
517
- }
518
-
519
- return { startId: requestId, endId: requestId };
520
- }
521
-
522
- private addEventScriptNodes(
523
- request: Request,
524
- requestId: string,
525
- parentPostId: string,
526
- parentPath: PathType
527
- ): void {
528
- const eventScripts = request.data?.scripts;
529
- if (eventScripts === undefined || eventScripts.length === 0) {
530
- return;
531
- }
532
-
533
- for (let i = 0; i < eventScripts.length; i++) {
534
- const eventScript = eventScripts[i];
535
- const eventNodeId = `script:plugin-event:${requestId}:${i}:${eventScript.event}`;
536
-
537
- this.addNode({
538
- id: eventNodeId,
539
- name: `${request.name}-${eventScript.event}`,
540
- type: 'script',
541
- scriptType: 'plugin-event' as ScriptType,
542
- script: eventScript.script,
543
- parentFolderId: parentPath.startsWith('folder:/') ? parentPath : undefined,
544
- path: `${requestId}:${eventScript.event}` as PathType,
545
- eventName: eventScript.event,
546
- parentRequestId: requestId
547
- });
548
-
549
- // Event edges: request → event → parent-post
550
- this.addEdge(requestId, eventNodeId, 'event');
551
- this.addEdge(eventNodeId, parentPostId, 'structural');
552
- }
553
- }
554
-
555
- private resolveDependsOnEdges(): void {
556
- for (const { depId, targetItemId } of this.pendingDependsOn) {
557
- const completionNodeId = this.completionNodeByItemId.get(depId);
558
- if (completionNodeId === undefined) {
559
- // Dependency not found - this can happen when filtering with excludeDeps=true
560
- // Skip this dependency edge instead of throwing
561
- this.logger.warn(`Skipping dependency: Item '${targetItemId}' depends on '${depId}' which is not in the filtered collection`);
562
- continue;
563
- }
564
-
565
- const targetStartNodeId = this.startNodeByItemId.get(targetItemId);
566
- if (targetStartNodeId === undefined) {
567
- throw new Error(`Internal error: No start node for item '${targetItemId}'`);
568
- }
569
-
570
- // Add edge: completionNode(dep) → startNode(target)
571
- this.addEdge(completionNodeId, targetStartNodeId, 'dependsOn');
572
- }
573
- }
574
-
575
-
576
- /**
577
- * Build path with proper type prefix (folder: or request:)
578
- * Similar to ExecutionNode path building
579
- */
580
- private buildPath(parent: string, name: string, type: 'folder' | 'request'): PathType {
581
- // If parent is collection:/
582
- if (parent === 'collection:/') {
583
- return `${type}:/${name}` as PathType;
584
- }
585
-
586
- // Remove type prefix from parent path
587
- const basePath = parent.replace(/^(folder|request):\//, '');
588
- return `${type}:/${basePath}/${name}` as PathType;
589
- }
590
-
591
- private toArray(script: string | undefined): string[] {
592
- return !isNullOrWhitespace(script) ? [script!] : [];
593
- }
594
-
595
- private isConditionFalse(condition: string | undefined): boolean {
596
- return condition?.toLowerCase() === 'false';
597
- }
598
- }
@@ -1,109 +0,0 @@
1
- import type { Collection, CollectionItem, Request, Folder } from '@apiquest/types';
2
- import { ScriptValidator } from './ScriptValidator.js';
3
- import type { PluginManager } from './PluginManager.js';
4
- import { Logger } from './Logger.js';
5
-
6
- /**
7
- * Counts total expected tests in a collection for deterministic test reporting
8
- */
9
- export class TestCounter {
10
- private logger: Logger;
11
-
12
- constructor(
13
- private readonly pluginManager: PluginManager,
14
- baseLogger?: Logger
15
- ) {
16
- this.logger = baseLogger?.createLogger('TestCounter') ?? new Logger('TestCounter');
17
- }
18
-
19
- /**
20
- * Count total expected tests in collection
21
- * - Counts quest.test() calls in all scripts
22
- * - Multiplies by iteration count
23
- * - Returns -1 if collection has dynamic plugin events (can't determine count)
24
- * @returns Total expected test count, or -1 if dynamic
25
- */
26
- countTests(collection: Collection): number {
27
- let totalTests = 0;
28
- let hasDynamicTests = false;
29
-
30
- this.logger.debug(`Counting tests for collection: ${collection.info.name}`);
31
-
32
- // Note: collectionPre/Post and folderPre/Post scripts CANNOT have tests (validation catches this)
33
- // Only preRequestScript and postRequestScript can have tests
34
-
35
- // Helper to walk tree and count tests for each REQUEST with inherited scripts
36
- const countRequestTests = (item: CollectionItem, inheritedPreRequest: string[], inheritedPostRequest: string[]): void => {
37
- if (item.type === 'folder') {
38
- const folder = item;
39
-
40
- // Build inherited script chain (scripts STACK, not override)
41
- const newInheritedPre = (folder.preRequestScript !== null && folder.preRequestScript !== undefined && folder.preRequestScript.length > 0)
42
- ? [...inheritedPreRequest, folder.preRequestScript]
43
- : inheritedPreRequest;
44
- const newInheritedPost = (folder.postRequestScript !== null && folder.postRequestScript !== undefined && folder.postRequestScript.length > 0)
45
- ? [...inheritedPostRequest, folder.postRequestScript]
46
- : inheritedPostRequest;
47
-
48
- // Recursively process folder contents
49
- for (const child of folder.items) {
50
- countRequestTests(child, newInheritedPre, newInheritedPost);
51
- }
52
- } else {
53
- // Request - count ALL scripts in execution chain for THIS request
54
- const request = item;
55
-
56
- // Inherited postRequestScripts (collection and all ancestor folders) - they STACK
57
- for (const script of inheritedPostRequest) {
58
- totalTests += ScriptValidator.countTests(script);
59
- }
60
-
61
- // Request-level postRequestScript (this is where tests are!)
62
- if (request.postRequestScript !== null && request.postRequestScript !== undefined && request.postRequestScript.length > 0) {
63
- totalTests += ScriptValidator.countTests(request.postRequestScript);
64
- }
65
-
66
- // Plugin event scripts
67
- if (request.data.scripts !== null && request.data.scripts !== undefined && Array.isArray(request.data.scripts)) {
68
- const protocolPlugin = this.pluginManager.getPlugin(collection.protocol);
69
- if (protocolPlugin?.events !== null && protocolPlugin?.events !== undefined) {
70
- for (const script of request.data.scripts) {
71
- const eventDef = protocolPlugin.events.find(e => e.name === script.event);
72
- if (eventDef?.canHaveTests === true) {
73
- const expectedMessages = ScriptValidator.extractExpectedMessages(
74
- (request.preRequestScript !== null && request.preRequestScript !== undefined && request.preRequestScript.length > 0) ? request.preRequestScript : ''
75
- );
76
-
77
- if (expectedMessages !== null) {
78
- const testsPerEvent = ScriptValidator.countTests(script.script);
79
- totalTests += testsPerEvent * expectedMessages;
80
- } else {
81
- hasDynamicTests = true;
82
- }
83
- }
84
- }
85
- }
86
- }
87
- }
88
- };
89
-
90
- // Process all items with collection-level inherited scripts
91
- const collectionPre = (collection.preRequestScript !== null && collection.preRequestScript !== undefined && collection.preRequestScript.length > 0) ? [collection.preRequestScript] : [];
92
- const collectionPost = (collection.postRequestScript !== null && collection.postRequestScript !== undefined && collection.postRequestScript.length > 0) ? [collection.postRequestScript] : [];
93
-
94
- for (const item of collection.items) {
95
- countRequestTests(item, collectionPre, collectionPost);
96
- }
97
-
98
- // Multiply by iteration count
99
- const iterationCount = (collection.testData?.length !== null && collection.testData?.length !== undefined && collection.testData.length > 0) ? collection.testData.length : 1;
100
- totalTests *= iterationCount;
101
-
102
- const result = hasDynamicTests ? -1 : totalTests;
103
- if (hasDynamicTests) {
104
- this.logger.debug('Dynamic test count detected; returning -1');
105
- }
106
- this.logger.debug(`Expected test count resolved: ${result}`);
107
- return result;
108
- }
109
- }