Skip to main content
Glama
test-helpers.ts13.1 kB
/** * Common Test Helpers * * Provides utility functions for common test operations: * - Async utilities * - Retry logic * - Timeout handling * - Test data generation * - Deterministic values for flaky test prevention */ // ============================================================================ // Deterministic Test Helpers (Flaky Test Prevention) // ============================================================================ /** * Fixed timestamp for deterministic tests. * Use this instead of Date.now() in tests to prevent flaky behavior. */ export const FIXED_TIMESTAMP = 1702300800000; // 2023-12-11T12:00:00.000Z /** * Create a fixed Date object for deterministic tests. * Use this instead of new Date() in tests. */ export function fixedDate(): Date { return new Date(FIXED_TIMESTAMP); } /** * Create a Date object offset from the fixed timestamp. * Useful for creating relative timestamps in tests. * * @param offsetMs - Offset in milliseconds (negative for past, positive for future) * @returns Date object at fixed timestamp + offset */ export function fixedDateOffset(offsetMs: number): Date { return new Date(FIXED_TIMESTAMP + offsetMs); } /** * Time constants for readable test offsets */ export const TIME = { SECOND: 1000, MINUTE: 60 * 1000, HOUR: 60 * 60 * 1000, DAY: 24 * 60 * 60 * 1000, WEEK: 7 * 24 * 60 * 60 * 1000, MONTH: 30 * 24 * 60 * 60 * 1000, } as const; /** * Seeded pseudo-random number generator for deterministic tests. * Use this instead of Math.random() in tests. */ export class SeededRandom { private seed: number; constructor(seed: number = 12345) { this.seed = seed; } /** * Generate next pseudo-random number between 0 and 1 */ next(): number { // Linear congruential generator this.seed = (this.seed * 1103515245 + 12345) & 0x7fffffff; return this.seed / 0x7fffffff; } /** * Generate pseudo-random integer in range [min, max] */ nextInt(min: number, max: number): number { return Math.floor(this.next() * (max - min + 1)) + min; } /** * Generate pseudo-random float in range [min, max] */ nextFloat(min: number, max: number): number { return this.next() * (max - min) + min; } /** * Reset the generator to initial seed */ reset(seed?: number): void { this.seed = seed ?? 12345; } } /** * Default seeded random instance for tests. * Call seededRandom.reset() in beforeEach to ensure deterministic behavior. */ export const seededRandom = new SeededRandom(); /** * Generate a deterministic ID based on a counter. * Use this instead of UUID or random ID generation in tests. */ let deterministicIdCounter = 0; export function deterministicId(prefix: string = "test"): string { return `${prefix}-${++deterministicIdCounter}`; } /** * Reset the deterministic ID counter. * Call this in beforeEach to ensure consistent IDs across test runs. */ export function resetDeterministicId(): void { deterministicIdCounter = 0; } /** * Generate deterministic values for calibration/learning tests. * These provide predictable sequences for testing statistical algorithms. */ export function deterministicCalibrationData( count: number, seed: number = 42 ): Array<{ actual: number; predicted: number }> { const rng = new SeededRandom(seed); const data: Array<{ actual: number; predicted: number }> = []; for (let i = 0; i < count; i++) { const actual = rng.next(); // Add small deterministic noise const noise = (rng.next() - 0.5) * 0.1; const predicted = Math.max(0, Math.min(1, actual + noise)); data.push({ actual, predicted }); } return data; } /** * Wait for a condition to be true * * @param condition - Function that returns true when condition is met * @param timeoutMs - Maximum time to wait * @param intervalMs - Check interval * @returns Promise that resolves when condition is met */ export async function waitFor( condition: () => boolean | Promise<boolean>, timeoutMs: number = 5000, intervalMs: number = 100 ): Promise<void> { const startTime = Date.now(); while (Date.now() - startTime < timeoutMs) { const result = await condition(); if (result) { return; } await sleep(intervalMs); } throw new Error(`Condition not met within ${timeoutMs}ms`); } /** * Sleep for specified milliseconds * * @param ms - Milliseconds to sleep */ export function sleep(ms: number): Promise<void> { return new Promise((resolve) => setTimeout(resolve, ms)); } /** * Retry an async operation with exponential backoff * * @param fn - Function to retry * @param maxRetries - Maximum number of retries * @param initialDelayMs - Initial delay between retries * @returns Result of successful execution */ export async function retryWithBackoff<T>( fn: () => Promise<T>, maxRetries: number = 3, initialDelayMs: number = 100 ): Promise<T> { let lastError: Error | undefined; for (let attempt = 0; attempt <= maxRetries; attempt++) { try { return await fn(); } catch (error) { lastError = error as Error; if (attempt < maxRetries) { const delayMs = initialDelayMs * Math.pow(2, attempt); await sleep(delayMs); } } } throw lastError ?? new Error("Retry failed"); } /** * Execute function with timeout * * @param fn - Function to execute * @param timeoutMs - Timeout in milliseconds * @returns Result of execution */ export async function withTimeout<T>(fn: () => Promise<T>, timeoutMs: number): Promise<T> { return Promise.race([ fn(), new Promise<T>((_, reject) => setTimeout(() => reject(new Error(`Timeout after ${timeoutMs}ms`)), timeoutMs) ), ]); } /** * Execute multiple promises in parallel with concurrency limit * * @param tasks - Array of task functions * @param concurrency - Maximum concurrent executions * @returns Array of results */ export async function parallelLimit<T>( tasks: (() => Promise<T>)[], concurrency: number ): Promise<T[]> { const results: T[] = []; let taskIndex = 0; async function runNext(): Promise<void> { while (taskIndex < tasks.length) { const currentIndex = taskIndex++; const result = await tasks[currentIndex](); results[currentIndex] = result; } } const workers = Array.from({ length: Math.min(concurrency, tasks.length) }, () => runNext()); await Promise.all(workers); return results; } /** * Generate random string * * @param length - String length * @returns Random string */ export function randomString(length: number = 10): string { const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; let result = ""; for (let i = 0; i < length; i++) { result += chars.charAt(Math.floor(Math.random() * chars.length)); } return result; } /** * Generate random integer in range * * @param min - Minimum value (inclusive) * @param max - Maximum value (inclusive) * @returns Random integer */ export function randomInt(min: number, max: number): number { return Math.floor(Math.random() * (max - min + 1)) + min; } /** * Generate random float in range * * @param min - Minimum value * @param max - Maximum value * @returns Random float */ export function randomFloat(min: number, max: number): number { return Math.random() * (max - min) + min; } /** * Shuffle array in place * * @param array - Array to shuffle * @returns Shuffled array */ export function shuffle<T>(array: T[]): T[] { const result = [...array]; for (let i = result.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [result[i], result[j]] = [result[j], result[i]]; } return result; } /** * Pick random element from array * * @param array - Array to pick from * @returns Random element */ export function randomPick<T>(array: T[]): T { return array[Math.floor(Math.random() * array.length)]; } /** * Create a spy function that tracks calls */ export interface SpyFunction<T extends (...args: unknown[]) => unknown> { (...args: Parameters<T>): ReturnType<T>; calls: Array<{ args: Parameters<T>; result: ReturnType<T> }>; callCount: number; reset: () => void; } export function createSpy<T extends (...args: unknown[]) => unknown>( implementation: T ): SpyFunction<T> { const calls: Array<{ args: Parameters<T>; result: ReturnType<T> }> = []; const spy = ((...args: Parameters<T>) => { const result = implementation(...args) as ReturnType<T>; calls.push({ args, result }); return result; }) as SpyFunction<T>; Object.defineProperty(spy, "calls", { get: () => calls, }); Object.defineProperty(spy, "callCount", { get: () => calls.length, }); spy.reset = () => { calls.length = 0; }; return spy; } /** * Create a mock function that returns predefined values */ export function createMock<T>(returnValues: T[]): () => T { let callIndex = 0; return () => { if (callIndex >= returnValues.length) { throw new Error("Mock called more times than return values provided"); } return returnValues[callIndex++]; }; } /** * Deep clone an object * * @param obj - Object to clone * @returns Cloned object */ export function deepClone<T>(obj: T): T { return JSON.parse(JSON.stringify(obj)); } /** * Compare two objects for deep equality * * @param a - First object * @param b - Second object * @returns True if objects are deeply equal */ export function deepEqual(a: unknown, b: unknown): boolean { if (a === b) return true; if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) { return false; } const keysA = Object.keys(a); const keysB = Object.keys(b); if (keysA.length !== keysB.length) return false; for (const key of keysA) { if (!keysB.includes(key)) return false; if (!deepEqual((a as Record<string, unknown>)[key], (b as Record<string, unknown>)[key])) { return false; } } return true; } /** * Format bytes to human readable string * * @param bytes - Number of bytes * @returns Formatted string */ export function formatBytes(bytes: number): string { if (bytes === 0) return "0 Bytes"; const k = 1024; const sizes = ["Bytes", "KB", "MB", "GB"]; const i = Math.floor(Math.log(bytes) / Math.log(k)); return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`; } /** * Format duration to human readable string * * @param ms - Duration in milliseconds * @returns Formatted string */ export function formatDuration(ms: number): string { if (ms < 1000) return `${ms.toFixed(0)}ms`; if (ms < 60000) return `${(ms / 1000).toFixed(2)}s`; if (ms < 3600000) return `${(ms / 60000).toFixed(2)}m`; return `${(ms / 3600000).toFixed(2)}h`; } /** * Calculate percentiles from array of numbers * * @param values - Array of numbers * @param percentiles - Array of percentiles to calculate (0-1) * @returns Map of percentile to value */ export function calculatePercentiles( values: number[], percentiles: number[] = [0.5, 0.95, 0.99] ): Map<number, number> { const sorted = [...values].sort((a, b) => a - b); const result = new Map<number, number>(); for (const p of percentiles) { const index = Math.floor(sorted.length * p); result.set(p, sorted[index]); } return result; } /** * Calculate statistics from array of numbers * * @param values - Array of numbers * @returns Statistics object */ export function calculateStats(values: number[]): { count: number; sum: number; mean: number; median: number; min: number; max: number; stdDev: number; } { const count = values.length; const sum = values.reduce((a, b) => a + b, 0); const mean = sum / count; const sorted = [...values].sort((a, b) => a - b); const median = sorted[Math.floor(count / 2)]; const min = Math.min(...values); const max = Math.max(...values); const variance = values.reduce((acc, val) => acc + Math.pow(val - mean, 2), 0) / count; const stdDev = Math.sqrt(variance); return { count, sum, mean, median, min, max, stdDev }; } /** * Create a deferred promise * * @returns Deferred promise with resolve and reject functions */ export function createDeferred<T>(): { promise: Promise<T>; resolve: (value: T) => void; reject: (error: Error) => void; } { let resolve!: (value: T) => void; let reject!: (error: Error) => void; const promise = new Promise<T>((res, rej) => { resolve = res; reject = rej; }); return { promise, resolve, reject }; } /** * Extract data from MCP response with type assertion * This helper safely extracts and casts the data field from MCPResponse * for use in test assertions where the data shape is known * * @param response - MCP response object * @returns The data field cast to the expected type */ export function getMCPData<T = Record<string, unknown>>(response: { success: boolean; data?: { [key: string]: unknown }; }): T { if (!response.data) { throw new Error("Response data is undefined"); } return response.data as unknown as T; }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/keyurgolani/ThoughtMcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server