@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.
Files changed (42) hide show
  1. package/README.md +547 -0
  2. package/__tests__/framework.test.ts +21 -0
  3. package/package.json +61 -0
  4. package/src/fixtures/agent-fixtures.ts +793 -0
  5. package/src/fixtures/agents.ts +212 -0
  6. package/src/fixtures/configurations.ts +491 -0
  7. package/src/fixtures/index.ts +21 -0
  8. package/src/fixtures/mcp-fixtures.ts +1030 -0
  9. package/src/fixtures/memory-entries.ts +328 -0
  10. package/src/fixtures/memory-fixtures.ts +750 -0
  11. package/src/fixtures/swarm-fixtures.ts +837 -0
  12. package/src/fixtures/tasks.ts +309 -0
  13. package/src/helpers/assertion-helpers.ts +616 -0
  14. package/src/helpers/assertions.ts +286 -0
  15. package/src/helpers/create-mock.ts +200 -0
  16. package/src/helpers/index.ts +182 -0
  17. package/src/helpers/mock-factory.ts +711 -0
  18. package/src/helpers/setup-teardown.ts +678 -0
  19. package/src/helpers/swarm-instance.ts +326 -0
  20. package/src/helpers/test-application.ts +310 -0
  21. package/src/helpers/test-utils.ts +670 -0
  22. package/src/index.ts +232 -0
  23. package/src/mocks/index.ts +29 -0
  24. package/src/mocks/mock-mcp-client.ts +723 -0
  25. package/src/mocks/mock-services.ts +793 -0
  26. package/src/regression/api-contract.ts +473 -0
  27. package/src/regression/index.ts +46 -0
  28. package/src/regression/integration-regression.ts +416 -0
  29. package/src/regression/performance-baseline.ts +356 -0
  30. package/src/regression/regression-runner.ts +339 -0
  31. package/src/regression/security-regression.ts +331 -0
  32. package/src/setup.ts +127 -0
  33. package/src/v2-compat/api-compat.test.ts +590 -0
  34. package/src/v2-compat/cli-compat.test.ts +484 -0
  35. package/src/v2-compat/compatibility-validator.ts +1072 -0
  36. package/src/v2-compat/hooks-compat.test.ts +602 -0
  37. package/src/v2-compat/index.ts +58 -0
  38. package/src/v2-compat/mcp-compat.test.ts +557 -0
  39. package/src/v2-compat/report-generator.ts +441 -0
  40. package/tmp.json +0 -0
  41. package/tsconfig.json +20 -0
  42. 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
+ }