WIP! A BB-style forum, on the ATmosphere! We're still working... we'll be back soon when we have something to show off!
node typescript hono htmx atproto
at root/atb-56-theme-caching-layer 180 lines 6.3 kB view raw
1import { describe, it, expect, beforeEach, vi, afterEach } from "vitest"; 2import { CircuitBreaker } from "../circuit-breaker.js"; 3import { createMockLogger } from "./mock-logger.js"; 4 5describe("CircuitBreaker", () => { 6 let onBreak: ReturnType<typeof vi.fn>; 7 let circuitBreaker: CircuitBreaker; 8 let mockLogger: ReturnType<typeof createMockLogger>; 9 10 beforeEach(() => { 11 onBreak = vi.fn().mockResolvedValue(undefined); 12 mockLogger = createMockLogger(); 13 }); 14 15 afterEach(() => { 16 vi.clearAllMocks(); 17 }); 18 19 describe("Construction", () => { 20 it("should initialize with maxFailures and onBreak callback", () => { 21 expect(() => { 22 circuitBreaker = new CircuitBreaker(5, onBreak, mockLogger); 23 }).not.toThrow(); 24 25 expect(circuitBreaker.getFailureCount()).toBe(0); 26 }); 27 }); 28 29 describe("execute", () => { 30 beforeEach(() => { 31 circuitBreaker = new CircuitBreaker(3, onBreak, mockLogger); 32 }); 33 34 it("should execute operation successfully and reset counter", async () => { 35 const operation = vi.fn().mockResolvedValue("success"); 36 37 await circuitBreaker.execute(operation, "test-operation"); 38 39 expect(operation).toHaveBeenCalledOnce(); 40 expect(circuitBreaker.getFailureCount()).toBe(0); 41 expect(onBreak).not.toHaveBeenCalled(); 42 }); 43 44 it("should track consecutive failures", async () => { 45 const operation = vi.fn().mockRejectedValue(new Error("Operation failed")); 46 47 await circuitBreaker.execute(operation, "test-operation"); 48 expect(circuitBreaker.getFailureCount()).toBe(1); 49 50 await circuitBreaker.execute(operation, "test-operation"); 51 expect(circuitBreaker.getFailureCount()).toBe(2); 52 53 expect(onBreak).not.toHaveBeenCalled(); 54 }); 55 56 it("should trigger onBreak when max failures reached", async () => { 57 const operation = vi.fn().mockRejectedValue(new Error("Operation failed")); 58 59 // Fail 3 times (maxFailures = 3) 60 await circuitBreaker.execute(operation, "test-operation"); 61 await circuitBreaker.execute(operation, "test-operation"); 62 await circuitBreaker.execute(operation, "test-operation"); 63 64 expect(circuitBreaker.getFailureCount()).toBe(3); 65 expect(onBreak).toHaveBeenCalledOnce(); 66 }); 67 68 it("should log failures with operation name", async () => { 69 const operation = vi.fn().mockRejectedValue(new Error("Operation failed")); 70 71 await circuitBreaker.execute(operation, "custom-operation"); 72 73 expect(mockLogger.error).toHaveBeenCalledWith( 74 expect.stringContaining("Circuit breaker"), 75 expect.objectContaining({ operationName: "custom-operation" }) 76 ); 77 }); 78 79 it("should log when circuit breaks", async () => { 80 const operation = vi.fn().mockRejectedValue(new Error("Operation failed")); 81 82 // Trigger circuit breaker 83 await circuitBreaker.execute(operation, "test-operation"); 84 await circuitBreaker.execute(operation, "test-operation"); 85 await circuitBreaker.execute(operation, "test-operation"); 86 87 expect(mockLogger.error).toHaveBeenCalledWith( 88 expect.stringContaining("max consecutive failures"), 89 expect.objectContaining({ maxFailures: 3 }) 90 ); 91 }); 92 93 it("should reset counter after successful operation", async () => { 94 const failingOp = vi.fn().mockRejectedValue(new Error("Failed")); 95 const successOp = vi.fn().mockResolvedValue("success"); 96 97 // Fail twice 98 await circuitBreaker.execute(failingOp, "failing-op"); 99 await circuitBreaker.execute(failingOp, "failing-op"); 100 expect(circuitBreaker.getFailureCount()).toBe(2); 101 102 // Succeed once - should reset counter 103 await circuitBreaker.execute(successOp, "success-op"); 104 expect(circuitBreaker.getFailureCount()).toBe(0); 105 106 // Verify onBreak was never called 107 expect(onBreak).not.toHaveBeenCalled(); 108 }); 109 }); 110 111 describe("reset", () => { 112 beforeEach(() => { 113 circuitBreaker = new CircuitBreaker(3, onBreak, mockLogger); 114 }); 115 116 it("should reset failure counter", async () => { 117 const operation = vi.fn().mockRejectedValue(new Error("Failed")); 118 119 await circuitBreaker.execute(operation, "test-operation"); 120 await circuitBreaker.execute(operation, "test-operation"); 121 expect(circuitBreaker.getFailureCount()).toBe(2); 122 123 circuitBreaker.reset(); 124 expect(circuitBreaker.getFailureCount()).toBe(0); 125 }); 126 }); 127 128 describe("getFailureCount", () => { 129 beforeEach(() => { 130 circuitBreaker = new CircuitBreaker(3, onBreak, mockLogger); 131 }); 132 133 it("should return current failure count", async () => { 134 const operation = vi.fn().mockRejectedValue(new Error("Failed")); 135 136 expect(circuitBreaker.getFailureCount()).toBe(0); 137 138 await circuitBreaker.execute(operation, "test-operation"); 139 expect(circuitBreaker.getFailureCount()).toBe(1); 140 141 await circuitBreaker.execute(operation, "test-operation"); 142 expect(circuitBreaker.getFailureCount()).toBe(2); 143 }); 144 }); 145 146 describe("Edge Cases", () => { 147 it("should handle onBreak callback errors", async () => { 148 const failingOnBreak = vi.fn().mockRejectedValue(new Error("onBreak failed")); 149 circuitBreaker = new CircuitBreaker(2, failingOnBreak, mockLogger); 150 151 const operation = vi.fn().mockRejectedValue(new Error("Operation failed")); 152 153 // This should not throw even if onBreak fails 154 await circuitBreaker.execute(operation, "test-operation"); 155 156 // Second call triggers the circuit breaker, which calls the failing onBreak 157 // We need to catch the unhandled promise rejection from onBreak 158 try { 159 await circuitBreaker.execute(operation, "test-operation"); 160 // Wait a bit for the onBreak promise to be handled 161 await new Promise(resolve => setTimeout(resolve, 10)); 162 } catch { 163 // Ignore error from onBreak 164 } 165 166 expect(failingOnBreak).toHaveBeenCalled(); 167 }); 168 169 it("should handle maxFailures of 1", async () => { 170 circuitBreaker = new CircuitBreaker(1, onBreak, mockLogger); 171 172 const operation = vi.fn().mockRejectedValue(new Error("Failed")); 173 174 await circuitBreaker.execute(operation, "test-operation"); 175 176 expect(circuitBreaker.getFailureCount()).toBe(1); 177 expect(onBreak).toHaveBeenCalledOnce(); 178 }); 179 }); 180});