'use strict'; /** * Unit tests for circuit-breaker.js */ const { describe, it, beforeEach } = require('node:test'); const assert = require('node:assert/strict'); const { CircuitBreaker, CircuitOpenError, STATE } = require('../../src/circuit-breaker'); function makeBreaker(opts = {}) { return new CircuitBreaker({ threshold: 3, cooldownMs: 100, ...opts }); } describe('CircuitBreaker', () => { let breaker; beforeEach(() => { breaker = makeBreaker(); }); it('starts in CLOSED state', () => { assert.equal(breaker.getState(), STATE.CLOSED); assert.equal(breaker.failures, 0); }); it('executes successfully in CLOSED state', async () => { const result = await breaker.execute(async () => 42); assert.equal(result, 42); assert.equal(breaker.getState(), STATE.CLOSED); assert.equal(breaker.failures, 0); }); it('tracks failures below threshold', async () => { const failFn = async () => { throw new Error('fail'); }; await assert.rejects(() => breaker.execute(failFn)); await assert.rejects(() => breaker.execute(failFn)); assert.equal(breaker.getState(), STATE.CLOSED); assert.equal(breaker.failures, 2); }); it('transitions to OPEN after threshold failures', async () => { const failFn = async () => { throw new Error('fail'); }; for (let i = 0; i < 3; i++) { await assert.rejects(() => breaker.execute(failFn)); } assert.equal(breaker.getState(), STATE.OPEN); }); it('rejects calls immediately when OPEN', async () => { const failFn = async () => { throw new Error('fail'); }; for (let i = 0; i < 3; i++) { await assert.rejects(() => breaker.execute(failFn)); } assert.equal(breaker.getState(), STATE.OPEN); await assert.rejects(() => breaker.execute(async () => 'should not run'), CircuitOpenError); }); it('transitions to HALF_OPEN after cooldown', async () => { const failFn = async () => { throw new Error('fail'); }; for (let i = 0; i < 3; i++) { await assert.rejects(() => breaker.execute(failFn)); } assert.equal(breaker.getState(), STATE.OPEN); // Wait for cooldown await sleep(150); // Next call transitions to HALF_OPEN and executes const result = await breaker.execute(async () => 'probe'); assert.equal(result, 'probe'); assert.equal(breaker.getState(), STATE.CLOSED); }); it('transitions HALF_OPEN -> OPEN if probe fails', async () => { const failFn = async () => { throw new Error('fail'); }; for (let i = 0; i < 3; i++) { await assert.rejects(() => breaker.execute(failFn)); } assert.equal(breaker.getState(), STATE.OPEN); await sleep(150); // Probe fails await assert.rejects(() => breaker.execute(failFn)); assert.equal(breaker.getState(), STATE.OPEN); }); it('resets on success after HALF_OPEN', async () => { const failFn = async () => { throw new Error('fail'); }; for (let i = 0; i < 3; i++) { await assert.rejects(() => breaker.execute(failFn)); } await sleep(150); await breaker.execute(async () => 'ok'); assert.equal(breaker.getState(), STATE.CLOSED); assert.equal(breaker.failures, 0); }); it('calls onStateChange callback on transitions', async () => { const changes = []; breaker = makeBreaker({ onStateChange: (newState, oldState) => changes.push({ newState, oldState }), }); const failFn = async () => { throw new Error('fail'); }; for (let i = 0; i < 3; i++) { await assert.rejects(() => breaker.execute(failFn)); } assert.equal(changes.length, 1); assert.equal(changes[0].newState, STATE.OPEN); assert.equal(changes[0].oldState, STATE.CLOSED); }); it('reset() returns to CLOSED', async () => { const failFn = async () => { throw new Error('fail'); }; for (let i = 0; i < 3; i++) { await assert.rejects(() => breaker.execute(failFn)); } assert.equal(breaker.getState(), STATE.OPEN); breaker.reset(); assert.equal(breaker.getState(), STATE.CLOSED); assert.equal(breaker.failures, 0); }); it('getMetrics() returns correct data', () => { const metrics = breaker.getMetrics(); assert.equal(metrics.state, STATE.CLOSED); assert.equal(metrics.failures, 0); assert.equal(metrics.threshold, 3); assert.equal(metrics.openedAt, null); assert.equal(metrics.lastError, null); }); it('getMetrics() reflects open state', async () => { const failFn = async () => { throw new Error('test error'); }; for (let i = 0; i < 3; i++) { await assert.rejects(() => breaker.execute(failFn)); } const metrics = breaker.getMetrics(); assert.equal(metrics.state, STATE.OPEN); assert.ok(metrics.openedAt > 0); assert.equal(metrics.lastError, 'test error'); }); }); function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); }