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
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});