@feasibleone/blong-chain 1.8.1 → 1.8.2
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/CHANGELOG.md +7 -0
- package/package.json +2 -2
- package/dist/examples/demo.test.d.ts +0 -6
- package/dist/examples/demo.test.d.ts.map +0 -1
- package/dist/examples/demo.test.js.map +0 -1
- package/dist/examples/error-demo.test.d.ts +0 -14
- package/dist/examples/error-demo.test.d.ts.map +0 -1
- package/dist/examples/error-demo.test.js.map +0 -1
- package/dist/index.d.ts +0 -87
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -917
- package/dist/index.js.map +0 -1
- package/dist/index.test.d.ts +0 -14
- package/dist/index.test.d.ts.map +0 -1
- package/dist/index.test.js.map +0 -1
- package/dist/package.json +0 -39
- package/dist/showcase.test.d.ts +0 -19
- package/dist/showcase.test.d.ts.map +0 -1
- package/dist/showcase.test.js.map +0 -1
- package/dist/snapshot-context.test.d.ts +0 -30
- package/dist/snapshot-context.test.d.ts.map +0 -1
- package/dist/snapshot-context.test.js.map +0 -1
- package/dist/test-types.d.ts +0 -377
- package/dist/test-types.d.ts.map +0 -1
- package/dist/test-types.js +0 -2
- package/dist/test-types.js.map +0 -1
- package/dist/timing-analysis.test.d.ts +0 -2
- package/dist/timing-analysis.test.d.ts.map +0 -1
- package/dist/timing-analysis.test.js.map +0 -1
package/dist/index.js
DELETED
|
@@ -1,917 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Parallel Test Executor
|
|
3
|
-
*
|
|
4
|
-
* Implements the new parallel test execution framework with:
|
|
5
|
-
* - Thenable proxies for automatic dependency detection
|
|
6
|
-
* - Parallel execution with configurable concurrency
|
|
7
|
-
* - Dependency graph tracking
|
|
8
|
-
* - Live progress tracking
|
|
9
|
-
* - Enhanced error reporting
|
|
10
|
-
* - Latency metrics
|
|
11
|
-
*/
|
|
12
|
-
import assert from 'node:assert';
|
|
13
|
-
import { EventEmitter } from 'node:events';
|
|
14
|
-
import PQueue from 'p-queue';
|
|
15
|
-
/**
|
|
16
|
-
* Creates a thenable proxy for a given context path.
|
|
17
|
-
* The proxy acts as a Promise and supports nested property access.
|
|
18
|
-
*
|
|
19
|
-
* @param path - The context path (e.g., 'setupData' or 'setupData.user.name')
|
|
20
|
-
* @param promiseManager - The promise manager to get/create promises
|
|
21
|
-
* @returns A thenable proxy that can be awaited or have properties accessed
|
|
22
|
-
*/
|
|
23
|
-
function createThenableProxy(path, promiseManager) {
|
|
24
|
-
// Get or create the promise for this path
|
|
25
|
-
const promiseEntry = promiseManager.getOrCreate(path);
|
|
26
|
-
// Create a proxy that intercepts property access
|
|
27
|
-
const proxy = new Proxy(promiseEntry.promise, {
|
|
28
|
-
get(target, prop) {
|
|
29
|
-
// Promise methods: delegate to the real promise
|
|
30
|
-
if (prop === 'then' || prop === 'catch' || prop === 'finally') {
|
|
31
|
-
return target[prop].bind(target);
|
|
32
|
-
}
|
|
33
|
-
// Symbol properties (like Symbol.toStringTag)
|
|
34
|
-
if (typeof prop === 'symbol') {
|
|
35
|
-
return target[prop];
|
|
36
|
-
}
|
|
37
|
-
// Property access: return nested thenable proxy
|
|
38
|
-
return createThenableProxy(`${path}.${prop}`, promiseManager);
|
|
39
|
-
},
|
|
40
|
-
});
|
|
41
|
-
return proxy;
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* Manages promises for all context paths.
|
|
45
|
-
* Provides lazy creation and caching of promises.
|
|
46
|
-
*/
|
|
47
|
-
class PromiseManager {
|
|
48
|
-
promises = new Map();
|
|
49
|
-
realContext;
|
|
50
|
-
constructor(realContext) {
|
|
51
|
-
this.realContext = realContext;
|
|
52
|
-
}
|
|
53
|
-
/**
|
|
54
|
-
* Gets an existing promise or creates a new one for the given path
|
|
55
|
-
*/
|
|
56
|
-
getOrCreate(path) {
|
|
57
|
-
if (this.promises.has(path)) {
|
|
58
|
-
return this.promises.get(path);
|
|
59
|
-
}
|
|
60
|
-
let resolve;
|
|
61
|
-
let reject;
|
|
62
|
-
const promise = new Promise((res, rej) => {
|
|
63
|
-
resolve = res;
|
|
64
|
-
reject = rej;
|
|
65
|
-
});
|
|
66
|
-
const entry = {
|
|
67
|
-
promise,
|
|
68
|
-
resolve: resolve,
|
|
69
|
-
reject: reject,
|
|
70
|
-
};
|
|
71
|
-
this.promises.set(path, entry);
|
|
72
|
-
// Check if this is a top-level step that has already completed
|
|
73
|
-
const parts = path.split('.');
|
|
74
|
-
const stepName = parts[0];
|
|
75
|
-
if (parts.length === 1 && stepName in this.realContext) {
|
|
76
|
-
// Step already completed, resolve immediately
|
|
77
|
-
entry.resolve(this.realContext[stepName]);
|
|
78
|
-
}
|
|
79
|
-
else if (parts.length > 1 && stepName in this.realContext) {
|
|
80
|
-
// Nested property of a completed step
|
|
81
|
-
const value = this._getNestedValue(this.realContext[stepName], parts.slice(1).join('.'));
|
|
82
|
-
entry.resolve(value);
|
|
83
|
-
}
|
|
84
|
-
else {
|
|
85
|
-
// Step hasn't completed yet, check if parent is already resolved
|
|
86
|
-
this._autoResolveIfParentResolved(path, entry);
|
|
87
|
-
}
|
|
88
|
-
return entry;
|
|
89
|
-
}
|
|
90
|
-
/**
|
|
91
|
-
* If parent path is already resolved, resolve this child path immediately
|
|
92
|
-
*/
|
|
93
|
-
_autoResolveIfParentResolved(path, entry) {
|
|
94
|
-
const parts = path.split('.');
|
|
95
|
-
if (parts.length <= 1)
|
|
96
|
-
return; // No parent
|
|
97
|
-
// Check each parent level from most specific to least
|
|
98
|
-
for (let i = parts.length - 1; i > 0; i--) {
|
|
99
|
-
const parentPath = parts.slice(0, i).join('.');
|
|
100
|
-
const parentEntry = this.promises.get(parentPath);
|
|
101
|
-
if (parentEntry) {
|
|
102
|
-
// Wait for parent to resolve, then resolve child
|
|
103
|
-
parentEntry.promise
|
|
104
|
-
.then(parentValue => {
|
|
105
|
-
// Navigate to the child value
|
|
106
|
-
const childPath = parts.slice(i).join('.');
|
|
107
|
-
const childValue = this._getNestedValue(parentValue, childPath);
|
|
108
|
-
// Resolve the child promise
|
|
109
|
-
entry.resolve(childValue);
|
|
110
|
-
})
|
|
111
|
-
.catch(error => {
|
|
112
|
-
// Parent rejected, reject child too
|
|
113
|
-
entry.reject(error);
|
|
114
|
-
});
|
|
115
|
-
return;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
/**
|
|
120
|
-
* Gets nested value from an object by path
|
|
121
|
-
*/
|
|
122
|
-
_getNestedValue(obj, path) {
|
|
123
|
-
const parts = path.split('.');
|
|
124
|
-
let current = obj;
|
|
125
|
-
for (const part of parts) {
|
|
126
|
-
if (current && typeof current === 'object') {
|
|
127
|
-
current = current[part];
|
|
128
|
-
}
|
|
129
|
-
else {
|
|
130
|
-
return undefined;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
return current;
|
|
134
|
-
}
|
|
135
|
-
/**
|
|
136
|
-
* Checks if a promise exists for the given path
|
|
137
|
-
*/
|
|
138
|
-
has(path) {
|
|
139
|
-
return this.promises.has(path);
|
|
140
|
-
}
|
|
141
|
-
/**
|
|
142
|
-
* Resolves all promises related to a step's output
|
|
143
|
-
*/
|
|
144
|
-
resolveStep(stepName, output) {
|
|
145
|
-
// Resolve the main step promise
|
|
146
|
-
if (this.promises.has(stepName)) {
|
|
147
|
-
this.promises.get(stepName).resolve(output);
|
|
148
|
-
}
|
|
149
|
-
// Resolve nested property promises
|
|
150
|
-
if (typeof output === 'object' && output !== null) {
|
|
151
|
-
this._resolveNestedProperties(stepName, output);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
/**
|
|
155
|
-
* Recursively resolves promises for nested properties
|
|
156
|
-
*/
|
|
157
|
-
_resolveNestedProperties(basePath, obj, depth = 0) {
|
|
158
|
-
// Limit recursion depth to avoid infinite loops
|
|
159
|
-
if (depth > 10)
|
|
160
|
-
return;
|
|
161
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
162
|
-
const nestedPath = `${basePath}.${key}`;
|
|
163
|
-
if (this.promises.has(nestedPath)) {
|
|
164
|
-
this.promises.get(nestedPath).resolve(value);
|
|
165
|
-
}
|
|
166
|
-
// Recursively resolve deeper properties
|
|
167
|
-
if (typeof value === 'object' && value !== null) {
|
|
168
|
-
this._resolveNestedProperties(nestedPath, value, depth + 1);
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
/**
|
|
173
|
-
* Rejects a promise for a given path
|
|
174
|
-
*/
|
|
175
|
-
reject(path, error) {
|
|
176
|
-
if (this.promises.has(path)) {
|
|
177
|
-
this.promises.get(path).reject(error);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
/**
|
|
182
|
-
* Creates a context proxy that returns thenable proxies for all properties
|
|
183
|
-
* except $meta, which is always available directly.
|
|
184
|
-
*
|
|
185
|
-
* Also tracks which properties are accessed for dependency detection.
|
|
186
|
-
*/
|
|
187
|
-
function createContextProxy(realContext, promiseManager, currentStep, dependencyTracker) {
|
|
188
|
-
return new Proxy(realContext, {
|
|
189
|
-
get(target, prop) {
|
|
190
|
-
// Special case: $meta is always available directly
|
|
191
|
-
if (prop === '$meta') {
|
|
192
|
-
return target.$meta;
|
|
193
|
-
}
|
|
194
|
-
// Track dependency if we're inside a step execution
|
|
195
|
-
if (currentStep && typeof prop === 'string') {
|
|
196
|
-
dependencyTracker.trackAccess(currentStep, prop);
|
|
197
|
-
}
|
|
198
|
-
// Return thenable proxy for step outputs
|
|
199
|
-
if (typeof prop === 'string') {
|
|
200
|
-
return createThenableProxy(prop, promiseManager);
|
|
201
|
-
}
|
|
202
|
-
return target[prop];
|
|
203
|
-
},
|
|
204
|
-
});
|
|
205
|
-
}
|
|
206
|
-
/**
|
|
207
|
-
* Tracks dependency relationships between steps
|
|
208
|
-
*/
|
|
209
|
-
class DependencyTracker {
|
|
210
|
-
dependencies = new Map();
|
|
211
|
-
validStepNames = new Set();
|
|
212
|
-
/**
|
|
213
|
-
* Sets the valid step names that can be referenced
|
|
214
|
-
*/
|
|
215
|
-
setValidStepNames(stepNames) {
|
|
216
|
-
this.validStepNames = stepNames;
|
|
217
|
-
}
|
|
218
|
-
/**
|
|
219
|
-
* Records that a step accessed a property
|
|
220
|
-
*/
|
|
221
|
-
trackAccess(fromStep, property) {
|
|
222
|
-
if (!this.dependencies.has(fromStep)) {
|
|
223
|
-
this.dependencies.set(fromStep, new Set());
|
|
224
|
-
}
|
|
225
|
-
this.dependencies.get(fromStep).add(property);
|
|
226
|
-
// Validate immediately if we have valid step names
|
|
227
|
-
if (this.validStepNames.size > 0) {
|
|
228
|
-
const stepName = property.split('.')[0];
|
|
229
|
-
if (!this.validStepNames.has(stepName)) {
|
|
230
|
-
throw new Error(`Invalid step reference(s) detected: Step "${fromStep}" references "context.${property}", ` +
|
|
231
|
-
`but no step named "${stepName}" exists. ` +
|
|
232
|
-
`Available steps: ${Array.from(this.validStepNames).sort().join(', ')}`);
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
/**
|
|
237
|
-
* Gets all dependencies for a step
|
|
238
|
-
*/
|
|
239
|
-
getDependencies(stepName) {
|
|
240
|
-
return Array.from(this.dependencies.get(stepName) || []);
|
|
241
|
-
}
|
|
242
|
-
/**
|
|
243
|
-
* Gets all dependency edges as graph edges
|
|
244
|
-
*/
|
|
245
|
-
getEdges() {
|
|
246
|
-
const edges = [];
|
|
247
|
-
for (const [from, properties] of this.dependencies.entries()) {
|
|
248
|
-
for (const property of properties) {
|
|
249
|
-
// Extract the base step name from the property path
|
|
250
|
-
const to = property.split('.')[0];
|
|
251
|
-
edges.push({ from, to, property });
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
return edges;
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
/**
|
|
258
|
-
* Captures source location information for error reporting
|
|
259
|
-
*/
|
|
260
|
-
function captureSourceLocation() {
|
|
261
|
-
try {
|
|
262
|
-
const stack = new Error().stack || '';
|
|
263
|
-
const lines = stack.split('\n');
|
|
264
|
-
// Find the first line that's not from this file
|
|
265
|
-
for (let i = 2; i < lines.length; i++) {
|
|
266
|
-
const line = lines[i];
|
|
267
|
-
if (!line.includes('executor.ts') && !line.includes('executor.js')) {
|
|
268
|
-
// Try to parse: "at functionName (file:line:column)"
|
|
269
|
-
const match = line.match(/\((.+):(\d+):(\d+)\)/);
|
|
270
|
-
if (match) {
|
|
271
|
-
return {
|
|
272
|
-
file: match[1],
|
|
273
|
-
line: parseInt(match[2], 10),
|
|
274
|
-
column: parseInt(match[3], 10),
|
|
275
|
-
};
|
|
276
|
-
}
|
|
277
|
-
// Try alternative format: "at file:line:column"
|
|
278
|
-
const altMatch = line.match(/at (.+):(\d+):(\d+)/);
|
|
279
|
-
if (altMatch) {
|
|
280
|
-
return {
|
|
281
|
-
file: altMatch[1],
|
|
282
|
-
line: parseInt(altMatch[2], 10),
|
|
283
|
-
column: parseInt(altMatch[3], 10),
|
|
284
|
-
};
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
catch {
|
|
290
|
-
// If parsing fails, return unknown location
|
|
291
|
-
}
|
|
292
|
-
return {
|
|
293
|
-
file: 'unknown',
|
|
294
|
-
line: 0,
|
|
295
|
-
column: 0,
|
|
296
|
-
};
|
|
297
|
-
}
|
|
298
|
-
/** Default number of retry attempts per failing step when `rerun.enabled` is true */
|
|
299
|
-
const DEFAULT_MAX_RETRIES = 1;
|
|
300
|
-
// ============================================================================
|
|
301
|
-
// Masking helpers (also used by assert.snapshot and checkpoint snapshots)
|
|
302
|
-
// ============================================================================
|
|
303
|
-
/**
|
|
304
|
-
* Deep-clone `value` and replace the leaf at each dot-path in `paths` with
|
|
305
|
-
* `'<masked>'`. Supports `'*'` as a wildcard in any path segment, meaning
|
|
306
|
-
* "apply to every direct child of the current object".
|
|
307
|
-
*
|
|
308
|
-
* Examples:
|
|
309
|
-
* maskPaths({id: '1', name: 'A'}, ['id'])
|
|
310
|
-
* → {id: '<masked>', name: 'A'}
|
|
311
|
-
* maskPaths({a: {id: '1'}, b: {id: '2'}}, ['*.id'])
|
|
312
|
-
* → {a: {id: '<masked>'}, b: {id: '<masked>'}}
|
|
313
|
-
*/
|
|
314
|
-
function maskPaths(value, paths) {
|
|
315
|
-
if (value === null || value === undefined || typeof value !== 'object')
|
|
316
|
-
return value;
|
|
317
|
-
const clone = JSON.parse(JSON.stringify(value));
|
|
318
|
-
for (const path of paths)
|
|
319
|
-
setAtPath(clone, path.split('.'));
|
|
320
|
-
return clone;
|
|
321
|
-
}
|
|
322
|
-
function setAtPath(obj, parts) {
|
|
323
|
-
if (typeof obj !== 'object' || obj === null || parts.length === 0)
|
|
324
|
-
return;
|
|
325
|
-
const [head, ...tail] = parts;
|
|
326
|
-
if (head === '__proto__' || head === 'constructor' || head === 'prototype')
|
|
327
|
-
return;
|
|
328
|
-
const record = obj;
|
|
329
|
-
if (tail.length === 0) {
|
|
330
|
-
if (Object.prototype.hasOwnProperty.call(record, head))
|
|
331
|
-
record[head] = '<masked>';
|
|
332
|
-
}
|
|
333
|
-
else if (head === '*') {
|
|
334
|
-
for (const key of Object.keys(record))
|
|
335
|
-
setAtPath(record[key], tail);
|
|
336
|
-
}
|
|
337
|
-
else {
|
|
338
|
-
setAtPath(record[head], tail);
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
/**
|
|
342
|
-
* Main test executor class
|
|
343
|
-
*/
|
|
344
|
-
export class TestExecutor extends EventEmitter {
|
|
345
|
-
config;
|
|
346
|
-
queue;
|
|
347
|
-
dependencyTracker = new DependencyTracker();
|
|
348
|
-
log;
|
|
349
|
-
// Progress tracking
|
|
350
|
-
progress = {
|
|
351
|
-
testName: 'test',
|
|
352
|
-
startTime: 0,
|
|
353
|
-
status: 'pending',
|
|
354
|
-
totalSteps: 0,
|
|
355
|
-
completedSteps: 0,
|
|
356
|
-
failedSteps: 0,
|
|
357
|
-
steps: new Map(),
|
|
358
|
-
groups: [],
|
|
359
|
-
};
|
|
360
|
-
// Dependency graph
|
|
361
|
-
graph = {
|
|
362
|
-
nodes: new Map(),
|
|
363
|
-
edges: [],
|
|
364
|
-
};
|
|
365
|
-
// Latency tracking
|
|
366
|
-
latencyMetrics = new Map();
|
|
367
|
-
// Real context (actual values)
|
|
368
|
-
realContext = {};
|
|
369
|
-
// Promise manager (needs realContext, initialized in constructor)
|
|
370
|
-
promiseManager;
|
|
371
|
-
// Test framework context for nested test output
|
|
372
|
-
testContext;
|
|
373
|
-
// Track step names to detect duplicates
|
|
374
|
-
stepNamesUsed = new Set();
|
|
375
|
-
constructor(config = {}) {
|
|
376
|
-
super();
|
|
377
|
-
this.config = {
|
|
378
|
-
concurrency: config.concurrency ?? 10,
|
|
379
|
-
captureStackTraces: config.captureStackTraces ?? false,
|
|
380
|
-
framework: config.framework,
|
|
381
|
-
log: config.log,
|
|
382
|
-
rerun: config.rerun,
|
|
383
|
-
mask: config.mask,
|
|
384
|
-
maskFn: config.maskFn,
|
|
385
|
-
autoSnapshot: config.autoSnapshot,
|
|
386
|
-
};
|
|
387
|
-
this.log = config.log;
|
|
388
|
-
this.queue = new PQueue({ concurrency: this.config.concurrency });
|
|
389
|
-
// Initialize promise manager with reference to realContext
|
|
390
|
-
this.promiseManager = new PromiseManager(this.realContext);
|
|
391
|
-
}
|
|
392
|
-
/**
|
|
393
|
-
* Executes an array of test steps
|
|
394
|
-
*/
|
|
395
|
-
async execute(steps, $meta, testContext) {
|
|
396
|
-
// Store test context for nested execution
|
|
397
|
-
this.testContext = testContext;
|
|
398
|
-
// Clear and initialize context with $meta (preserve reference for PromiseManager)
|
|
399
|
-
Object.keys(this.realContext).forEach(key => delete this.realContext[key]);
|
|
400
|
-
this.realContext.$meta = $meta;
|
|
401
|
-
// Clear step name tracking for new test run
|
|
402
|
-
this.stepNamesUsed.clear();
|
|
403
|
-
// Initialize progress
|
|
404
|
-
this.progress.testName = steps.name || 'test';
|
|
405
|
-
this.progress.startTime = Date.now();
|
|
406
|
-
this.progress.status = 'running';
|
|
407
|
-
this.progress.completedSteps = 0;
|
|
408
|
-
this.progress.failedSteps = 0;
|
|
409
|
-
this.progress.steps.clear();
|
|
410
|
-
this.progress.groups = [];
|
|
411
|
-
// Reset dependency graph
|
|
412
|
-
this.graph.nodes.clear();
|
|
413
|
-
this.graph.edges = [];
|
|
414
|
-
// Reset latency metrics
|
|
415
|
-
this.latencyMetrics.clear();
|
|
416
|
-
// Count total steps and collect step names
|
|
417
|
-
this.progress.totalSteps = this._countSteps(steps);
|
|
418
|
-
const allStepNames = this._collectStepNames(steps);
|
|
419
|
-
// Set valid step names for dependency validation
|
|
420
|
-
this.dependencyTracker.setValidStepNames(allStepNames);
|
|
421
|
-
// Emit test start event
|
|
422
|
-
this.emit('test:start', this.progress);
|
|
423
|
-
try {
|
|
424
|
-
// Execute all steps
|
|
425
|
-
await this._executeSteps(steps, [], this.testContext);
|
|
426
|
-
// Mark as completed
|
|
427
|
-
this.progress.status = 'completed';
|
|
428
|
-
this.progress.endTime = Date.now();
|
|
429
|
-
// Build final dependency graph
|
|
430
|
-
this.graph.edges = this.dependencyTracker.getEdges();
|
|
431
|
-
}
|
|
432
|
-
catch (error) {
|
|
433
|
-
this.progress.status = 'failed';
|
|
434
|
-
this.progress.endTime = Date.now();
|
|
435
|
-
throw error;
|
|
436
|
-
}
|
|
437
|
-
finally {
|
|
438
|
-
this.emit('test:end', this.progress);
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
/**
|
|
442
|
-
* Recursively executes steps, handling both functions and nested arrays
|
|
443
|
-
*/
|
|
444
|
-
async _executeSteps(steps, groupPath, parentTestContext) {
|
|
445
|
-
const stepPromises = [];
|
|
446
|
-
const namedPromises = new Map();
|
|
447
|
-
let checkpointIndex = 0;
|
|
448
|
-
for (const step of steps) {
|
|
449
|
-
if (Array.isArray(step)) {
|
|
450
|
-
// Distinguish by element type:
|
|
451
|
-
// [] empty array → sync barrier (existing behaviour)
|
|
452
|
-
// ['*'] / ['s1','s2'] → snapshot checkpoint (new)
|
|
453
|
-
// [fn, ...] nested → nested step group (existing behaviour)
|
|
454
|
-
if (step.length === 0) {
|
|
455
|
-
// Sync barrier — wait for all parallel steps in this batch
|
|
456
|
-
await Promise.all(stepPromises);
|
|
457
|
-
stepPromises.length = 0;
|
|
458
|
-
continue;
|
|
459
|
-
}
|
|
460
|
-
if (step.every(s => typeof s === 'string')) {
|
|
461
|
-
// Snapshot checkpoint: await relevant steps, then snapshot
|
|
462
|
-
const checkpoint = step;
|
|
463
|
-
if (checkpoint.length === 1 && checkpoint[0] === '*') {
|
|
464
|
-
// ['*'] — wait for entire current batch
|
|
465
|
-
await Promise.all(stepPromises);
|
|
466
|
-
stepPromises.length = 0;
|
|
467
|
-
}
|
|
468
|
-
else {
|
|
469
|
-
// ['step1','step2'] — wait only for the named steps
|
|
470
|
-
const namedToWait = checkpoint
|
|
471
|
-
.map(name => namedPromises.get(name))
|
|
472
|
-
.filter((p) => p !== undefined);
|
|
473
|
-
await Promise.all(namedToWait);
|
|
474
|
-
// stepPromises is NOT cleared — other steps keep running
|
|
475
|
-
}
|
|
476
|
-
const cpName = checkpoint.name ??
|
|
477
|
-
(checkpoint.length === 1 && checkpoint[0] === '*'
|
|
478
|
-
? `context`
|
|
479
|
-
: checkpoint.join('-'));
|
|
480
|
-
// Disambiguate when the same name is used more than once
|
|
481
|
-
const snapshotName = checkpointIndex === 0 ? cpName : `${cpName}-${checkpointIndex}`;
|
|
482
|
-
checkpointIndex++;
|
|
483
|
-
const stepsToSnapshot = checkpoint.length === 1 && checkpoint[0] === '*'
|
|
484
|
-
? [...this.progress.steps.entries()]
|
|
485
|
-
.filter(([, s]) => s.status === 'completed')
|
|
486
|
-
.map(([name]) => name)
|
|
487
|
-
: checkpoint.filter(name => Object.prototype.hasOwnProperty.call(this.realContext, name));
|
|
488
|
-
const contextSnapshot = Object.fromEntries(stepsToSnapshot.map(name => [
|
|
489
|
-
name,
|
|
490
|
-
this._applyMask(this.realContext[name]),
|
|
491
|
-
]));
|
|
492
|
-
const snapshotTarget = parentTestContext;
|
|
493
|
-
if (snapshotTarget && typeof snapshotTarget.matchSnapshot === 'function') {
|
|
494
|
-
snapshotTarget.matchSnapshot(contextSnapshot, snapshotName);
|
|
495
|
-
}
|
|
496
|
-
continue;
|
|
497
|
-
}
|
|
498
|
-
// Nested step group — wait for current batch first
|
|
499
|
-
await Promise.all(stepPromises);
|
|
500
|
-
stepPromises.length = 0;
|
|
501
|
-
const nestedGroupPath = [...groupPath, step.name || `group-${groupPath.length}`];
|
|
502
|
-
// If we have a test context, use it to create nested test scope
|
|
503
|
-
if (this.testContext && parentTestContext) {
|
|
504
|
-
const nestedName = step.name || `group-${groupPath.length}`;
|
|
505
|
-
await this.testContext.test.call(parentTestContext, nestedName, async (nestedContext) => {
|
|
506
|
-
await this._executeSteps(step, nestedGroupPath, nestedContext);
|
|
507
|
-
});
|
|
508
|
-
}
|
|
509
|
-
else if (this.testContext && groupPath.length === 0) {
|
|
510
|
-
// Top-level nested array
|
|
511
|
-
const nestedName = step.name || `group-${groupPath.length}`;
|
|
512
|
-
await this.testContext.test(nestedName, async (nestedContext) => {
|
|
513
|
-
await this._executeSteps(step, nestedGroupPath, nestedContext);
|
|
514
|
-
});
|
|
515
|
-
}
|
|
516
|
-
else {
|
|
517
|
-
// No test context, execute directly
|
|
518
|
-
await this._executeSteps(step, nestedGroupPath, parentTestContext);
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
else if (typeof step === 'function') {
|
|
522
|
-
// Execute function step in parallel
|
|
523
|
-
const promise = this._executeStep(step, groupPath, parentTestContext);
|
|
524
|
-
const stepName = step.name || 'anonymous';
|
|
525
|
-
stepPromises.push(promise);
|
|
526
|
-
namedPromises.set(stepName, promise);
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
// Wait for remaining steps at this level
|
|
530
|
-
await Promise.all(stepPromises);
|
|
531
|
-
}
|
|
532
|
-
/**
|
|
533
|
-
* Executes a single step function
|
|
534
|
-
*/
|
|
535
|
-
async _executeStep(fn, groupPath, parentTestContext) {
|
|
536
|
-
const stepName = fn.name || 'anonymous';
|
|
537
|
-
// Check for duplicate step names
|
|
538
|
-
this._checkForDuplicateStepName(stepName);
|
|
539
|
-
// Capture source location if enabled
|
|
540
|
-
const sourceLocation = this.config.captureStackTraces ? captureSourceLocation() : undefined;
|
|
541
|
-
// Initialize step progress
|
|
542
|
-
const stepProgress = {
|
|
543
|
-
stepName,
|
|
544
|
-
displayName: stepName,
|
|
545
|
-
groupPath,
|
|
546
|
-
status: 'pending',
|
|
547
|
-
dependencies: [],
|
|
548
|
-
dependents: [],
|
|
549
|
-
sourceLocation,
|
|
550
|
-
};
|
|
551
|
-
this.progress.steps.set(stepName, stepProgress);
|
|
552
|
-
// Initialize dependency graph node
|
|
553
|
-
this.graph.nodes.set(stepName, {
|
|
554
|
-
stepName,
|
|
555
|
-
groupPath,
|
|
556
|
-
status: 'pending',
|
|
557
|
-
});
|
|
558
|
-
// Initialize latency tracking
|
|
559
|
-
const latency = {
|
|
560
|
-
stepName,
|
|
561
|
-
queuedAt: Date.now(),
|
|
562
|
-
queueTime: 0,
|
|
563
|
-
waitTime: 0,
|
|
564
|
-
executionTime: 0,
|
|
565
|
-
totalTime: 0,
|
|
566
|
-
};
|
|
567
|
-
this.latencyMetrics.set(stepName, latency);
|
|
568
|
-
// Wrap execution function for potential test context wrapping
|
|
569
|
-
// When a TAP sub-test context is supplied, assert is augmented with:
|
|
570
|
-
// assert.snapshot(value, 'name', opts?) — explicit snapshot
|
|
571
|
-
// assert.snapshot({mask?: []}) — deferred: snapshot the
|
|
572
|
-
// step's return value
|
|
573
|
-
// assert.snapshot() — deferred, no extra mask
|
|
574
|
-
// Deferred snapshots are taken after fn() returns, under the step name.
|
|
575
|
-
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
576
|
-
const self = this;
|
|
577
|
-
const executeStepFn = async (stepTestContext) => {
|
|
578
|
-
const hasSnapshotTarget = stepTestContext !== undefined &&
|
|
579
|
-
typeof stepTestContext.matchSnapshot === 'function';
|
|
580
|
-
// Tracks a deferred assert.snapshot() call made inside the step.
|
|
581
|
-
const snapshotRequest = {};
|
|
582
|
-
const stepAssert = hasSnapshotTarget
|
|
583
|
-
? new Proxy(assert, {
|
|
584
|
-
get(target, prop) {
|
|
585
|
-
if (prop === 'matchSnapshot')
|
|
586
|
-
return stepTestContext?.matchSnapshot;
|
|
587
|
-
if (prop === 'snapshot')
|
|
588
|
-
return (valueOrOpts, nameOrNothing, opts) => {
|
|
589
|
-
if (typeof nameOrNothing === 'string') {
|
|
590
|
-
// Explicit: assert.snapshot(value, 'name', opts?)
|
|
591
|
-
if (!valueOrOpts)
|
|
592
|
-
throw new assert.AssertionError({
|
|
593
|
-
message: `snapshot "${nameOrNothing}": value is falsy`,
|
|
594
|
-
});
|
|
595
|
-
const masked = self._applyMask(valueOrOpts, opts?.mask);
|
|
596
|
-
stepTestContext.matchSnapshot(masked, nameOrNothing);
|
|
597
|
-
}
|
|
598
|
-
else {
|
|
599
|
-
// Deferred: assert.snapshot() or assert.snapshot({mask})
|
|
600
|
-
const deferOpts = typeof valueOrOpts === 'object' &&
|
|
601
|
-
valueOrOpts !== null &&
|
|
602
|
-
!Array.isArray(valueOrOpts)
|
|
603
|
-
? valueOrOpts
|
|
604
|
-
: {};
|
|
605
|
-
snapshotRequest.deferred = { mask: deferOpts.mask };
|
|
606
|
-
}
|
|
607
|
-
};
|
|
608
|
-
return target[prop];
|
|
609
|
-
},
|
|
610
|
-
})
|
|
611
|
-
: assert;
|
|
612
|
-
latency.startedAt = Date.now();
|
|
613
|
-
latency.queueTime = latency.startedAt - latency.queuedAt;
|
|
614
|
-
stepProgress.status = 'running';
|
|
615
|
-
stepProgress.startTime = latency.startedAt;
|
|
616
|
-
this.graph.nodes.get(stepName).status = 'running';
|
|
617
|
-
this.graph.nodes.get(stepName).startTime = latency.startedAt;
|
|
618
|
-
this.emit('step:start', stepName, stepProgress);
|
|
619
|
-
try {
|
|
620
|
-
// Create tracking context
|
|
621
|
-
const context = createContextProxy(this.realContext, this.promiseManager, stepName, this.dependencyTracker);
|
|
622
|
-
// Execute the step (with optional retry loop)
|
|
623
|
-
const maxRetries = this.config.rerun?.enabled
|
|
624
|
-
? (this.config.rerun.maxRetries ?? DEFAULT_MAX_RETRIES)
|
|
625
|
-
: 0;
|
|
626
|
-
let result;
|
|
627
|
-
let lastError;
|
|
628
|
-
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
629
|
-
// Reset per-attempt deferred snapshot flag
|
|
630
|
-
delete snapshotRequest.deferred;
|
|
631
|
-
try {
|
|
632
|
-
result = await fn(stepAssert, context);
|
|
633
|
-
lastError = undefined;
|
|
634
|
-
break;
|
|
635
|
-
}
|
|
636
|
-
catch (err) {
|
|
637
|
-
lastError = err;
|
|
638
|
-
if (attempt < maxRetries) {
|
|
639
|
-
this.log?.warn?.({ err }, `step ${stepName} failed (attempt ${attempt + 1}/${maxRetries + 1}), retrying`);
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
}
|
|
643
|
-
if (lastError !== undefined) {
|
|
644
|
-
throw lastError;
|
|
645
|
-
}
|
|
646
|
-
// Handle deferred assert.snapshot() — called inside step with no explicit value
|
|
647
|
-
if (snapshotRequest.deferred !== undefined && hasSnapshotTarget) {
|
|
648
|
-
if (!result)
|
|
649
|
-
throw new assert.AssertionError({
|
|
650
|
-
message: `snapshot "${stepName}": step returned a falsy value`,
|
|
651
|
-
});
|
|
652
|
-
const masked = self._applyMask(result, snapshotRequest.deferred.mask);
|
|
653
|
-
stepTestContext.matchSnapshot(masked, stepName);
|
|
654
|
-
}
|
|
655
|
-
else if (self.config.autoSnapshot && hasSnapshotTarget) {
|
|
656
|
-
// Auto-snapshot: capture result automatically, no assert.snapshot() needed
|
|
657
|
-
if (!result)
|
|
658
|
-
throw new assert.AssertionError({
|
|
659
|
-
message: `snapshot "${stepName}": step returned a falsy value`,
|
|
660
|
-
});
|
|
661
|
-
const masked = self._applyMask(result);
|
|
662
|
-
stepTestContext.matchSnapshot(masked, stepName);
|
|
663
|
-
}
|
|
664
|
-
// Store result in real context
|
|
665
|
-
this.realContext[stepName] = result;
|
|
666
|
-
// Resolve all promises for this step
|
|
667
|
-
this.promiseManager.resolveStep(stepName, result);
|
|
668
|
-
// Update progress - calculate latency metrics
|
|
669
|
-
latency.completedAt = Date.now();
|
|
670
|
-
latency.totalTime = latency.completedAt - latency.queuedAt;
|
|
671
|
-
latency.executionTime = latency.completedAt - latency.startedAt;
|
|
672
|
-
latency.queueTime = latency.startedAt - latency.queuedAt;
|
|
673
|
-
latency.waitTime = 0; // TODO: More sophisticated wait time tracking
|
|
674
|
-
stepProgress.status = 'completed';
|
|
675
|
-
stepProgress.endTime = latency.completedAt;
|
|
676
|
-
stepProgress.duration = latency.totalTime;
|
|
677
|
-
stepProgress.queueTime = latency.queueTime;
|
|
678
|
-
stepProgress.executionTime = latency.executionTime;
|
|
679
|
-
stepProgress.waitTime = latency.waitTime;
|
|
680
|
-
stepProgress.result = result;
|
|
681
|
-
stepProgress.dependencies = this.dependencyTracker.getDependencies(stepName);
|
|
682
|
-
this.graph.nodes.get(stepName).status = 'completed';
|
|
683
|
-
this.graph.nodes.get(stepName).endTime = latency.completedAt;
|
|
684
|
-
this.progress.completedSteps++;
|
|
685
|
-
this.emit('step:end', stepName, stepProgress);
|
|
686
|
-
}
|
|
687
|
-
catch (error) {
|
|
688
|
-
// Handle error
|
|
689
|
-
latency.completedAt = Date.now();
|
|
690
|
-
latency.totalTime = latency.completedAt - latency.queuedAt;
|
|
691
|
-
latency.executionTime = latency.completedAt - latency.startedAt;
|
|
692
|
-
latency.queueTime = latency.startedAt - latency.queuedAt;
|
|
693
|
-
latency.waitTime = 0;
|
|
694
|
-
stepProgress.status = 'failed';
|
|
695
|
-
stepProgress.endTime = latency.completedAt;
|
|
696
|
-
stepProgress.duration = latency.totalTime;
|
|
697
|
-
stepProgress.dependencies = this.dependencyTracker.getDependencies(stepName);
|
|
698
|
-
const stepError = {
|
|
699
|
-
message: error.message,
|
|
700
|
-
stack: error.stack || '',
|
|
701
|
-
context: { ...this.realContext },
|
|
702
|
-
};
|
|
703
|
-
stepProgress.error = stepError;
|
|
704
|
-
this.graph.nodes.get(stepName).status = 'failed';
|
|
705
|
-
this.graph.nodes.get(stepName).endTime = latency.completedAt;
|
|
706
|
-
this.graph.nodes.get(stepName).error = error;
|
|
707
|
-
this.progress.failedSteps++;
|
|
708
|
-
this.emit('step:error', stepName, error, stepProgress);
|
|
709
|
-
this.log?.error?.({ err: error }, `step ${stepName} failed`);
|
|
710
|
-
// Reject promises for this step
|
|
711
|
-
this.promiseManager.reject(stepName, error);
|
|
712
|
-
throw error;
|
|
713
|
-
}
|
|
714
|
-
};
|
|
715
|
-
// If we have test context, wrap in nested test
|
|
716
|
-
if (this.testContext && parentTestContext) {
|
|
717
|
-
await this.queue.add(async () => {
|
|
718
|
-
try {
|
|
719
|
-
await this.testContext.test.call(parentTestContext, stepName, async (stepT) => {
|
|
720
|
-
await executeStepFn(stepT);
|
|
721
|
-
});
|
|
722
|
-
}
|
|
723
|
-
catch {
|
|
724
|
-
// Error already handled in executeStepFn, don't rethrow to break the queue
|
|
725
|
-
// The test framework will report it
|
|
726
|
-
}
|
|
727
|
-
});
|
|
728
|
-
}
|
|
729
|
-
else if (this.testContext && groupPath.length === 0) {
|
|
730
|
-
// Top-level step with test context
|
|
731
|
-
await this.queue.add(async () => {
|
|
732
|
-
try {
|
|
733
|
-
await this.testContext.test(stepName, async (stepT) => {
|
|
734
|
-
await executeStepFn(stepT);
|
|
735
|
-
});
|
|
736
|
-
}
|
|
737
|
-
catch {
|
|
738
|
-
// Error already handled in executeStepFn, don't rethrow to break the queue
|
|
739
|
-
}
|
|
740
|
-
});
|
|
741
|
-
}
|
|
742
|
-
else {
|
|
743
|
-
// No test context or not at top level
|
|
744
|
-
await this.queue.add(executeStepFn);
|
|
745
|
-
}
|
|
746
|
-
}
|
|
747
|
-
/**
|
|
748
|
-
* Counts total number of steps (including nested)
|
|
749
|
-
*/
|
|
750
|
-
_countSteps(steps) {
|
|
751
|
-
let count = 0;
|
|
752
|
-
for (const step of steps) {
|
|
753
|
-
if (Array.isArray(step)) {
|
|
754
|
-
count += this._countSteps(step);
|
|
755
|
-
}
|
|
756
|
-
else if (typeof step === 'function') {
|
|
757
|
-
count++;
|
|
758
|
-
}
|
|
759
|
-
}
|
|
760
|
-
return count;
|
|
761
|
-
}
|
|
762
|
-
/**
|
|
763
|
-
* Collects all step names (including nested)
|
|
764
|
-
*/
|
|
765
|
-
_collectStepNames(steps) {
|
|
766
|
-
const stepNames = new Set();
|
|
767
|
-
for (const step of steps) {
|
|
768
|
-
if (Array.isArray(step)) {
|
|
769
|
-
// Skip checkpoint markers (string-only arrays) — they are not steps
|
|
770
|
-
if (step.every(s => typeof s === 'string'))
|
|
771
|
-
continue;
|
|
772
|
-
// Recursively collect from nested step groups
|
|
773
|
-
const nested = this._collectStepNames(step);
|
|
774
|
-
nested.forEach(name => stepNames.add(name));
|
|
775
|
-
}
|
|
776
|
-
else if (typeof step === 'function') {
|
|
777
|
-
const stepName = step.name || 'anonymous';
|
|
778
|
-
stepNames.add(stepName);
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
|
-
return stepNames;
|
|
782
|
-
}
|
|
783
|
-
/**
|
|
784
|
-
* Checks for duplicate step names and throws an error if found
|
|
785
|
-
*/
|
|
786
|
-
_checkForDuplicateStepName(stepName) {
|
|
787
|
-
if (this.stepNamesUsed.has(stepName)) {
|
|
788
|
-
throw new Error(`Duplicate step name detected: "${stepName}". ` +
|
|
789
|
-
`Each step must have a unique function name within the same test context.`);
|
|
790
|
-
}
|
|
791
|
-
this.stepNamesUsed.add(stepName);
|
|
792
|
-
}
|
|
793
|
-
/**
|
|
794
|
-
* Applies the chain-level `mask` (and optional per-call `extraPaths`) to
|
|
795
|
-
* `value`. Returns the original reference unchanged when no masking is
|
|
796
|
-
* configured. Falls back to the deprecated `maskFn` when supplied.
|
|
797
|
-
*/
|
|
798
|
-
_applyMask(value, extraPaths) {
|
|
799
|
-
const paths = [...(this.config.mask ?? []), ...(extraPaths ?? [])];
|
|
800
|
-
if (!paths.length && !this.config.maskFn)
|
|
801
|
-
return value;
|
|
802
|
-
if (this.config.maskFn)
|
|
803
|
-
return this.config.maskFn(value, paths);
|
|
804
|
-
return maskPaths(value, paths);
|
|
805
|
-
}
|
|
806
|
-
/**
|
|
807
|
-
* Gets the current progress snapshot
|
|
808
|
-
*/
|
|
809
|
-
getProgress() {
|
|
810
|
-
return this.progress;
|
|
811
|
-
}
|
|
812
|
-
/**
|
|
813
|
-
* Gets the dependency graph
|
|
814
|
-
*/
|
|
815
|
-
getDependencyGraph() {
|
|
816
|
-
return this.graph;
|
|
817
|
-
}
|
|
818
|
-
/**
|
|
819
|
-
* Gets latency metrics
|
|
820
|
-
*/
|
|
821
|
-
getLatencyReport() {
|
|
822
|
-
const totalDuration = this.progress.endTime
|
|
823
|
-
? this.progress.endTime - this.progress.startTime
|
|
824
|
-
: 0;
|
|
825
|
-
// Calculate critical path
|
|
826
|
-
const criticalPath = this._calculateCriticalPath();
|
|
827
|
-
// Calculate parallel efficiency
|
|
828
|
-
const totalStepTime = Array.from(this.latencyMetrics.values()).reduce((sum, l) => sum + l.executionTime, 0);
|
|
829
|
-
const parallelEfficiency = totalDuration > 0 ? totalStepTime / totalDuration : 0;
|
|
830
|
-
// Identify bottlenecks
|
|
831
|
-
const bottlenecks = this._identifyBottlenecks();
|
|
832
|
-
return {
|
|
833
|
-
testName: this.progress.testName,
|
|
834
|
-
totalDuration,
|
|
835
|
-
steps: this.latencyMetrics,
|
|
836
|
-
criticalPath,
|
|
837
|
-
parallelEfficiency,
|
|
838
|
-
bottlenecks,
|
|
839
|
-
};
|
|
840
|
-
}
|
|
841
|
-
/**
|
|
842
|
-
* Calculates the critical path (longest dependency chain)
|
|
843
|
-
*/
|
|
844
|
-
_calculateCriticalPath() {
|
|
845
|
-
// Build adjacency list
|
|
846
|
-
const adjacency = new Map();
|
|
847
|
-
for (const edge of this.graph.edges) {
|
|
848
|
-
if (!adjacency.has(edge.to)) {
|
|
849
|
-
adjacency.set(edge.to, []);
|
|
850
|
-
}
|
|
851
|
-
adjacency.get(edge.to).push(edge.from);
|
|
852
|
-
}
|
|
853
|
-
// Find longest path using DFS
|
|
854
|
-
const visited = new Set();
|
|
855
|
-
let longestPath = [];
|
|
856
|
-
const dfs = (node, path) => {
|
|
857
|
-
if (visited.has(node))
|
|
858
|
-
return;
|
|
859
|
-
visited.add(node);
|
|
860
|
-
const newPath = [...path, node];
|
|
861
|
-
const children = adjacency.get(node) || [];
|
|
862
|
-
if (children.length === 0) {
|
|
863
|
-
// Leaf node - check if this is the longest path
|
|
864
|
-
if (newPath.length > longestPath.length) {
|
|
865
|
-
longestPath = newPath;
|
|
866
|
-
}
|
|
867
|
-
}
|
|
868
|
-
else {
|
|
869
|
-
for (const child of children) {
|
|
870
|
-
dfs(child, newPath);
|
|
871
|
-
}
|
|
872
|
-
}
|
|
873
|
-
visited.delete(node);
|
|
874
|
-
};
|
|
875
|
-
// Start DFS from all roots (nodes with no dependencies)
|
|
876
|
-
const allNodes = new Set(this.graph.nodes.keys());
|
|
877
|
-
const dependentNodes = new Set(this.graph.edges.map(e => e.from));
|
|
878
|
-
const roots = Array.from(allNodes).filter(n => !dependentNodes.has(n));
|
|
879
|
-
for (const root of roots) {
|
|
880
|
-
dfs(root, []);
|
|
881
|
-
}
|
|
882
|
-
return longestPath.reverse(); // Reverse to get correct order
|
|
883
|
-
}
|
|
884
|
-
/**
|
|
885
|
-
* Identifies bottleneck steps that blocked many other steps
|
|
886
|
-
*/
|
|
887
|
-
_identifyBottlenecks() {
|
|
888
|
-
const bottlenecks = new Map();
|
|
889
|
-
// Count how many steps each step blocks
|
|
890
|
-
for (const edge of this.graph.edges) {
|
|
891
|
-
if (!bottlenecks.has(edge.to)) {
|
|
892
|
-
bottlenecks.set(edge.to, new Set());
|
|
893
|
-
}
|
|
894
|
-
bottlenecks.get(edge.to).add(edge.from);
|
|
895
|
-
}
|
|
896
|
-
// Sort by number of blocked steps
|
|
897
|
-
const result = Array.from(bottlenecks.entries())
|
|
898
|
-
.map(([stepName, blockedSteps]) => ({
|
|
899
|
-
stepName,
|
|
900
|
-
executionTime: this.latencyMetrics.get(stepName)?.executionTime || 0,
|
|
901
|
-
blockedSteps: Array.from(blockedSteps),
|
|
902
|
-
}))
|
|
903
|
-
.sort((a, b) => b.blockedSteps.length - a.blockedSteps.length)
|
|
904
|
-
.slice(0, 5); // Top 5 bottlenecks
|
|
905
|
-
return result;
|
|
906
|
-
}
|
|
907
|
-
/**
|
|
908
|
-
* Type-safe event emitter
|
|
909
|
-
*/
|
|
910
|
-
on(event, handler) {
|
|
911
|
-
return super.on(event, handler);
|
|
912
|
-
}
|
|
913
|
-
emit(event, ...args) {
|
|
914
|
-
return super.emit(event, ...args);
|
|
915
|
-
}
|
|
916
|
-
}
|
|
917
|
-
//# sourceMappingURL=index.js.map
|