@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,616 @@
1
+ /**
2
+ * @sparkleideas/testing - Assertion Helpers
3
+ *
4
+ * Custom Vitest matchers and assertion utilities for V3 module testing.
5
+ * Implements London School TDD behavior verification patterns.
6
+ */
7
+ import { expect, type Mock, type ExpectStatic } from 'vitest';
8
+
9
+ /**
10
+ * Assert that a mock was called with arguments matching a pattern
11
+ *
12
+ * @example
13
+ * assertCalledWithPattern(mockFn, { userId: expect.any(String) });
14
+ */
15
+ export function assertCalledWithPattern(
16
+ mock: Mock,
17
+ pattern: Record<string, unknown> | unknown[]
18
+ ): void {
19
+ const calls = mock.mock.calls;
20
+ const matched = calls.some(call => {
21
+ if (Array.isArray(pattern)) {
22
+ return pattern.every((expected, i) => {
23
+ if (typeof expected === 'object' && expected !== null && 'asymmetricMatch' in expected) {
24
+ return (expected as { asymmetricMatch: (actual: unknown) => boolean }).asymmetricMatch(call[i]);
25
+ }
26
+ return JSON.stringify(call[i]) === JSON.stringify(expected);
27
+ });
28
+ }
29
+
30
+ const callArg = call[0] as Record<string, unknown>;
31
+ return Object.entries(pattern).every(([key, expected]) => {
32
+ if (typeof expected === 'object' && expected !== null && 'asymmetricMatch' in expected) {
33
+ return (expected as { asymmetricMatch: (actual: unknown) => boolean }).asymmetricMatch(callArg[key]);
34
+ }
35
+ return JSON.stringify(callArg[key]) === JSON.stringify(expected);
36
+ });
37
+ });
38
+
39
+ expect(matched).toBe(true);
40
+ }
41
+
42
+ /**
43
+ * Assert that events were published in order
44
+ *
45
+ * @example
46
+ * assertEventOrder(mockEventBus.publish, ['UserCreated', 'EmailSent']);
47
+ */
48
+ export function assertEventOrder(
49
+ publishMock: Mock,
50
+ expectedEventTypes: string[]
51
+ ): void {
52
+ const actualEventTypes = publishMock.mock.calls
53
+ .map(call => (call[0] as { type: string }).type)
54
+ .filter(type => expectedEventTypes.includes(type));
55
+
56
+ expect(actualEventTypes).toEqual(expectedEventTypes);
57
+ }
58
+
59
+ /**
60
+ * Assert that an event was published with specific payload
61
+ *
62
+ * @example
63
+ * assertEventPublished(mockEventBus, 'UserCreated', { userId: '123' });
64
+ */
65
+ export function assertEventPublished(
66
+ eventBusMock: { publish: Mock } | Mock,
67
+ eventType: string,
68
+ expectedPayload?: Record<string, unknown>
69
+ ): void {
70
+ const publishMock = 'publish' in eventBusMock ? eventBusMock.publish : eventBusMock;
71
+ const calls = publishMock.mock.calls;
72
+
73
+ const matchingEvent = calls.find(call => {
74
+ const event = call[0] as { type: string; payload?: unknown };
75
+ return event.type === eventType;
76
+ });
77
+
78
+ expect(matchingEvent).toBeDefined();
79
+
80
+ if (expectedPayload && matchingEvent) {
81
+ const actualPayload = (matchingEvent[0] as { payload: unknown }).payload;
82
+ expect(actualPayload).toMatchObject(expectedPayload);
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Assert that no event of a specific type was published
88
+ *
89
+ * @example
90
+ * assertEventNotPublished(mockEventBus, 'UserDeleted');
91
+ */
92
+ export function assertEventNotPublished(
93
+ eventBusMock: { publish: Mock } | Mock,
94
+ eventType: string
95
+ ): void {
96
+ const publishMock = 'publish' in eventBusMock ? eventBusMock.publish : eventBusMock;
97
+ const calls = publishMock.mock.calls;
98
+
99
+ const matchingEvent = calls.find(call => {
100
+ const event = call[0] as { type: string };
101
+ return event.type === eventType;
102
+ });
103
+
104
+ expect(matchingEvent).toBeUndefined();
105
+ }
106
+
107
+ /**
108
+ * Assert that mocks were called in a specific order
109
+ *
110
+ * @example
111
+ * assertMocksCalledInOrder([mockValidate, mockSave, mockNotify]);
112
+ */
113
+ export function assertMocksCalledInOrder(mocks: Mock[]): void {
114
+ const orders = mocks.map(mock => {
115
+ if (mock.mock.invocationCallOrder.length === 0) {
116
+ return Infinity;
117
+ }
118
+ return Math.min(...mock.mock.invocationCallOrder);
119
+ });
120
+
121
+ for (let i = 1; i < orders.length; i++) {
122
+ expect(orders[i]).toBeGreaterThan(orders[i - 1]);
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Assert that a mock was called exactly n times with specific arguments
128
+ *
129
+ * @example
130
+ * assertCalledNTimesWith(mockFn, 3, ['arg1', 'arg2']);
131
+ */
132
+ export function assertCalledNTimesWith(
133
+ mock: Mock,
134
+ times: number,
135
+ args: unknown[]
136
+ ): void {
137
+ const matchingCalls = mock.mock.calls.filter(
138
+ call => JSON.stringify(call) === JSON.stringify(args)
139
+ );
140
+
141
+ expect(matchingCalls).toHaveLength(times);
142
+ }
143
+
144
+ /**
145
+ * Assert that async operations completed within time limit
146
+ *
147
+ * @example
148
+ * await assertCompletesWithin(async () => await slowOp(), 1000);
149
+ */
150
+ export async function assertCompletesWithin(
151
+ operation: () => Promise<unknown>,
152
+ maxMs: number
153
+ ): Promise<void> {
154
+ const start = performance.now();
155
+ await operation();
156
+ const duration = performance.now() - start;
157
+
158
+ expect(duration).toBeLessThanOrEqual(maxMs);
159
+ }
160
+
161
+ /**
162
+ * Assert that an operation throws a specific error
163
+ *
164
+ * @example
165
+ * await assertThrowsError(
166
+ * async () => await riskyOp(),
167
+ * ValidationError,
168
+ * 'Invalid input'
169
+ * );
170
+ */
171
+ export async function assertThrowsError<E extends Error>(
172
+ operation: () => Promise<unknown>,
173
+ ErrorType: new (...args: unknown[]) => E,
174
+ messagePattern?: string | RegExp
175
+ ): Promise<E> {
176
+ let error: E | undefined;
177
+
178
+ try {
179
+ await operation();
180
+ } catch (e) {
181
+ error = e as E;
182
+ }
183
+
184
+ expect(error).toBeInstanceOf(ErrorType);
185
+
186
+ if (messagePattern && error) {
187
+ if (typeof messagePattern === 'string') {
188
+ expect(error.message).toContain(messagePattern);
189
+ } else {
190
+ expect(error.message).toMatch(messagePattern);
191
+ }
192
+ }
193
+
194
+ return error!;
195
+ }
196
+
197
+ /**
198
+ * Assert that no sensitive data appears in logs
199
+ *
200
+ * @example
201
+ * assertNoSensitiveData(mockLogger.logs, ['password', 'token', 'secret']);
202
+ */
203
+ export function assertNoSensitiveData(
204
+ logs: Array<{ message: string; context?: Record<string, unknown> }>,
205
+ sensitivePatterns: string[]
206
+ ): void {
207
+ for (const log of logs) {
208
+ const content = JSON.stringify(log).toLowerCase();
209
+
210
+ for (const pattern of sensitivePatterns) {
211
+ expect(content).not.toContain(pattern.toLowerCase());
212
+ }
213
+ }
214
+ }
215
+
216
+ /**
217
+ * Assert that a value matches a snapshot with custom serialization
218
+ *
219
+ * @example
220
+ * assertMatchesSnapshot(result, { ignoreFields: ['timestamp', 'id'] });
221
+ */
222
+ export function assertMatchesSnapshot(
223
+ value: unknown,
224
+ options: SnapshotOptions = {}
225
+ ): void {
226
+ const { ignoreFields = [], transform } = options;
227
+
228
+ let processed = value;
229
+
230
+ if (ignoreFields.length > 0 && typeof processed === 'object' && processed !== null) {
231
+ processed = removeFields(processed as Record<string, unknown>, ignoreFields);
232
+ }
233
+
234
+ if (transform) {
235
+ processed = transform(processed);
236
+ }
237
+
238
+ expect(processed).toMatchSnapshot();
239
+ }
240
+
241
+ /**
242
+ * Snapshot options interface
243
+ */
244
+ export interface SnapshotOptions {
245
+ ignoreFields?: string[];
246
+ transform?: (value: unknown) => unknown;
247
+ }
248
+
249
+ /**
250
+ * Remove fields from object for snapshot comparison
251
+ */
252
+ function removeFields(obj: Record<string, unknown>, fields: string[]): Record<string, unknown> {
253
+ const result = { ...obj };
254
+
255
+ for (const field of fields) {
256
+ delete result[field];
257
+ }
258
+
259
+ for (const [key, value] of Object.entries(result)) {
260
+ if (typeof value === 'object' && value !== null) {
261
+ result[key] = removeFields(value as Record<string, unknown>, fields);
262
+ }
263
+ }
264
+
265
+ return result;
266
+ }
267
+
268
+ /**
269
+ * Assert that performance metrics meet V3 targets
270
+ *
271
+ * @example
272
+ * assertV3PerformanceTargets({
273
+ * searchSpeedup: 160,
274
+ * memoryReduction: 0.55,
275
+ * });
276
+ */
277
+ export function assertV3PerformanceTargets(metrics: V3PerformanceMetrics): void {
278
+ // Search speedup: 150x - 12500x
279
+ if (metrics.searchSpeedup !== undefined) {
280
+ expect(metrics.searchSpeedup).toBeGreaterThanOrEqual(150);
281
+ expect(metrics.searchSpeedup).toBeLessThanOrEqual(12500);
282
+ }
283
+
284
+ // Flash attention speedup: 2.49x - 7.47x
285
+ if (metrics.flashAttentionSpeedup !== undefined) {
286
+ expect(metrics.flashAttentionSpeedup).toBeGreaterThanOrEqual(2.49);
287
+ expect(metrics.flashAttentionSpeedup).toBeLessThanOrEqual(7.47);
288
+ }
289
+
290
+ // Memory reduction: >= 50%
291
+ if (metrics.memoryReduction !== undefined) {
292
+ expect(metrics.memoryReduction).toBeGreaterThanOrEqual(0.50);
293
+ }
294
+
295
+ // Startup time: < 500ms
296
+ if (metrics.startupTimeMs !== undefined) {
297
+ expect(metrics.startupTimeMs).toBeLessThan(500);
298
+ }
299
+
300
+ // Response time: sub-100ms
301
+ if (metrics.responseTimeMs !== undefined) {
302
+ expect(metrics.responseTimeMs).toBeLessThan(100);
303
+ }
304
+ }
305
+
306
+ /**
307
+ * V3 performance metrics interface
308
+ */
309
+ export interface V3PerformanceMetrics {
310
+ searchSpeedup?: number;
311
+ flashAttentionSpeedup?: number;
312
+ memoryReduction?: number;
313
+ startupTimeMs?: number;
314
+ responseTimeMs?: number;
315
+ }
316
+
317
+ /**
318
+ * Assert that a domain object is valid
319
+ *
320
+ * @example
321
+ * assertValidDomainObject(user, UserSchema);
322
+ */
323
+ export function assertValidDomainObject<T>(
324
+ object: T,
325
+ validator: (obj: T) => { valid: boolean; errors?: string[] }
326
+ ): void {
327
+ const result = validator(object);
328
+
329
+ if (!result.valid) {
330
+ throw new Error(`Invalid domain object: ${result.errors?.join(', ')}`);
331
+ }
332
+ }
333
+
334
+ /**
335
+ * Assert that a mock was only called with allowed arguments
336
+ *
337
+ * @example
338
+ * assertOnlyCalledWithAllowed(mockFn, [['valid1'], ['valid2']]);
339
+ */
340
+ export function assertOnlyCalledWithAllowed(
341
+ mock: Mock,
342
+ allowedCalls: unknown[][]
343
+ ): void {
344
+ const calls = mock.mock.calls;
345
+
346
+ for (const call of calls) {
347
+ const isAllowed = allowedCalls.some(
348
+ allowed => JSON.stringify(call) === JSON.stringify(allowed)
349
+ );
350
+
351
+ if (!isAllowed) {
352
+ throw new Error(
353
+ `Mock was called with unexpected arguments: ${JSON.stringify(call)}\n` +
354
+ `Allowed: ${JSON.stringify(allowedCalls)}`
355
+ );
356
+ }
357
+ }
358
+ }
359
+
360
+ /**
361
+ * Assert that an array contains elements in partial order
362
+ *
363
+ * @example
364
+ * assertPartialOrder(events, [
365
+ * { type: 'Start' },
366
+ * { type: 'Process' },
367
+ * { type: 'End' },
368
+ * ]);
369
+ */
370
+ export function assertPartialOrder<T>(
371
+ actual: T[],
372
+ expectedOrder: Partial<T>[]
373
+ ): void {
374
+ let lastIndex = -1;
375
+
376
+ for (const expected of expectedOrder) {
377
+ const index = actual.findIndex((item, i) =>
378
+ i > lastIndex &&
379
+ Object.entries(expected as Record<string, unknown>).every(
380
+ ([key, value]) => (item as Record<string, unknown>)[key] === value
381
+ )
382
+ );
383
+
384
+ if (index === -1) {
385
+ throw new Error(
386
+ `Expected to find ${JSON.stringify(expected)} after index ${lastIndex} in array`
387
+ );
388
+ }
389
+
390
+ lastIndex = index;
391
+ }
392
+ }
393
+
394
+ /**
395
+ * Assert that all items in a collection pass a predicate
396
+ *
397
+ * @example
398
+ * assertAllPass(results, result => result.success);
399
+ */
400
+ export function assertAllPass<T>(
401
+ items: T[],
402
+ predicate: (item: T, index: number) => boolean,
403
+ message?: string
404
+ ): void {
405
+ for (let i = 0; i < items.length; i++) {
406
+ if (!predicate(items[i], i)) {
407
+ throw new Error(
408
+ message ?? `Item at index ${i} failed predicate: ${JSON.stringify(items[i])}`
409
+ );
410
+ }
411
+ }
412
+ }
413
+
414
+ /**
415
+ * Assert that none of the items in a collection pass a predicate
416
+ *
417
+ * @example
418
+ * assertNonePass(results, result => result.error);
419
+ */
420
+ export function assertNonePass<T>(
421
+ items: T[],
422
+ predicate: (item: T, index: number) => boolean,
423
+ message?: string
424
+ ): void {
425
+ for (let i = 0; i < items.length; i++) {
426
+ if (predicate(items[i], i)) {
427
+ throw new Error(
428
+ message ?? `Item at index ${i} passed predicate but should not have: ${JSON.stringify(items[i])}`
429
+ );
430
+ }
431
+ }
432
+ }
433
+
434
+ /**
435
+ * Assert that two arrays have the same elements regardless of order
436
+ *
437
+ * @example
438
+ * assertSameElements([1, 2, 3], [3, 1, 2]);
439
+ */
440
+ export function assertSameElements<T>(actual: T[], expected: T[]): void {
441
+ expect(actual).toHaveLength(expected.length);
442
+
443
+ const actualSorted = [...actual].sort((a, b) =>
444
+ JSON.stringify(a).localeCompare(JSON.stringify(b))
445
+ );
446
+ const expectedSorted = [...expected].sort((a, b) =>
447
+ JSON.stringify(a).localeCompare(JSON.stringify(b))
448
+ );
449
+
450
+ expect(actualSorted).toEqual(expectedSorted);
451
+ }
452
+
453
+ /**
454
+ * Assert that a mock returns expected results in sequence
455
+ *
456
+ * @example
457
+ * await assertMockReturnsSequence(mockFn, [1, 2, 3]);
458
+ */
459
+ export async function assertMockReturnsSequence(
460
+ mock: Mock,
461
+ expectedResults: unknown[]
462
+ ): Promise<void> {
463
+ for (const expected of expectedResults) {
464
+ const result = await mock();
465
+ expect(result).toEqual(expected);
466
+ }
467
+ }
468
+
469
+ /**
470
+ * Assert state transition is valid
471
+ *
472
+ * @example
473
+ * assertValidStateTransition(
474
+ * 'pending',
475
+ * 'running',
476
+ * { pending: ['running', 'cancelled'], running: ['completed', 'failed'] }
477
+ * );
478
+ */
479
+ export function assertValidStateTransition<T extends string>(
480
+ from: T,
481
+ to: T,
482
+ allowedTransitions: Record<T, T[]>
483
+ ): void {
484
+ const allowed = allowedTransitions[from];
485
+
486
+ if (!allowed || !allowed.includes(to)) {
487
+ throw new Error(
488
+ `Invalid state transition from '${from}' to '${to}'. ` +
489
+ `Allowed transitions from '${from}': ${allowed?.join(', ') ?? 'none'}`
490
+ );
491
+ }
492
+ }
493
+
494
+ /**
495
+ * Assert that a retry policy was followed
496
+ *
497
+ * @example
498
+ * assertRetryPattern(mockFn, { attempts: 3, backoffPattern: 'exponential' });
499
+ */
500
+ export function assertRetryPattern(
501
+ mock: Mock,
502
+ options: RetryPatternOptions
503
+ ): void {
504
+ const calls = mock.mock.calls;
505
+
506
+ expect(calls).toHaveLength(options.attempts);
507
+
508
+ if (options.backoffPattern === 'exponential' && calls.length > 1) {
509
+ // Check that intervals roughly follow exponential pattern
510
+ const invocationOrder = mock.mock.invocationCallOrder;
511
+ for (let i = 2; i < invocationOrder.length; i++) {
512
+ const prevGap = invocationOrder[i - 1] - invocationOrder[i - 2];
513
+ const currentGap = invocationOrder[i] - invocationOrder[i - 1];
514
+ // Allow some variance in timing
515
+ expect(currentGap).toBeGreaterThanOrEqual(prevGap * 0.8);
516
+ }
517
+ }
518
+ }
519
+
520
+ /**
521
+ * Retry pattern options interface
522
+ */
523
+ export interface RetryPatternOptions {
524
+ attempts: number;
525
+ backoffPattern?: 'linear' | 'exponential' | 'constant';
526
+ initialDelayMs?: number;
527
+ }
528
+
529
+ /**
530
+ * Assert that a dependency was properly injected
531
+ *
532
+ * @example
533
+ * assertDependencyInjected(service, 'repository', mockRepository);
534
+ */
535
+ export function assertDependencyInjected<T extends object>(
536
+ subject: T,
537
+ propertyName: keyof T,
538
+ expectedDependency: unknown
539
+ ): void {
540
+ expect(subject[propertyName]).toBe(expectedDependency);
541
+ }
542
+
543
+ /**
544
+ * Custom Vitest matcher declarations
545
+ * Note: Main declarations in setup.ts - these extend CustomMatchers
546
+ */
547
+
548
+ /**
549
+ * Register custom Vitest matchers
550
+ */
551
+ export function registerCustomMatchers(): void {
552
+ expect.extend({
553
+ toHaveBeenCalledWithPattern(received: Mock, pattern: Record<string, unknown>) {
554
+ const calls = received.mock.calls;
555
+ const pass = calls.some(call => {
556
+ const callArg = call[0] as Record<string, unknown>;
557
+ return Object.entries(pattern).every(([key, expected]) =>
558
+ JSON.stringify(callArg[key]) === JSON.stringify(expected)
559
+ );
560
+ });
561
+
562
+ return {
563
+ pass,
564
+ message: () => pass
565
+ ? `Expected mock not to have been called with pattern ${JSON.stringify(pattern)}`
566
+ : `Expected mock to have been called with pattern ${JSON.stringify(pattern)}`,
567
+ };
568
+ },
569
+
570
+ toHaveEventType(received: { type: string }, eventType: string) {
571
+ const pass = received.type === eventType;
572
+
573
+ return {
574
+ pass,
575
+ message: () => pass
576
+ ? `Expected event not to have type ${eventType}`
577
+ : `Expected event to have type ${eventType}, but got ${received.type}`,
578
+ };
579
+ },
580
+
581
+ toMeetV3PerformanceTargets(received: V3PerformanceMetrics) {
582
+ const issues: string[] = [];
583
+
584
+ if (received.searchSpeedup !== undefined) {
585
+ if (received.searchSpeedup < 150) {
586
+ issues.push(`Search speedup ${received.searchSpeedup}x is below minimum 150x`);
587
+ }
588
+ }
589
+
590
+ if (received.flashAttentionSpeedup !== undefined) {
591
+ if (received.flashAttentionSpeedup < 2.49) {
592
+ issues.push(`Flash attention speedup ${received.flashAttentionSpeedup}x is below minimum 2.49x`);
593
+ }
594
+ }
595
+
596
+ if (received.memoryReduction !== undefined) {
597
+ if (received.memoryReduction < 0.50) {
598
+ issues.push(`Memory reduction ${received.memoryReduction * 100}% is below target 50%`);
599
+ }
600
+ }
601
+
602
+ if (received.startupTimeMs !== undefined) {
603
+ if (received.startupTimeMs >= 500) {
604
+ issues.push(`Startup time ${received.startupTimeMs}ms exceeds target 500ms`);
605
+ }
606
+ }
607
+
608
+ return {
609
+ pass: issues.length === 0,
610
+ message: () => issues.length === 0
611
+ ? 'Performance metrics meet V3 targets'
612
+ : `Performance issues: ${issues.join('; ')}`,
613
+ };
614
+ },
615
+ });
616
+ }