UNPKG

claude-flow

Version:

Ruflo - Enterprise AI agent orchestration for Claude Code. Deploy 60+ specialized agents in coordinated swarms with self-learning, fault-tolerant consensus, vector memory, and MCP integration

617 lines (542 loc) 16.4 kB
/** * @claude-flow/testing - Assertion Helpers * * Custom Vitest matchers and assertion utilities for V3 module testing. * Implements London School TDD behavior verification patterns. */ import { expect, type Mock, type ExpectStatic } from 'vitest'; /** * Assert that a mock was called with arguments matching a pattern * * @example * assertCalledWithPattern(mockFn, { userId: expect.any(String) }); */ export function assertCalledWithPattern( mock: Mock, pattern: Record<string, unknown> | unknown[] ): void { const calls = mock.mock.calls; const matched = calls.some(call => { if (Array.isArray(pattern)) { return pattern.every((expected, i) => { if (typeof expected === 'object' && expected !== null && 'asymmetricMatch' in expected) { return (expected as { asymmetricMatch: (actual: unknown) => boolean }).asymmetricMatch(call[i]); } return JSON.stringify(call[i]) === JSON.stringify(expected); }); } const callArg = call[0] as Record<string, unknown>; return Object.entries(pattern).every(([key, expected]) => { if (typeof expected === 'object' && expected !== null && 'asymmetricMatch' in expected) { return (expected as { asymmetricMatch: (actual: unknown) => boolean }).asymmetricMatch(callArg[key]); } return JSON.stringify(callArg[key]) === JSON.stringify(expected); }); }); expect(matched).toBe(true); } /** * Assert that events were published in order * * @example * assertEventOrder(mockEventBus.publish, ['UserCreated', 'EmailSent']); */ export function assertEventOrder( publishMock: Mock, expectedEventTypes: string[] ): void { const actualEventTypes = publishMock.mock.calls .map(call => (call[0] as { type: string }).type) .filter(type => expectedEventTypes.includes(type)); expect(actualEventTypes).toEqual(expectedEventTypes); } /** * Assert that an event was published with specific payload * * @example * assertEventPublished(mockEventBus, 'UserCreated', { userId: '123' }); */ export function assertEventPublished( eventBusMock: { publish: Mock } | Mock, eventType: string, expectedPayload?: Record<string, unknown> ): void { const publishMock = 'publish' in eventBusMock ? eventBusMock.publish : eventBusMock; const calls = publishMock.mock.calls; const matchingEvent = calls.find(call => { const event = call[0] as { type: string; payload?: unknown }; return event.type === eventType; }); expect(matchingEvent).toBeDefined(); if (expectedPayload && matchingEvent) { const actualPayload = (matchingEvent[0] as { payload: unknown }).payload; expect(actualPayload).toMatchObject(expectedPayload); } } /** * Assert that no event of a specific type was published * * @example * assertEventNotPublished(mockEventBus, 'UserDeleted'); */ export function assertEventNotPublished( eventBusMock: { publish: Mock } | Mock, eventType: string ): void { const publishMock = 'publish' in eventBusMock ? eventBusMock.publish : eventBusMock; const calls = publishMock.mock.calls; const matchingEvent = calls.find(call => { const event = call[0] as { type: string }; return event.type === eventType; }); expect(matchingEvent).toBeUndefined(); } /** * Assert that mocks were called in a specific order * * @example * assertMocksCalledInOrder([mockValidate, mockSave, mockNotify]); */ export function assertMocksCalledInOrder(mocks: Mock[]): void { const orders = mocks.map(mock => { if (mock.mock.invocationCallOrder.length === 0) { return Infinity; } return Math.min(...mock.mock.invocationCallOrder); }); for (let i = 1; i < orders.length; i++) { expect(orders[i]).toBeGreaterThan(orders[i - 1]); } } /** * Assert that a mock was called exactly n times with specific arguments * * @example * assertCalledNTimesWith(mockFn, 3, ['arg1', 'arg2']); */ export function assertCalledNTimesWith( mock: Mock, times: number, args: unknown[] ): void { const matchingCalls = mock.mock.calls.filter( call => JSON.stringify(call) === JSON.stringify(args) ); expect(matchingCalls).toHaveLength(times); } /** * Assert that async operations completed within time limit * * @example * await assertCompletesWithin(async () => await slowOp(), 1000); */ export async function assertCompletesWithin( operation: () => Promise<unknown>, maxMs: number ): Promise<void> { const start = performance.now(); await operation(); const duration = performance.now() - start; expect(duration).toBeLessThanOrEqual(maxMs); } /** * Assert that an operation throws a specific error * * @example * await assertThrowsError( * async () => await riskyOp(), * ValidationError, * 'Invalid input' * ); */ export async function assertThrowsError<E extends Error>( operation: () => Promise<unknown>, ErrorType: new (...args: unknown[]) => E, messagePattern?: string | RegExp ): Promise<E> { let error: E | undefined; try { await operation(); } catch (e) { error = e as E; } expect(error).toBeInstanceOf(ErrorType); if (messagePattern && error) { if (typeof messagePattern === 'string') { expect(error.message).toContain(messagePattern); } else { expect(error.message).toMatch(messagePattern); } } return error!; } /** * Assert that no sensitive data appears in logs * * @example * assertNoSensitiveData(mockLogger.logs, ['password', 'token', 'secret']); */ export function assertNoSensitiveData( logs: Array<{ message: string; context?: Record<string, unknown> }>, sensitivePatterns: string[] ): void { for (const log of logs) { const content = JSON.stringify(log).toLowerCase(); for (const pattern of sensitivePatterns) { expect(content).not.toContain(pattern.toLowerCase()); } } } /** * Assert that a value matches a snapshot with custom serialization * * @example * assertMatchesSnapshot(result, { ignoreFields: ['timestamp', 'id'] }); */ export function assertMatchesSnapshot( value: unknown, options: SnapshotOptions = {} ): void { const { ignoreFields = [], transform } = options; let processed = value; if (ignoreFields.length > 0 && typeof processed === 'object' && processed !== null) { processed = removeFields(processed as Record<string, unknown>, ignoreFields); } if (transform) { processed = transform(processed); } expect(processed).toMatchSnapshot(); } /** * Snapshot options interface */ export interface SnapshotOptions { ignoreFields?: string[]; transform?: (value: unknown) => unknown; } /** * Remove fields from object for snapshot comparison */ function removeFields(obj: Record<string, unknown>, fields: string[]): Record<string, unknown> { const result = { ...obj }; for (const field of fields) { delete result[field]; } for (const [key, value] of Object.entries(result)) { if (typeof value === 'object' && value !== null) { result[key] = removeFields(value as Record<string, unknown>, fields); } } return result; } /** * Assert that performance metrics meet V3 targets * * @example * assertV3PerformanceTargets({ * searchSpeedup: 160, * memoryReduction: 0.55, * }); */ export function assertV3PerformanceTargets(metrics: V3PerformanceMetrics): void { // Search speedup: 150x - 12500x if (metrics.searchSpeedup !== undefined) { expect(metrics.searchSpeedup).toBeGreaterThanOrEqual(150); expect(metrics.searchSpeedup).toBeLessThanOrEqual(12500); } // Flash attention speedup: 2.49x - 7.47x if (metrics.flashAttentionSpeedup !== undefined) { expect(metrics.flashAttentionSpeedup).toBeGreaterThanOrEqual(2.49); expect(metrics.flashAttentionSpeedup).toBeLessThanOrEqual(7.47); } // Memory reduction: >= 50% if (metrics.memoryReduction !== undefined) { expect(metrics.memoryReduction).toBeGreaterThanOrEqual(0.50); } // Startup time: < 500ms if (metrics.startupTimeMs !== undefined) { expect(metrics.startupTimeMs).toBeLessThan(500); } // Response time: sub-100ms if (metrics.responseTimeMs !== undefined) { expect(metrics.responseTimeMs).toBeLessThan(100); } } /** * V3 performance metrics interface */ export interface V3PerformanceMetrics { searchSpeedup?: number; flashAttentionSpeedup?: number; memoryReduction?: number; startupTimeMs?: number; responseTimeMs?: number; } /** * Assert that a domain object is valid * * @example * assertValidDomainObject(user, UserSchema); */ export function assertValidDomainObject<T>( object: T, validator: (obj: T) => { valid: boolean; errors?: string[] } ): void { const result = validator(object); if (!result.valid) { throw new Error(`Invalid domain object: ${result.errors?.join(', ')}`); } } /** * Assert that a mock was only called with allowed arguments * * @example * assertOnlyCalledWithAllowed(mockFn, [['valid1'], ['valid2']]); */ export function assertOnlyCalledWithAllowed( mock: Mock, allowedCalls: unknown[][] ): void { const calls = mock.mock.calls; for (const call of calls) { const isAllowed = allowedCalls.some( allowed => JSON.stringify(call) === JSON.stringify(allowed) ); if (!isAllowed) { throw new Error( `Mock was called with unexpected arguments: ${JSON.stringify(call)}\n` + `Allowed: ${JSON.stringify(allowedCalls)}` ); } } } /** * Assert that an array contains elements in partial order * * @example * assertPartialOrder(events, [ * { type: 'Start' }, * { type: 'Process' }, * { type: 'End' }, * ]); */ export function assertPartialOrder<T>( actual: T[], expectedOrder: Partial<T>[] ): void { let lastIndex = -1; for (const expected of expectedOrder) { const index = actual.findIndex((item, i) => i > lastIndex && Object.entries(expected as Record<string, unknown>).every( ([key, value]) => (item as Record<string, unknown>)[key] === value ) ); if (index === -1) { throw new Error( `Expected to find ${JSON.stringify(expected)} after index ${lastIndex} in array` ); } lastIndex = index; } } /** * Assert that all items in a collection pass a predicate * * @example * assertAllPass(results, result => result.success); */ export function assertAllPass<T>( items: T[], predicate: (item: T, index: number) => boolean, message?: string ): void { for (let i = 0; i < items.length; i++) { if (!predicate(items[i], i)) { throw new Error( message ?? `Item at index ${i} failed predicate: ${JSON.stringify(items[i])}` ); } } } /** * Assert that none of the items in a collection pass a predicate * * @example * assertNonePass(results, result => result.error); */ export function assertNonePass<T>( items: T[], predicate: (item: T, index: number) => boolean, message?: string ): void { for (let i = 0; i < items.length; i++) { if (predicate(items[i], i)) { throw new Error( message ?? `Item at index ${i} passed predicate but should not have: ${JSON.stringify(items[i])}` ); } } } /** * Assert that two arrays have the same elements regardless of order * * @example * assertSameElements([1, 2, 3], [3, 1, 2]); */ export function assertSameElements<T>(actual: T[], expected: T[]): void { expect(actual).toHaveLength(expected.length); const actualSorted = [...actual].sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b)) ); const expectedSorted = [...expected].sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b)) ); expect(actualSorted).toEqual(expectedSorted); } /** * Assert that a mock returns expected results in sequence * * @example * await assertMockReturnsSequence(mockFn, [1, 2, 3]); */ export async function assertMockReturnsSequence( mock: Mock, expectedResults: unknown[] ): Promise<void> { for (const expected of expectedResults) { const result = await mock(); expect(result).toEqual(expected); } } /** * Assert state transition is valid * * @example * assertValidStateTransition( * 'pending', * 'running', * { pending: ['running', 'cancelled'], running: ['completed', 'failed'] } * ); */ export function assertValidStateTransition<T extends string>( from: T, to: T, allowedTransitions: Record<T, T[]> ): void { const allowed = allowedTransitions[from]; if (!allowed || !allowed.includes(to)) { throw new Error( `Invalid state transition from '${from}' to '${to}'. ` + `Allowed transitions from '${from}': ${allowed?.join(', ') ?? 'none'}` ); } } /** * Assert that a retry policy was followed * * @example * assertRetryPattern(mockFn, { attempts: 3, backoffPattern: 'exponential' }); */ export function assertRetryPattern( mock: Mock, options: RetryPatternOptions ): void { const calls = mock.mock.calls; expect(calls).toHaveLength(options.attempts); if (options.backoffPattern === 'exponential' && calls.length > 1) { // Check that intervals roughly follow exponential pattern const invocationOrder = mock.mock.invocationCallOrder; for (let i = 2; i < invocationOrder.length; i++) { const prevGap = invocationOrder[i - 1] - invocationOrder[i - 2]; const currentGap = invocationOrder[i] - invocationOrder[i - 1]; // Allow some variance in timing expect(currentGap).toBeGreaterThanOrEqual(prevGap * 0.8); } } } /** * Retry pattern options interface */ export interface RetryPatternOptions { attempts: number; backoffPattern?: 'linear' | 'exponential' | 'constant'; initialDelayMs?: number; } /** * Assert that a dependency was properly injected * * @example * assertDependencyInjected(service, 'repository', mockRepository); */ export function assertDependencyInjected<T extends object>( subject: T, propertyName: keyof T, expectedDependency: unknown ): void { expect(subject[propertyName]).toBe(expectedDependency); } /** * Custom Vitest matcher declarations * Note: Main declarations in setup.ts - these extend CustomMatchers */ /** * Register custom Vitest matchers */ export function registerCustomMatchers(): void { expect.extend({ toHaveBeenCalledWithPattern(received: Mock, pattern: Record<string, unknown>) { const calls = received.mock.calls; const pass = calls.some(call => { const callArg = call[0] as Record<string, unknown>; return Object.entries(pattern).every(([key, expected]) => JSON.stringify(callArg[key]) === JSON.stringify(expected) ); }); return { pass, message: () => pass ? `Expected mock not to have been called with pattern ${JSON.stringify(pattern)}` : `Expected mock to have been called with pattern ${JSON.stringify(pattern)}`, }; }, toHaveEventType(received: { type: string }, eventType: string) { const pass = received.type === eventType; return { pass, message: () => pass ? `Expected event not to have type ${eventType}` : `Expected event to have type ${eventType}, but got ${received.type}`, }; }, toMeetV3PerformanceTargets(received: V3PerformanceMetrics) { const issues: string[] = []; if (received.searchSpeedup !== undefined) { if (received.searchSpeedup < 150) { issues.push(`Search speedup ${received.searchSpeedup}x is below minimum 150x`); } } if (received.flashAttentionSpeedup !== undefined) { if (received.flashAttentionSpeedup < 2.49) { issues.push(`Flash attention speedup ${received.flashAttentionSpeedup}x is below minimum 2.49x`); } } if (received.memoryReduction !== undefined) { if (received.memoryReduction < 0.50) { issues.push(`Memory reduction ${received.memoryReduction * 100}% is below target 50%`); } } if (received.startupTimeMs !== undefined) { if (received.startupTimeMs >= 500) { issues.push(`Startup time ${received.startupTimeMs}ms exceeds target 500ms`); } } return { pass: issues.length === 0, message: () => issues.length === 0 ? 'Performance metrics meet V3 targets' : `Performance issues: ${issues.join('; ')}`, }; }, }); }