@apiquest/fracture 1.0.4 → 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +90 -2
- package/dist/CollectionRunner.d.ts +3 -0
- package/dist/CollectionRunner.d.ts.map +1 -1
- package/dist/CollectionRunner.js +249 -154
- package/dist/CollectionRunner.js.map +1 -1
- package/dist/CollectionValidator.d.ts.map +1 -1
- package/dist/CollectionValidator.js +11 -0
- package/dist/CollectionValidator.js.map +1 -1
- package/dist/ConsoleReporter.d.ts.map +1 -1
- package/dist/ConsoleReporter.js +9 -6
- package/dist/ConsoleReporter.js.map +1 -1
- package/dist/DagScheduler.d.ts.map +1 -1
- package/dist/DagScheduler.js +11 -0
- package/dist/DagScheduler.js.map +1 -1
- package/dist/LibraryLoader.d.ts +49 -0
- package/dist/LibraryLoader.d.ts.map +1 -0
- package/dist/LibraryLoader.js +198 -0
- package/dist/LibraryLoader.js.map +1 -0
- package/dist/PluginLoader.d.ts.map +1 -1
- package/dist/PluginLoader.js +9 -6
- package/dist/PluginLoader.js.map +1 -1
- package/dist/PluginManager.d.ts.map +1 -1
- package/dist/PluginManager.js +11 -7
- package/dist/PluginManager.js.map +1 -1
- package/dist/PluginResolver.d.ts +1 -1
- package/dist/PluginResolver.d.ts.map +1 -1
- package/dist/PluginResolver.js +1 -1
- package/dist/PluginResolver.js.map +1 -1
- package/dist/QuestAPI.d.ts.map +1 -1
- package/dist/QuestAPI.js +114 -217
- package/dist/QuestAPI.js.map +1 -1
- package/dist/ScriptEngine.d.ts +2 -1
- package/dist/ScriptEngine.d.ts.map +1 -1
- package/dist/ScriptEngine.js +15 -8
- package/dist/ScriptEngine.js.map +1 -1
- package/dist/TaskGraph.d.ts +2 -1
- package/dist/TaskGraph.d.ts.map +1 -1
- package/dist/TaskGraph.js +28 -26
- package/dist/TaskGraph.js.map +1 -1
- package/dist/VariableResolver.d.ts +1 -1
- package/dist/VariableResolver.js +10 -10
- package/dist/VariableResolver.js.map +1 -1
- package/dist/cli/index.js +35 -3
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/plugin-commands.d.ts.map +1 -1
- package/dist/cli/plugin-commands.js +47 -81
- package/dist/cli/plugin-commands.js.map +1 -1
- package/dist/cli/plugin-installer.d.ts +48 -0
- package/dist/cli/plugin-installer.d.ts.map +1 -0
- package/dist/cli/plugin-installer.js +136 -0
- package/dist/cli/plugin-installer.js.map +1 -0
- package/dist/cli/plugin-registry.d.ts +17 -0
- package/dist/cli/plugin-registry.d.ts.map +1 -0
- package/dist/cli/plugin-registry.js +77 -0
- package/dist/cli/plugin-registry.js.map +1 -0
- package/package.json +1 -1
- package/tsconfig.json +1 -0
- package/tsconfig.test.json +3 -0
- package/dist/QuestAPI.types.d.ts +0 -35
- package/dist/QuestAPI.types.d.ts.map +0 -1
- package/dist/QuestAPI.types.js +0 -3
- package/dist/QuestAPI.types.js.map +0 -1
- package/src/CollectionAnalyzer.ts +0 -102
- package/src/CollectionRunner.ts +0 -1423
- package/src/CollectionRunner.types.ts +0 -9
- package/src/CollectionValidator.ts +0 -289
- package/src/ConsoleReporter.ts +0 -143
- package/src/CookieJar.ts +0 -258
- package/src/DagScheduler.ts +0 -439
- package/src/Logger.ts +0 -85
- package/src/PluginLoader.ts +0 -126
- package/src/PluginManager.ts +0 -208
- package/src/PluginResolver.ts +0 -154
- package/src/QuestAPI.ts +0 -764
- package/src/QuestAPI.types.ts +0 -33
- package/src/QuestTestAPI.ts +0 -164
- package/src/RequestFilter.ts +0 -224
- package/src/ScriptEngine.ts +0 -219
- package/src/ScriptValidator.ts +0 -428
- package/src/TaskGraph.ts +0 -598
- package/src/TestCounter.ts +0 -109
- package/src/VariableResolver.ts +0 -114
- package/src/cli/index.ts +0 -480
- package/src/cli/plugin-commands.ts +0 -342
- package/src/cli/plugin-discovery.ts +0 -44
- package/src/index.ts +0 -24
- package/src/utils.ts +0 -52
package/src/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
|
-
}
|
package/src/TestCounter.ts
DELETED
|
@@ -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
|
-
}
|