@toolprint/hypertool-mcp
Version:
HyperTool MCP proxy server for routing requests between clients and multiple underlying MCP servers
228 lines • 6.22 kB
JavaScript
/**
* Async test helpers for preventing hanging tests
* Provides utilities for timeouts, retries, and cleanup
*/
import { vi } from "vitest";
/**
* Timeout configuration for different test types
*/
export const TEST_TIMEOUTS = {
unit: 3000, // 3 seconds for unit tests
integration: 10000, // 10 seconds for integration tests
e2e: 30000, // 30 seconds for e2e tests
default: 5000, // 5 seconds default
};
/**
* Promise with timeout utility
* Prevents tests from hanging indefinitely
*/
export function withTimeout(promise, timeoutMs = TEST_TIMEOUTS.default, errorMessage) {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
reject(new Error(errorMessage || `Operation timed out after ${timeoutMs}ms`));
}, timeoutMs);
promise
.then((result) => {
clearTimeout(timer);
resolve(result);
})
.catch((error) => {
clearTimeout(timer);
reject(error);
});
});
}
/**
* Retry utility for flaky operations
* Useful for tests that occasionally fail due to timing issues
*/
export async function retry(fn, options = {}) {
const { maxRetries = 3, delay = 100, backoff = true, timeout = TEST_TIMEOUTS.default, } = options;
let lastError;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await withTimeout(fn(), timeout);
}
catch (error) {
lastError = error;
if (attempt < maxRetries - 1) {
const waitTime = backoff ? delay * Math.pow(2, attempt) : delay;
await new Promise((resolve) => setTimeout(resolve, waitTime));
}
}
}
throw lastError;
}
/**
* Wait for a condition to be true
* Useful for waiting for async state changes
*/
export async function waitFor(condition, options = {}) {
const { timeout = TEST_TIMEOUTS.default, interval = 50, errorMessage = "Condition not met within timeout", } = options;
const startTime = Date.now();
while (Date.now() - startTime < timeout) {
const result = await condition();
if (result) {
return;
}
await new Promise((resolve) => setTimeout(resolve, interval));
}
throw new Error(errorMessage);
}
/**
* Create a deferred promise for testing async flows
*/
export function createDeferred() {
let resolve;
let reject;
const promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
return { promise, resolve: resolve, reject: reject };
}
/**
* Test fixture for managing async resources
* Ensures cleanup even if test fails
*/
export class AsyncTestFixture {
cleanupFns = [];
activeTimers = new Set();
activeIntervals = new Set();
/**
* Register a cleanup function
*/
addCleanup(fn) {
this.cleanupFns.push(fn);
}
/**
* Create a timer that will be automatically cleaned up
*/
setTimeout(fn, delay) {
const timer = setTimeout(() => {
this.activeTimers.delete(timer);
fn();
}, delay);
this.activeTimers.add(timer);
return timer;
}
/**
* Create an interval that will be automatically cleaned up
*/
setInterval(fn, delay) {
const interval = setInterval(fn, delay);
this.activeIntervals.add(interval);
return interval;
}
/**
* Clear a timer
*/
clearTimeout(timer) {
clearTimeout(timer);
this.activeTimers.delete(timer);
}
/**
* Clear an interval
*/
clearInterval(interval) {
clearInterval(interval);
this.activeIntervals.delete(interval);
}
/**
* Run all cleanup functions
*/
async cleanup() {
// Clear all timers
for (const timer of this.activeTimers) {
clearTimeout(timer);
}
for (const interval of this.activeIntervals) {
clearInterval(interval);
}
this.activeTimers.clear();
this.activeIntervals.clear();
// Run cleanup functions in reverse order
const fns = [...this.cleanupFns].reverse();
this.cleanupFns = [];
for (const fn of fns) {
try {
await fn();
}
catch (error) {
console.error("Cleanup error:", error);
}
}
}
}
/**
* Mock timer utilities for deterministic async tests
*/
export const mockTimers = {
/**
* Use fake timers and return cleanup function
*/
useFakeTimers() {
vi.useFakeTimers();
return () => {
vi.clearAllTimers();
vi.useRealTimers();
};
},
/**
* Advance timers by time and flush promises
*/
async advanceTimersByTime(ms) {
vi.advanceTimersByTime(ms);
await flushPromises();
},
/**
* Run all pending timers and flush promises
*/
async runAllTimers() {
vi.runAllTimers();
await flushPromises();
},
/**
* Run only pending timers (not recursive) and flush promises
*/
async runOnlyPendingTimers() {
vi.runOnlyPendingTimers();
await flushPromises();
},
};
/**
* Flush all pending promises
* Useful for ensuring async operations complete
*/
export function flushPromises() {
return new Promise((resolve) => {
setImmediate(resolve);
});
}
/**
* Create a test timeout guard
* Automatically fails test if it takes too long
*/
export function createTimeoutGuard(timeoutMs = TEST_TIMEOUTS.default) {
const timer = setTimeout(() => {
throw new Error(`Test exceeded timeout of ${timeoutMs}ms`);
}, timeoutMs);
return {
cancel: () => clearTimeout(timer),
};
}
/**
* Wrap an async test with automatic timeout
*/
export function withTestTimeout(testFn, timeoutMs = TEST_TIMEOUTS.default) {
return async () => {
const guard = createTimeoutGuard(timeoutMs);
try {
return await testFn();
}
finally {
guard.cancel();
}
};
}
//# sourceMappingURL=async-helpers.js.map