Skip to main content
Glama
error-handler.test.ts13.4 kB
/** * Tests for CognitiveErrorHandler * * Validates error handling, recovery strategies, graceful degradation, * circuit breaker pattern, and user message generation. */ import { beforeEach, describe, expect, it, vi } from "vitest"; import { CognitiveErrorHandler } from "../../../utils/error-handler.js"; import { CognitiveError, DatabaseError, EmbeddingError, ErrorCodes, ReasoningError, ValidationError, } from "../../../utils/errors.js"; import { Logger } from "../../../utils/logger.js"; describe("CognitiveErrorHandler", () => { let handler: CognitiveErrorHandler; beforeEach(() => { handler = new CognitiveErrorHandler(); vi.clearAllMocks(); vi.spyOn(Logger, "error").mockImplementation(() => {}); vi.spyOn(Logger, "warn").mockImplementation(() => {}); vi.spyOn(Logger, "info").mockImplementation(() => {}); }); describe("handleError", () => { it("should handle DatabaseError with connection retry", async () => { const error = new DatabaseError("Connection failed", ErrorCodes.DB_CONNECTION_FAILED, { component: "MemoryRepository", operation: "query", }); const result = await handler.handleError(error); expect(result.recovered).toBe(true); expect(result.strategy).toBe("connection_retry"); expect(result.userMessage).toContain("database"); expect(Logger.error).toHaveBeenCalled(); }); it("should handle DatabaseError with transaction rollback", async () => { const error = new DatabaseError("Transaction failed", ErrorCodes.DB_TRANSACTION_FAILED, { component: "MemoryRepository", operation: "create", }); const result = await handler.handleError(error); expect(result.recovered).toBe(true); expect(result.strategy).toBe("transaction_rollback"); expect(result.userMessage).toContain("try again"); }); it("should handle DatabaseError with query simplification", async () => { const error = new DatabaseError("Query timeout", ErrorCodes.DB_QUERY_TIMEOUT, { component: "SearchEngine", operation: "search", }); const result = await handler.handleError(error); expect(result.recovered).toBe(true); expect(result.strategy).toBe("query_simplification"); expect(result.userMessage).toContain("simpler"); }); it("should handle EmbeddingError with cache fallback", async () => { const error = new EmbeddingError( "Embedding timeout", ErrorCodes.EMBEDDING_TIMEOUT, "nomic-embed-text", 1000, { component: "EmbeddingEngine", operation: "generate" } ); const result = await handler.handleError(error); expect(result.recovered).toBe(true); expect(result.strategy).toBe("cache_fallback"); expect(result.userMessage).toContain("cached"); }); it("should handle EmbeddingError with model switching", async () => { const error = new EmbeddingError( "Model unavailable", ErrorCodes.EMBEDDING_MODEL_UNAVAILABLE, "nomic-embed-text", 1000, { component: "EmbeddingEngine", operation: "generate" } ); const result = await handler.handleError(error); expect(result.recovered).toBe(true); expect(result.strategy).toBe("model_switching"); expect(result.userMessage).toContain("alternative"); }); it("should handle ReasoningError with framework fallback", async () => { const error = new ReasoningError( "Framework failed", ErrorCodes.REASONING_FRAMEWORK_FAILED, { component: "FrameworkSelector", operation: "execute" }, true, "scientific-method" ); const result = await handler.handleError(error); expect(result.recovered).toBe(true); expect(result.strategy).toBe("framework_fallback"); expect(result.userMessage).toContain("simpler approach"); }); it("should handle ReasoningError with stream continuation", async () => { const error = new ReasoningError( "Stream timeout", ErrorCodes.REASONING_STREAM_TIMEOUT, { component: "ReasoningOrchestrator", operation: "execute" }, true, undefined, "synthesis" ); const result = await handler.handleError(error); expect(result.recovered).toBe(true); expect(result.strategy).toBe("stream_continuation"); expect(result.userMessage).toContain("partial results"); }); it("should not recover from ValidationError", async () => { const error = new ValidationError("Invalid field", "userId", null, "required"); const result = await handler.handleError(error); expect(result.recovered).toBe(false); expect(result.strategy).toBe("none"); expect(result.userMessage).toContain("Invalid"); }); it("should not recover from non-recoverable errors", async () => { const error = new CognitiveError( "Fatal error", ErrorCodes.UNKNOWN_ERROR, {}, false // not recoverable ); const result = await handler.handleError(error); expect(result.recovered).toBe(false); expect(result.strategy).toBe("none"); }); }); describe("graceful degradation", () => { it("should degrade to basic mode after multiple failures", async () => { const error = new DatabaseError("Connection failed", ErrorCodes.DB_CONNECTION_FAILED, { component: "MemoryRepository", }); // Simulate multiple failures for (let i = 0; i < 5; i++) { await handler.handleError(error); } const degraded = handler.isInBasicMode(); expect(degraded).toBe(true); }); it("should provide basic mode status", () => { expect(handler.isInBasicMode()).toBe(false); handler.degradeToBasicMode(); expect(handler.isInBasicMode()).toBe(true); }); it("should allow recovery from basic mode", () => { handler.degradeToBasicMode(); expect(handler.isInBasicMode()).toBe(true); handler.recoverFromBasicMode(); expect(handler.isInBasicMode()).toBe(false); }); }); describe("circuit breaker", () => { it("should open circuit after threshold failures", async () => { const error = new DatabaseError("Connection failed", ErrorCodes.DB_CONNECTION_FAILED, { component: "MemoryRepository", }); // Trigger circuit breaker (default threshold is 5) for (let i = 0; i < 5; i++) { await handler.handleError(error); } const result = await handler.handleError(error); expect(result.recovered).toBe(false); expect(result.strategy).toBe("circuit_open"); expect(result.userMessage).toContain("temporarily unavailable"); }); it("should reset circuit after successful recovery", async () => { const error = new DatabaseError("Connection failed", ErrorCodes.DB_CONNECTION_FAILED, { component: "MemoryRepository", }); // Trigger some failures await handler.handleError(error); await handler.handleError(error); // Simulate successful operation handler.recordSuccess(); // Circuit should be reset const result = await handler.handleError(error); expect(result.recovered).toBe(true); }); it("should track failure count per error type", async () => { const dbError = new DatabaseError("Connection failed", ErrorCodes.DB_CONNECTION_FAILED, {}); const embeddingError = new EmbeddingError( "Timeout", ErrorCodes.EMBEDDING_TIMEOUT, "model", 100, {} ); // Different error types should have separate counters await handler.handleError(dbError); await handler.handleError(embeddingError); const stats = handler.getErrorStats(); expect(stats.DB_CONNECTION_FAILED).toBeGreaterThan(0); expect(stats.EMBEDDING_TIMEOUT).toBeGreaterThan(0); }); }); describe("recovery time tracking", () => { it("should track recovery time for successful recoveries", async () => { const error = new DatabaseError("Connection failed", ErrorCodes.DB_CONNECTION_FAILED, {}); const result = await handler.handleError(error); expect(result.recoveryTimeMs).toBeGreaterThan(0); expect(result.recoveryTimeMs).toBeLessThan(5000); // Should be fast }); it("should not track recovery time for failed recoveries", async () => { const error = new ValidationError("Invalid", "field", null, "required"); const result = await handler.handleError(error); expect(result.recoveryTimeMs).toBe(0); }); }); describe("exponential backoff", () => { it("should increase delay with each retry attempt", async () => { const error = new DatabaseError("Connection failed", ErrorCodes.DB_CONNECTION_FAILED, {}); const times: number[] = []; for (let i = 0; i < 3; i++) { const start = Date.now(); await handler.handleError(error); times.push(Date.now() - start); } // Each retry should take longer (exponential backoff) expect(times[1]).toBeGreaterThanOrEqual(times[0]); expect(times[2]).toBeGreaterThanOrEqual(times[1]); }); }); describe("user message generation", () => { it("should generate clear user messages for database errors", async () => { const error = new DatabaseError("Connection failed", ErrorCodes.DB_CONNECTION_FAILED, {}); const result = await handler.handleError(error); expect(result.userMessage).not.toContain("stack"); expect(result.userMessage).not.toContain("undefined"); expect(result.userMessage.length).toBeGreaterThan(10); }); it("should generate actionable messages", async () => { const error = new DatabaseError("Query timeout", ErrorCodes.DB_QUERY_TIMEOUT, {}); const result = await handler.handleError(error); expect(result.userMessage).toMatch(/try|simpler|again/i); }); it("should avoid technical jargon in user messages", async () => { const error = new EmbeddingError( "Model unavailable", ErrorCodes.EMBEDDING_MODEL_UNAVAILABLE, "nomic-embed-text", 1000, {} ); const result = await handler.handleError(error); expect(result.userMessage).not.toContain("nomic-embed-text"); expect(result.userMessage).not.toContain("vector"); // User message should be simple and clear expect(result.userMessage.length).toBeGreaterThan(10); }); }); describe("error logging", () => { it("should log errors with context", async () => { const error = new DatabaseError("Connection failed", ErrorCodes.DB_CONNECTION_FAILED, { component: "MemoryRepository", operation: "query", userId: "user123", }); await handler.handleError(error); expect(Logger.error).toHaveBeenCalledWith( "CognitiveError", expect.objectContaining({ code: ErrorCodes.DB_CONNECTION_FAILED, name: "DatabaseError", context: expect.objectContaining({ component: "MemoryRepository", operation: "query", }), }) ); }); it("should log recovery attempts", async () => { const error = new DatabaseError("Connection failed", ErrorCodes.DB_CONNECTION_FAILED, {}); await handler.handleError(error); expect(Logger.info).toHaveBeenCalledWith( expect.stringContaining("Recovery"), expect.anything() ); }); it("should log circuit breaker state changes", async () => { const error = new DatabaseError("Connection failed", ErrorCodes.DB_CONNECTION_FAILED, {}); // Trigger circuit breaker for (let i = 0; i < 5; i++) { await handler.handleError(error); } expect(Logger.warn).toHaveBeenCalledWith( expect.stringContaining("Circuit breaker"), expect.anything() ); }); }); describe("edge cases", () => { it("should handle unknown error types", async () => { const error = new Error("Unknown error"); const result = await handler.handleError(error); expect(result.recovered).toBe(false); expect(result.userMessage).toContain("unexpected"); }); it("should handle errors without context", async () => { const error = new DatabaseError("Error", ErrorCodes.DB_CONNECTION_FAILED); const result = await handler.handleError(error); expect(result.recovered).toBe(true); expect(result.userMessage).toBeDefined(); }); it("should handle cascading failures", async () => { const error1 = new DatabaseError("Error 1", ErrorCodes.DB_CONNECTION_FAILED, {}); const error2 = new EmbeddingError("Error 2", ErrorCodes.EMBEDDING_TIMEOUT, "model", 100, {}); await handler.handleError(error1); const result = await handler.handleError(error2); expect(result.recovered).toBe(true); }); it("should handle partial recovery scenarios", async () => { const error = new ReasoningError( "Stream timeout", ErrorCodes.REASONING_STREAM_TIMEOUT, {}, true ); const result = await handler.handleError(error); expect(result.recovered).toBe(true); expect(result.userMessage).toContain("partial"); }); }); });

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