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

679 lines (589 loc) 15.7 kB
/** * @claude-flow/testing - Setup & Teardown Helpers * * Global setup and teardown utilities for V3 module testing. * Provides test isolation, resource cleanup, and environment management. */ import { vi, beforeEach, afterEach, beforeAll, afterAll } from 'vitest'; /** * Setup context for managing test resources */ export interface SetupContext { /** * Register a cleanup function to be called during teardown */ addCleanup(cleanup: CleanupFunction): void; /** * Register a resource that needs to be closed/disposed */ registerResource<T extends Disposable>(resource: T): T; /** * Get a registered resource by name */ getResource<T>(name: string): T | undefined; /** * Set a named resource */ setResource<T>(name: string, resource: T): void; /** * Run all cleanup functions */ runCleanup(): Promise<void>; } /** * Cleanup function type */ export type CleanupFunction = () => void | Promise<void>; /** * Disposable interface */ export interface Disposable { dispose?(): void | Promise<void>; close?(): void | Promise<void>; destroy?(): void | Promise<void>; shutdown?(): void | Promise<void>; } /** * Create a setup context for managing test resources * * @example * const ctx = createSetupContext(); * ctx.addCleanup(() => server.close()); * ctx.registerResource(database); * // ... run tests * await ctx.runCleanup(); */ export function createSetupContext(): SetupContext { const cleanups: CleanupFunction[] = []; const resources = new Map<string, unknown>(); const disposables: Disposable[] = []; return { addCleanup(cleanup: CleanupFunction): void { cleanups.push(cleanup); }, registerResource<T extends Disposable>(resource: T): T { disposables.push(resource); return resource; }, getResource<T>(name: string): T | undefined { return resources.get(name) as T | undefined; }, setResource<T>(name: string, resource: T): void { resources.set(name, resource); }, async runCleanup(): Promise<void> { // Run cleanups in reverse order for (const cleanup of cleanups.reverse()) { try { await cleanup(); } catch (error) { console.error('Cleanup error:', error); } } // Dispose resources for (const resource of disposables) { try { if (resource.dispose) { await resource.dispose(); } else if (resource.close) { await resource.close(); } else if (resource.destroy) { await resource.destroy(); } else if (resource.shutdown) { await resource.shutdown(); } } catch (error) { console.error('Resource disposal error:', error); } } cleanups.length = 0; resources.clear(); disposables.length = 0; }, }; } /** * Global test context that persists across test files */ let globalContext: SetupContext | null = null; /** * Get or create the global test context */ export function getGlobalContext(): SetupContext { if (!globalContext) { globalContext = createSetupContext(); } return globalContext; } /** * Reset the global test context */ export async function resetGlobalContext(): Promise<void> { if (globalContext) { await globalContext.runCleanup(); globalContext = null; } } /** * Test environment configuration */ export interface TestEnvironmentConfig { /** * Reset all mocks before each test */ resetMocks?: boolean; /** * Use fake timers */ fakeTimers?: boolean; /** * Initial fake time */ initialTime?: Date | number; /** * Environment variables to set */ env?: Record<string, string>; /** * Suppress console output during tests */ suppressConsole?: boolean | ('log' | 'warn' | 'error' | 'info')[]; /** * Timeout for async operations */ timeout?: number; } /** * Configure test environment with standard settings * * @example * configureTestEnvironment({ * resetMocks: true, * fakeTimers: true, * suppressConsole: ['log', 'warn'], * }); */ export function configureTestEnvironment(config: TestEnvironmentConfig = {}): void { const { resetMocks = true, fakeTimers = false, initialTime, env = {}, suppressConsole = false, } = config; const originalEnv: Record<string, string | undefined> = {}; const originalConsole: Partial<Console> = {}; beforeAll(() => { // Set environment variables for (const [key, value] of Object.entries(env)) { originalEnv[key] = process.env[key]; process.env[key] = value; } // Suppress console if (suppressConsole) { const methods = suppressConsole === true ? ['log', 'warn', 'error', 'info'] as const : suppressConsole; for (const method of methods) { originalConsole[method] = console[method]; console[method] = vi.fn(); } } }); afterAll(() => { // Restore environment variables for (const [key, value] of Object.entries(originalEnv)) { if (value === undefined) { delete process.env[key]; } else { process.env[key] = value; } } // Restore console for (const [method, original] of Object.entries(originalConsole)) { if (original) { (console as unknown as Record<string, unknown>)[method] = original; } } }); beforeEach(() => { if (resetMocks) { vi.clearAllMocks(); } if (fakeTimers) { vi.useFakeTimers(); if (initialTime) { vi.setSystemTime(initialTime); } } }); afterEach(() => { if (fakeTimers) { vi.useRealTimers(); } vi.restoreAllMocks(); }); } /** * Create a test suite with automatic setup/teardown * * @example * const { beforeEachTest, afterEachTest, getContext } = createTestSuite({ * resetMocks: true, * }); */ export function createTestSuite(config: TestEnvironmentConfig = {}): TestSuiteHelpers { const context = createSetupContext(); configureTestEnvironment(config); return { beforeEachTest: (fn: (ctx: SetupContext) => void | Promise<void>) => { beforeEach(async () => { await fn(context); }); }, afterEachTest: (fn: (ctx: SetupContext) => void | Promise<void>) => { afterEach(async () => { await fn(context); await context.runCleanup(); }); }, getContext: () => context, }; } /** * Test suite helpers interface */ export interface TestSuiteHelpers { beforeEachTest: (fn: (ctx: SetupContext) => void | Promise<void>) => void; afterEachTest: (fn: (ctx: SetupContext) => void | Promise<void>) => void; getContext: () => SetupContext; } /** * Create isolated test scope * * @example * const scope = createTestScope(); * scope.addMock(mockService); * await scope.run(async () => { * // test code * }); */ export function createTestScope(): TestScope { const mocks: ReturnType<typeof vi.fn>[] = []; const cleanups: CleanupFunction[] = []; return { addMock<T extends ReturnType<typeof vi.fn>>(mock: T): T { mocks.push(mock); return mock; }, addCleanup(cleanup: CleanupFunction): void { cleanups.push(cleanup); }, async run<T>(fn: () => Promise<T>): Promise<T> { try { return await fn(); } finally { // Clear all mocks for (const mock of mocks) { mock.mockClear(); } // Run cleanups for (const cleanup of cleanups.reverse()) { await cleanup(); } } }, clear(): void { for (const mock of mocks) { mock.mockClear(); } }, reset(): void { for (const mock of mocks) { mock.mockReset(); } }, }; } /** * Test scope interface */ export interface TestScope { addMock<T extends ReturnType<typeof vi.fn>>(mock: T): T; addCleanup(cleanup: CleanupFunction): void; run<T>(fn: () => Promise<T>): Promise<T>; clear(): void; reset(): void; } /** * Database test helper for memory/agentdb testing */ export interface DatabaseTestHelper { setup(): Promise<void>; teardown(): Promise<void>; clear(): Promise<void>; seed(data: Record<string, unknown[]>): Promise<void>; } /** * Create in-memory database helper for testing * * @example * const db = createInMemoryDatabaseHelper(); * await db.setup(); * await db.seed({ users: [{ id: 1, name: 'Test' }] }); * // ... run tests * await db.teardown(); */ export function createInMemoryDatabaseHelper(): DatabaseTestHelper { const data = new Map<string, unknown[]>(); return { async setup(): Promise<void> { data.clear(); }, async teardown(): Promise<void> { data.clear(); }, async clear(): Promise<void> { data.clear(); }, async seed(seedData: Record<string, unknown[]>): Promise<void> { for (const [table, records] of Object.entries(seedData)) { data.set(table, [...records]); } }, }; } /** * Network test helper for mocking HTTP/WebSocket */ export interface NetworkTestHelper { mockFetch(responses: MockFetchResponse[]): void; mockWebSocket(handler: (message: unknown) => unknown): void; clearMocks(): void; } /** * Mock fetch response */ export interface MockFetchResponse { url: string | RegExp; method?: string; status?: number; body?: unknown; headers?: Record<string, string>; delay?: number; } /** * Create network test helper * * @example * const network = createNetworkTestHelper(); * network.mockFetch([ * { url: '/api/users', body: [{ id: 1 }] }, * ]); */ export function createNetworkTestHelper(): NetworkTestHelper { const fetchResponses: MockFetchResponse[] = []; let originalFetch: typeof global.fetch; return { mockFetch(responses: MockFetchResponse[]): void { fetchResponses.push(...responses); if (!originalFetch) { originalFetch = global.fetch; global.fetch = vi.fn(async (input: string | URL | Request, init?: RequestInit) => { const url = typeof input === 'string' ? input : input.toString(); const method = init?.method ?? 'GET'; const match = fetchResponses.find(r => { const urlMatch = typeof r.url === 'string' ? url.includes(r.url) : r.url.test(url); const methodMatch = !r.method || r.method === method; return urlMatch && methodMatch; }); if (!match) { throw new Error(`No mock found for ${method} ${url}`); } if (match.delay) { await new Promise(resolve => setTimeout(resolve, match.delay)); } return new Response(JSON.stringify(match.body), { status: match.status ?? 200, headers: match.headers ?? { 'Content-Type': 'application/json' }, }); }); } }, mockWebSocket(handler: (message: unknown) => unknown): void { // WebSocket mocking would require more setup // This is a placeholder for the interface console.warn('WebSocket mocking not yet implemented'); }, clearMocks(): void { fetchResponses.length = 0; if (originalFetch) { global.fetch = originalFetch; } }, }; } /** * File system test helper */ export interface FileSystemTestHelper { createTempDir(): Promise<string>; createFile(path: string, content: string): Promise<void>; readFile(path: string): Promise<string>; cleanup(): Promise<void>; } /** * Create in-memory file system helper * * @example * const fs = createInMemoryFileSystemHelper(); * await fs.createFile('/test.txt', 'content'); * const content = await fs.readFile('/test.txt'); */ export function createInMemoryFileSystemHelper(): FileSystemTestHelper { const files = new Map<string, string>(); const tempDirs: string[] = []; return { async createTempDir(): Promise<string> { const dir = `/tmp/test-${Date.now()}-${Math.random().toString(36).slice(2)}`; tempDirs.push(dir); return dir; }, async createFile(path: string, content: string): Promise<void> { files.set(path, content); }, async readFile(path: string): Promise<string> { const content = files.get(path); if (content === undefined) { throw new Error(`File not found: ${path}`); } return content; }, async cleanup(): Promise<void> { files.clear(); tempDirs.length = 0; }, }; } /** * Performance test helper */ export interface PerformanceTestHelper { startMeasurement(name: string): void; endMeasurement(name: string): number; getMeasurements(): Record<string, number[]>; getStats(name: string): { min: number; max: number; avg: number; p95: number }; clear(): void; } /** * Create performance test helper * * @example * const perf = createPerformanceTestHelper(); * perf.startMeasurement('search'); * await search(); * const duration = perf.endMeasurement('search'); */ export function createPerformanceTestHelper(): PerformanceTestHelper { const measurements = new Map<string, number[]>(); const starts = new Map<string, number>(); return { startMeasurement(name: string): void { starts.set(name, performance.now()); }, endMeasurement(name: string): number { const start = starts.get(name); if (start === undefined) { throw new Error(`No measurement started for: ${name}`); } const duration = performance.now() - start; starts.delete(name); if (!measurements.has(name)) { measurements.set(name, []); } measurements.get(name)!.push(duration); return duration; }, getMeasurements(): Record<string, number[]> { return Object.fromEntries(measurements); }, getStats(name: string): { min: number; max: number; avg: number; p95: number } { const values = measurements.get(name); if (!values || values.length === 0) { return { min: 0, max: 0, avg: 0, p95: 0 }; } const sorted = [...values].sort((a, b) => a - b); const sum = sorted.reduce((a, b) => a + b, 0); return { min: sorted[0], max: sorted[sorted.length - 1], avg: sum / sorted.length, p95: sorted[Math.floor(sorted.length * 0.95)], }; }, clear(): void { measurements.clear(); starts.clear(); }, }; } /** * Standard V3 test setup * * @example * // In your test file: * setupV3Tests(); * * describe('MyModule', () => { * // tests... * }); */ export function setupV3Tests(config: V3TestConfig = {}): void { configureTestEnvironment({ resetMocks: true, suppressConsole: config.suppressConsole ?? false, env: { NODE_ENV: 'test', CLAUDE_FLOW_MODE: 'test', ...config.env, }, }); } /** * V3 test configuration */ export interface V3TestConfig { suppressConsole?: boolean | ('log' | 'warn' | 'error' | 'info')[]; env?: Record<string, string>; timeout?: number; } /** * Wait for all pending promises to resolve * * @example * await flushPromises(); */ export function flushPromises(): Promise<void> { return new Promise(resolve => setImmediate(resolve)); } /** * Run with timeout * * @example * await withTimeout(async () => { * await longRunningOperation(); * }, 5000); */ export async function withTestTimeout<T>( fn: () => Promise<T>, timeoutMs: number = 5000 ): Promise<T> { return Promise.race([ fn(), new Promise<never>((_, reject) => setTimeout(() => reject(new Error(`Test timed out after ${timeoutMs}ms`)), timeoutMs) ), ]); }