@sparkleideas/testing 3.0.0-alpha.7
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 +547 -0
- package/__tests__/framework.test.ts +21 -0
- package/package.json +61 -0
- package/src/fixtures/agent-fixtures.ts +793 -0
- package/src/fixtures/agents.ts +212 -0
- package/src/fixtures/configurations.ts +491 -0
- package/src/fixtures/index.ts +21 -0
- package/src/fixtures/mcp-fixtures.ts +1030 -0
- package/src/fixtures/memory-entries.ts +328 -0
- package/src/fixtures/memory-fixtures.ts +750 -0
- package/src/fixtures/swarm-fixtures.ts +837 -0
- package/src/fixtures/tasks.ts +309 -0
- package/src/helpers/assertion-helpers.ts +616 -0
- package/src/helpers/assertions.ts +286 -0
- package/src/helpers/create-mock.ts +200 -0
- package/src/helpers/index.ts +182 -0
- package/src/helpers/mock-factory.ts +711 -0
- package/src/helpers/setup-teardown.ts +678 -0
- package/src/helpers/swarm-instance.ts +326 -0
- package/src/helpers/test-application.ts +310 -0
- package/src/helpers/test-utils.ts +670 -0
- package/src/index.ts +232 -0
- package/src/mocks/index.ts +29 -0
- package/src/mocks/mock-mcp-client.ts +723 -0
- package/src/mocks/mock-services.ts +793 -0
- package/src/regression/api-contract.ts +473 -0
- package/src/regression/index.ts +46 -0
- package/src/regression/integration-regression.ts +416 -0
- package/src/regression/performance-baseline.ts +356 -0
- package/src/regression/regression-runner.ts +339 -0
- package/src/regression/security-regression.ts +331 -0
- package/src/setup.ts +127 -0
- package/src/v2-compat/api-compat.test.ts +590 -0
- package/src/v2-compat/cli-compat.test.ts +484 -0
- package/src/v2-compat/compatibility-validator.ts +1072 -0
- package/src/v2-compat/hooks-compat.test.ts +602 -0
- package/src/v2-compat/index.ts +58 -0
- package/src/v2-compat/mcp-compat.test.ts +557 -0
- package/src/v2-compat/report-generator.ts +441 -0
- package/tmp.json +0 -0
- package/tsconfig.json +20 -0
- package/vitest.config.ts +12 -0
|
@@ -0,0 +1,670 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @sparkleideas/testing - Test Utilities
|
|
3
|
+
*
|
|
4
|
+
* Common test utilities for async operations, timing, retries, and more.
|
|
5
|
+
* Designed for robust V3 module testing.
|
|
6
|
+
*/
|
|
7
|
+
import { vi } from 'vitest';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Wait for a condition to be true with timeout
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* await waitFor(() => element.isVisible(), { timeout: 5000 });
|
|
14
|
+
*/
|
|
15
|
+
export async function waitFor<T>(
|
|
16
|
+
condition: () => T | Promise<T>,
|
|
17
|
+
options: WaitForOptions = {}
|
|
18
|
+
): Promise<T> {
|
|
19
|
+
const {
|
|
20
|
+
timeout = 5000,
|
|
21
|
+
interval = 50,
|
|
22
|
+
timeoutMessage = 'Condition not met within timeout',
|
|
23
|
+
} = options;
|
|
24
|
+
|
|
25
|
+
const startTime = Date.now();
|
|
26
|
+
|
|
27
|
+
while (true) {
|
|
28
|
+
try {
|
|
29
|
+
const result = await condition();
|
|
30
|
+
if (result) {
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
33
|
+
} catch (error) {
|
|
34
|
+
// Condition threw, continue waiting
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (Date.now() - startTime >= timeout) {
|
|
38
|
+
throw new Error(timeoutMessage);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
await sleep(interval);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Options for waitFor utility
|
|
47
|
+
*/
|
|
48
|
+
export interface WaitForOptions {
|
|
49
|
+
timeout?: number;
|
|
50
|
+
interval?: number;
|
|
51
|
+
timeoutMessage?: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Wait until a value changes
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* await waitUntilChanged(() => counter.value, { from: 0 });
|
|
59
|
+
*/
|
|
60
|
+
export async function waitUntilChanged<T>(
|
|
61
|
+
getValue: () => T | Promise<T>,
|
|
62
|
+
options: WaitUntilChangedOptions<T> = {}
|
|
63
|
+
): Promise<T> {
|
|
64
|
+
const { from, timeout = 5000, interval = 50 } = options;
|
|
65
|
+
const initialValue = from ?? await getValue();
|
|
66
|
+
const startTime = Date.now();
|
|
67
|
+
|
|
68
|
+
while (true) {
|
|
69
|
+
const currentValue = await getValue();
|
|
70
|
+
if (currentValue !== initialValue) {
|
|
71
|
+
return currentValue;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (Date.now() - startTime >= timeout) {
|
|
75
|
+
throw new Error(`Value did not change from ${String(initialValue)} within timeout`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
await sleep(interval);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Options for waitUntilChanged utility
|
|
84
|
+
*/
|
|
85
|
+
export interface WaitUntilChangedOptions<T> {
|
|
86
|
+
from?: T;
|
|
87
|
+
timeout?: number;
|
|
88
|
+
interval?: number;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Retry an operation with exponential backoff
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* const result = await retry(
|
|
96
|
+
* async () => await fetchData(),
|
|
97
|
+
* { maxAttempts: 3, backoff: 100 }
|
|
98
|
+
* );
|
|
99
|
+
*/
|
|
100
|
+
export async function retry<T>(
|
|
101
|
+
operation: () => Promise<T>,
|
|
102
|
+
options: RetryOptions = {}
|
|
103
|
+
): Promise<T> {
|
|
104
|
+
const {
|
|
105
|
+
maxAttempts = 3,
|
|
106
|
+
backoff = 100,
|
|
107
|
+
maxBackoff = 10000,
|
|
108
|
+
exponential = true,
|
|
109
|
+
onError,
|
|
110
|
+
shouldRetry = () => true,
|
|
111
|
+
} = options;
|
|
112
|
+
|
|
113
|
+
let lastError: Error | undefined;
|
|
114
|
+
let currentBackoff = backoff;
|
|
115
|
+
|
|
116
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
117
|
+
try {
|
|
118
|
+
return await operation();
|
|
119
|
+
} catch (error) {
|
|
120
|
+
lastError = error as Error;
|
|
121
|
+
|
|
122
|
+
if (attempt === maxAttempts || !shouldRetry(lastError, attempt)) {
|
|
123
|
+
throw lastError;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
onError?.(lastError, attempt);
|
|
127
|
+
|
|
128
|
+
await sleep(currentBackoff);
|
|
129
|
+
|
|
130
|
+
if (exponential) {
|
|
131
|
+
currentBackoff = Math.min(currentBackoff * 2, maxBackoff);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
throw lastError;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Options for retry utility
|
|
141
|
+
*/
|
|
142
|
+
export interface RetryOptions {
|
|
143
|
+
maxAttempts?: number;
|
|
144
|
+
backoff?: number;
|
|
145
|
+
maxBackoff?: number;
|
|
146
|
+
exponential?: boolean;
|
|
147
|
+
onError?: (error: Error, attempt: number) => void;
|
|
148
|
+
shouldRetry?: (error: Error, attempt: number) => boolean;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Wrap an operation with a timeout
|
|
153
|
+
*
|
|
154
|
+
* @example
|
|
155
|
+
* const result = await withTimeout(
|
|
156
|
+
* async () => await longRunningOperation(),
|
|
157
|
+
* 5000
|
|
158
|
+
* );
|
|
159
|
+
*/
|
|
160
|
+
export async function withTimeout<T>(
|
|
161
|
+
operation: () => Promise<T>,
|
|
162
|
+
timeoutMs: number,
|
|
163
|
+
timeoutMessage?: string
|
|
164
|
+
): Promise<T> {
|
|
165
|
+
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
166
|
+
const timer = setTimeout(() => {
|
|
167
|
+
reject(new TimeoutError(timeoutMessage ?? `Operation timed out after ${timeoutMs}ms`));
|
|
168
|
+
}, timeoutMs);
|
|
169
|
+
|
|
170
|
+
// Cleanup timer if operation completes first
|
|
171
|
+
operation().finally(() => clearTimeout(timer));
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
return Promise.race([operation(), timeoutPromise]);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Custom timeout error
|
|
179
|
+
*/
|
|
180
|
+
export class TimeoutError extends Error {
|
|
181
|
+
constructor(message: string) {
|
|
182
|
+
super(message);
|
|
183
|
+
this.name = 'TimeoutError';
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Sleep for a specified duration
|
|
189
|
+
*
|
|
190
|
+
* @example
|
|
191
|
+
* await sleep(1000); // Sleep for 1 second
|
|
192
|
+
*/
|
|
193
|
+
export function sleep(ms: number): Promise<void> {
|
|
194
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Create a deferred promise that can be resolved/rejected externally
|
|
199
|
+
*
|
|
200
|
+
* @example
|
|
201
|
+
* const deferred = createDeferred<string>();
|
|
202
|
+
* setTimeout(() => deferred.resolve('done'), 1000);
|
|
203
|
+
* const result = await deferred.promise;
|
|
204
|
+
*/
|
|
205
|
+
export function createDeferred<T>(): Deferred<T> {
|
|
206
|
+
let resolve!: (value: T) => void;
|
|
207
|
+
let reject!: (error: Error) => void;
|
|
208
|
+
|
|
209
|
+
const promise = new Promise<T>((res, rej) => {
|
|
210
|
+
resolve = res;
|
|
211
|
+
reject = rej;
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
return { promise, resolve, reject };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Deferred promise interface
|
|
219
|
+
*/
|
|
220
|
+
export interface Deferred<T> {
|
|
221
|
+
promise: Promise<T>;
|
|
222
|
+
resolve: (value: T) => void;
|
|
223
|
+
reject: (error: Error) => void;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Run operations in parallel with concurrency limit
|
|
228
|
+
*
|
|
229
|
+
* @example
|
|
230
|
+
* const results = await parallelLimit(
|
|
231
|
+
* items.map(item => () => processItem(item)),
|
|
232
|
+
* 5 // max 5 concurrent operations
|
|
233
|
+
* );
|
|
234
|
+
*/
|
|
235
|
+
export async function parallelLimit<T>(
|
|
236
|
+
operations: Array<() => Promise<T>>,
|
|
237
|
+
limit: number
|
|
238
|
+
): Promise<T[]> {
|
|
239
|
+
const results: T[] = [];
|
|
240
|
+
const executing: Set<Promise<void>> = new Set();
|
|
241
|
+
|
|
242
|
+
for (const operation of operations) {
|
|
243
|
+
const promise = (async () => {
|
|
244
|
+
const result = await operation();
|
|
245
|
+
results.push(result);
|
|
246
|
+
})();
|
|
247
|
+
|
|
248
|
+
executing.add(promise);
|
|
249
|
+
promise.finally(() => executing.delete(promise));
|
|
250
|
+
|
|
251
|
+
if (executing.size >= limit) {
|
|
252
|
+
await Promise.race(executing);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
await Promise.all(executing);
|
|
257
|
+
return results;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Measure execution time of an operation
|
|
262
|
+
*
|
|
263
|
+
* @example
|
|
264
|
+
* const { result, duration } = await measureTime(async () => {
|
|
265
|
+
* return await expensiveOperation();
|
|
266
|
+
* });
|
|
267
|
+
*/
|
|
268
|
+
export async function measureTime<T>(
|
|
269
|
+
operation: () => Promise<T>
|
|
270
|
+
): Promise<{ result: T; duration: number }> {
|
|
271
|
+
const start = performance.now();
|
|
272
|
+
const result = await operation();
|
|
273
|
+
const duration = performance.now() - start;
|
|
274
|
+
return { result, duration };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Create a mock clock for time-dependent tests
|
|
279
|
+
*
|
|
280
|
+
* @example
|
|
281
|
+
* const clock = createMockClock();
|
|
282
|
+
* clock.install();
|
|
283
|
+
* // ... tests with controlled time
|
|
284
|
+
* clock.uninstall();
|
|
285
|
+
*/
|
|
286
|
+
export function createMockClock(): MockClock {
|
|
287
|
+
let installed = false;
|
|
288
|
+
let currentTime = Date.now();
|
|
289
|
+
|
|
290
|
+
return {
|
|
291
|
+
install() {
|
|
292
|
+
if (installed) return;
|
|
293
|
+
vi.useFakeTimers();
|
|
294
|
+
vi.setSystemTime(currentTime);
|
|
295
|
+
installed = true;
|
|
296
|
+
},
|
|
297
|
+
|
|
298
|
+
uninstall() {
|
|
299
|
+
if (!installed) return;
|
|
300
|
+
vi.useRealTimers();
|
|
301
|
+
installed = false;
|
|
302
|
+
},
|
|
303
|
+
|
|
304
|
+
tick(ms: number) {
|
|
305
|
+
if (!installed) {
|
|
306
|
+
throw new Error('Clock not installed. Call install() first.');
|
|
307
|
+
}
|
|
308
|
+
currentTime += ms;
|
|
309
|
+
vi.advanceTimersByTime(ms);
|
|
310
|
+
},
|
|
311
|
+
|
|
312
|
+
setTime(time: number | Date) {
|
|
313
|
+
currentTime = typeof time === 'number' ? time : time.getTime();
|
|
314
|
+
if (installed) {
|
|
315
|
+
vi.setSystemTime(currentTime);
|
|
316
|
+
}
|
|
317
|
+
},
|
|
318
|
+
|
|
319
|
+
getTime() {
|
|
320
|
+
return currentTime;
|
|
321
|
+
},
|
|
322
|
+
|
|
323
|
+
runAllTimers() {
|
|
324
|
+
if (!installed) {
|
|
325
|
+
throw new Error('Clock not installed. Call install() first.');
|
|
326
|
+
}
|
|
327
|
+
vi.runAllTimers();
|
|
328
|
+
},
|
|
329
|
+
|
|
330
|
+
runPendingTimers() {
|
|
331
|
+
if (!installed) {
|
|
332
|
+
throw new Error('Clock not installed. Call install() first.');
|
|
333
|
+
}
|
|
334
|
+
vi.runOnlyPendingTimers();
|
|
335
|
+
},
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Mock clock interface
|
|
341
|
+
*/
|
|
342
|
+
export interface MockClock {
|
|
343
|
+
install(): void;
|
|
344
|
+
uninstall(): void;
|
|
345
|
+
tick(ms: number): void;
|
|
346
|
+
setTime(time: number | Date): void;
|
|
347
|
+
getTime(): number;
|
|
348
|
+
runAllTimers(): void;
|
|
349
|
+
runPendingTimers(): void;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Create an event emitter for testing
|
|
354
|
+
*
|
|
355
|
+
* @example
|
|
356
|
+
* const emitter = createTestEmitter<{ message: string }>();
|
|
357
|
+
* const handler = vi.fn();
|
|
358
|
+
* emitter.on('message', handler);
|
|
359
|
+
* emitter.emit('message', 'hello');
|
|
360
|
+
*/
|
|
361
|
+
export function createTestEmitter<T extends Record<string, unknown>>(): TestEmitter<T> {
|
|
362
|
+
const listeners = new Map<keyof T, Set<(data: unknown) => void>>();
|
|
363
|
+
|
|
364
|
+
return {
|
|
365
|
+
on<K extends keyof T>(event: K, handler: (data: T[K]) => void): () => void {
|
|
366
|
+
if (!listeners.has(event)) {
|
|
367
|
+
listeners.set(event, new Set());
|
|
368
|
+
}
|
|
369
|
+
listeners.get(event)!.add(handler as (data: unknown) => void);
|
|
370
|
+
|
|
371
|
+
return () => {
|
|
372
|
+
listeners.get(event)?.delete(handler as (data: unknown) => void);
|
|
373
|
+
};
|
|
374
|
+
},
|
|
375
|
+
|
|
376
|
+
once<K extends keyof T>(event: K, handler: (data: T[K]) => void): () => void {
|
|
377
|
+
const wrappedHandler = (data: T[K]) => {
|
|
378
|
+
this.off(event, wrappedHandler);
|
|
379
|
+
handler(data);
|
|
380
|
+
};
|
|
381
|
+
return this.on(event, wrappedHandler);
|
|
382
|
+
},
|
|
383
|
+
|
|
384
|
+
off<K extends keyof T>(event: K, handler: (data: T[K]) => void): void {
|
|
385
|
+
listeners.get(event)?.delete(handler as (data: unknown) => void);
|
|
386
|
+
},
|
|
387
|
+
|
|
388
|
+
emit<K extends keyof T>(event: K, data: T[K]): void {
|
|
389
|
+
listeners.get(event)?.forEach(handler => handler(data));
|
|
390
|
+
},
|
|
391
|
+
|
|
392
|
+
removeAllListeners(event?: keyof T): void {
|
|
393
|
+
if (event) {
|
|
394
|
+
listeners.delete(event);
|
|
395
|
+
} else {
|
|
396
|
+
listeners.clear();
|
|
397
|
+
}
|
|
398
|
+
},
|
|
399
|
+
|
|
400
|
+
listenerCount(event: keyof T): number {
|
|
401
|
+
return listeners.get(event)?.size ?? 0;
|
|
402
|
+
},
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Test emitter interface
|
|
408
|
+
*/
|
|
409
|
+
export interface TestEmitter<T extends Record<string, unknown>> {
|
|
410
|
+
on<K extends keyof T>(event: K, handler: (data: T[K]) => void): () => void;
|
|
411
|
+
once<K extends keyof T>(event: K, handler: (data: T[K]) => void): () => void;
|
|
412
|
+
off<K extends keyof T>(event: K, handler: (data: T[K]) => void): void;
|
|
413
|
+
emit<K extends keyof T>(event: K, data: T[K]): void;
|
|
414
|
+
removeAllListeners(event?: keyof T): void;
|
|
415
|
+
listenerCount(event: keyof T): number;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Create a test spy that records all calls
|
|
420
|
+
*
|
|
421
|
+
* @example
|
|
422
|
+
* const spy = createCallSpy();
|
|
423
|
+
* myFunction = spy.wrap(myFunction);
|
|
424
|
+
* // ... use myFunction
|
|
425
|
+
* expect(spy.calls).toHaveLength(3);
|
|
426
|
+
*/
|
|
427
|
+
export function createCallSpy<T extends (...args: unknown[]) => unknown>(): CallSpy<T> {
|
|
428
|
+
const calls: Array<{ args: Parameters<T>; result?: ReturnType<T>; error?: Error; timestamp: number }> = [];
|
|
429
|
+
|
|
430
|
+
return {
|
|
431
|
+
calls,
|
|
432
|
+
|
|
433
|
+
wrap(fn: T): T {
|
|
434
|
+
return ((...args: Parameters<T>) => {
|
|
435
|
+
const call = { args, timestamp: Date.now() } as typeof calls[number];
|
|
436
|
+
calls.push(call);
|
|
437
|
+
|
|
438
|
+
try {
|
|
439
|
+
const result = fn(...args);
|
|
440
|
+
call.result = result as ReturnType<T>;
|
|
441
|
+
return result;
|
|
442
|
+
} catch (error) {
|
|
443
|
+
call.error = error as Error;
|
|
444
|
+
throw error;
|
|
445
|
+
}
|
|
446
|
+
}) as T;
|
|
447
|
+
},
|
|
448
|
+
|
|
449
|
+
clear() {
|
|
450
|
+
calls.length = 0;
|
|
451
|
+
},
|
|
452
|
+
|
|
453
|
+
getLastCall() {
|
|
454
|
+
return calls[calls.length - 1];
|
|
455
|
+
},
|
|
456
|
+
|
|
457
|
+
getCallCount() {
|
|
458
|
+
return calls.length;
|
|
459
|
+
},
|
|
460
|
+
|
|
461
|
+
wasCalledWith(...args: Partial<Parameters<T>>): boolean {
|
|
462
|
+
return calls.some(call =>
|
|
463
|
+
args.every((arg, i) => arg === undefined || call.args[i] === arg)
|
|
464
|
+
);
|
|
465
|
+
},
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Call spy interface
|
|
471
|
+
*/
|
|
472
|
+
export interface CallSpy<T extends (...args: unknown[]) => unknown> {
|
|
473
|
+
calls: Array<{ args: Parameters<T>; result?: ReturnType<T>; error?: Error; timestamp: number }>;
|
|
474
|
+
wrap(fn: T): T;
|
|
475
|
+
clear(): void;
|
|
476
|
+
getLastCall(): { args: Parameters<T>; result?: ReturnType<T>; error?: Error; timestamp: number } | undefined;
|
|
477
|
+
getCallCount(): number;
|
|
478
|
+
wasCalledWith(...args: Partial<Parameters<T>>): boolean;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Create a mock stream for testing streaming operations
|
|
483
|
+
*
|
|
484
|
+
* @example
|
|
485
|
+
* const stream = createMockStream(['chunk1', 'chunk2', 'chunk3']);
|
|
486
|
+
* for await (const chunk of stream) {
|
|
487
|
+
* console.log(chunk);
|
|
488
|
+
* }
|
|
489
|
+
*/
|
|
490
|
+
export function createMockStream<T>(
|
|
491
|
+
chunks: T[],
|
|
492
|
+
options: MockStreamOptions = {}
|
|
493
|
+
): AsyncIterable<T> {
|
|
494
|
+
const { delayMs = 0, errorAt, errorMessage = 'Stream error' } = options;
|
|
495
|
+
|
|
496
|
+
return {
|
|
497
|
+
async *[Symbol.asyncIterator]() {
|
|
498
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
499
|
+
if (errorAt !== undefined && i === errorAt) {
|
|
500
|
+
throw new Error(errorMessage);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
if (delayMs > 0) {
|
|
504
|
+
await sleep(delayMs);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
yield chunks[i];
|
|
508
|
+
}
|
|
509
|
+
},
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Mock stream options
|
|
515
|
+
*/
|
|
516
|
+
export interface MockStreamOptions {
|
|
517
|
+
delayMs?: number;
|
|
518
|
+
errorAt?: number;
|
|
519
|
+
errorMessage?: string;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Collect all items from an async iterable
|
|
524
|
+
*
|
|
525
|
+
* @example
|
|
526
|
+
* const items = await collectStream(asyncGenerator());
|
|
527
|
+
*/
|
|
528
|
+
export async function collectStream<T>(stream: AsyncIterable<T>): Promise<T[]> {
|
|
529
|
+
const items: T[] = [];
|
|
530
|
+
for await (const item of stream) {
|
|
531
|
+
items.push(item);
|
|
532
|
+
}
|
|
533
|
+
return items;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Generate a unique ID for testing
|
|
538
|
+
*/
|
|
539
|
+
export function generateTestId(prefix: string = 'test'): string {
|
|
540
|
+
return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Create a test context that provides isolated test data
|
|
545
|
+
*
|
|
546
|
+
* @example
|
|
547
|
+
* const ctx = createTestContext();
|
|
548
|
+
* ctx.set('user', { id: 1, name: 'Test' });
|
|
549
|
+
* const user = ctx.get('user');
|
|
550
|
+
*/
|
|
551
|
+
export function createTestContext(): TestContext {
|
|
552
|
+
const data = new Map<string, unknown>();
|
|
553
|
+
|
|
554
|
+
return {
|
|
555
|
+
set<T>(key: string, value: T): void {
|
|
556
|
+
data.set(key, value);
|
|
557
|
+
},
|
|
558
|
+
|
|
559
|
+
get<T>(key: string): T | undefined {
|
|
560
|
+
return data.get(key) as T | undefined;
|
|
561
|
+
},
|
|
562
|
+
|
|
563
|
+
has(key: string): boolean {
|
|
564
|
+
return data.has(key);
|
|
565
|
+
},
|
|
566
|
+
|
|
567
|
+
delete(key: string): boolean {
|
|
568
|
+
return data.delete(key);
|
|
569
|
+
},
|
|
570
|
+
|
|
571
|
+
clear(): void {
|
|
572
|
+
data.clear();
|
|
573
|
+
},
|
|
574
|
+
|
|
575
|
+
keys(): string[] {
|
|
576
|
+
return Array.from(data.keys());
|
|
577
|
+
},
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Test context interface
|
|
583
|
+
*/
|
|
584
|
+
export interface TestContext {
|
|
585
|
+
set<T>(key: string, value: T): void;
|
|
586
|
+
get<T>(key: string): T | undefined;
|
|
587
|
+
has(key: string): boolean;
|
|
588
|
+
delete(key: string): boolean;
|
|
589
|
+
clear(): void;
|
|
590
|
+
keys(): string[];
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* Assert that a promise rejects with a specific error type
|
|
595
|
+
*
|
|
596
|
+
* @example
|
|
597
|
+
* await expectToReject(
|
|
598
|
+
* async () => await riskyOperation(),
|
|
599
|
+
* ValidationError
|
|
600
|
+
* );
|
|
601
|
+
*/
|
|
602
|
+
export async function expectToReject<T extends Error>(
|
|
603
|
+
operation: () => Promise<unknown>,
|
|
604
|
+
ErrorClass?: new (...args: unknown[]) => T
|
|
605
|
+
): Promise<T> {
|
|
606
|
+
try {
|
|
607
|
+
await operation();
|
|
608
|
+
throw new Error('Expected operation to reject, but it resolved');
|
|
609
|
+
} catch (error) {
|
|
610
|
+
if (ErrorClass && !(error instanceof ErrorClass)) {
|
|
611
|
+
throw new Error(
|
|
612
|
+
`Expected error to be instance of ${ErrorClass.name}, but got ${(error as Error).constructor.name}`
|
|
613
|
+
);
|
|
614
|
+
}
|
|
615
|
+
return error as T;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* Create a mock function with tracking capabilities
|
|
621
|
+
*/
|
|
622
|
+
export function createTrackedMock<T extends (...args: unknown[]) => unknown>(
|
|
623
|
+
implementation?: T
|
|
624
|
+
): TrackedMock<T> {
|
|
625
|
+
// Use type assertion to handle the optional implementation
|
|
626
|
+
const mock = implementation ? vi.fn(implementation) : vi.fn();
|
|
627
|
+
const calls: Array<{ args: Parameters<T>; result?: ReturnType<T>; error?: Error; duration: number }> = [];
|
|
628
|
+
|
|
629
|
+
const tracked = ((...args: Parameters<T>) => {
|
|
630
|
+
const start = performance.now();
|
|
631
|
+
const call: typeof calls[number] = { args, duration: 0 };
|
|
632
|
+
calls.push(call);
|
|
633
|
+
|
|
634
|
+
try {
|
|
635
|
+
const result = mock(...args);
|
|
636
|
+
call.result = result as ReturnType<T>;
|
|
637
|
+
call.duration = performance.now() - start;
|
|
638
|
+
return result;
|
|
639
|
+
} catch (error) {
|
|
640
|
+
call.error = error as Error;
|
|
641
|
+
call.duration = performance.now() - start;
|
|
642
|
+
throw error;
|
|
643
|
+
}
|
|
644
|
+
}) as TrackedMock<T>;
|
|
645
|
+
|
|
646
|
+
Object.assign(tracked, {
|
|
647
|
+
mock,
|
|
648
|
+
calls,
|
|
649
|
+
getAverageDuration: () => {
|
|
650
|
+
if (calls.length === 0) return 0;
|
|
651
|
+
return calls.reduce((sum, c) => sum + c.duration, 0) / calls.length;
|
|
652
|
+
},
|
|
653
|
+
getTotalDuration: () => calls.reduce((sum, c) => sum + c.duration, 0),
|
|
654
|
+
getErrors: () => calls.filter(c => c.error).map(c => c.error!),
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
return tracked;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
* Tracked mock interface
|
|
662
|
+
*/
|
|
663
|
+
export interface TrackedMock<T extends (...args: unknown[]) => unknown> {
|
|
664
|
+
(...args: Parameters<T>): ReturnType<T>;
|
|
665
|
+
mock: ReturnType<typeof vi.fn>;
|
|
666
|
+
calls: Array<{ args: Parameters<T>; result?: ReturnType<T>; error?: Error; duration: number }>;
|
|
667
|
+
getAverageDuration(): number;
|
|
668
|
+
getTotalDuration(): number;
|
|
669
|
+
getErrors(): Error[];
|
|
670
|
+
}
|