Skip to content

Add unit tests for circuit-breaker module#269

Closed
nikolasdehor wants to merge 1 commit intoSynkraAI:mainfrom
nikolasdehor:test/circuit-breaker-coverage
Closed

Add unit tests for circuit-breaker module#269
nikolasdehor wants to merge 1 commit intoSynkraAI:mainfrom
nikolasdehor:test/circuit-breaker-coverage

Conversation

@nikolasdehor
Copy link
Contributor

@nikolasdehor nikolasdehor commented Feb 18, 2026

Closes #268

Summary

  • Add 22 unit tests for the circuit-breaker IDS module
  • Cover all state transitions: CLOSED, OPEN, HALF_OPEN
  • Test full lifecycle including probe requests and re-opening
  • No mocks needed — pure class tests

Test Coverage

Area Tests Key Scenarios
Constants/Constructor 5 States, defaults, custom options
isAllowed 4 State-based allow/block
recordSuccess 3 Failure reset, circuit close
recordFailure 5 Threshold, trips, re-open
getStats/reset 3 Diagnostics, full reset
Lifecycle 2 Full state machine cycles
Total 22

Test plan

  • All 22 tests pass
  • No external dependencies

Summary by CodeRabbit

  • Tests
    • Added comprehensive unit test coverage for the circuit breaker module, validating all operational states, state transitions, success/failure scenarios, statistics tracking, and reset functionality to ensure robust behavior across different conditions.

Copilot AI review requested due to automatic review settings February 18, 2026 20:00
@vercel
Copy link

vercel bot commented Feb 18, 2026

@nikolasdehor is attempting to deploy a commit to the Pedro Valério Lopez's projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link

coderabbitai bot commented Feb 18, 2026

Walkthrough

Adds a new test file with 22 unit tests for the CircuitBreaker module, covering state constants, constructor initialization, state transitions (CLOSED/OPEN/HALF_OPEN), success/failure recording, statistics retrieval, reset behavior, and complete lifecycle scenarios.

Changes

Cohort / File(s) Summary
Circuit Breaker Unit Tests
tests/core/ids/circuit-breaker.test.js
Comprehensive test suite validating CircuitBreaker state definitions, constructor behavior, state machine transitions, threshold-based state changes, success/failure recording, statistics structure, reset functionality, timeout-based transitions to HALF_OPEN state, and full lifecycle scenarios including CLOSED→OPEN→HALF_OPEN→CLOSED transitions and failure handling in HALF_OPEN state.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Suggested labels

core, tests

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'Add unit tests for circuit-breaker module' directly and accurately summarizes the main change—adding comprehensive unit tests for the circuit-breaker module.
Linked Issues check ✅ Passed The PR implements all 22 unit tests specified in issue #268, covering constants, constructor, isAllowed, recordSuccess, recordFailure, getStats, reset, and full lifecycle scenarios with proper state transition validation.
Out of Scope Changes check ✅ Passed All changes are scoped to the unit test file (tests/core/ids/circuit-breaker.test.js) with no production code modifications, making the PR focused and on-scope.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds dedicated Jest unit coverage for the IDS CircuitBreaker implementation to validate state transitions (CLOSED/OPEN/HALF_OPEN), counters, and lifecycle behavior.

Changes:

  • Introduces a new circuit-breaker.test.js suite with 22 unit tests.
  • Covers constants/defaults, constructor options, isAllowed, recordSuccess, recordFailure, getStats, reset, and end-to-end lifecycle transitions.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

expect(breaker.getState()).toBe(STATE_OPEN);

// Simulate timeout passing
breaker._lastFailureTime = Date.now() - 61000;
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

61000 is a magic number tied to the default reset timeout (60000ms). To keep the test resilient if defaults change, compute this using DEFAULT_RESET_TIMEOUT_MS (e.g. Date.now() - (DEFAULT_RESET_TIMEOUT_MS + 1000)) rather than hard-coding 61000.

Suggested change
breaker._lastFailureTime = Date.now() - 61000;
breaker._lastFailureTime = Date.now() - (DEFAULT_RESET_TIMEOUT_MS + 1000);

Copilot uses AI. Check for mistakes.
Comment on lines +233 to +237
// 4. Success probe closes circuit
breaker.recordSuccess();
breaker._halfOpenProbeInFlight = true;
breaker.recordSuccess();
breaker._halfOpenProbeInFlight = true;
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this lifecycle test, manually toggling _halfOpenProbeInFlight between recordSuccess() calls bypasses the actual probe-gating logic in isAllowed() and doesn’t exercise the intended HALF_OPEN flow. Consider calling isAllowed() before each probe attempt (or using fake timers) so each success corresponds to an allowed probe and the test validates the real state machine behavior end-to-end.

Suggested change
// 4. Success probe closes circuit
breaker.recordSuccess();
breaker._halfOpenProbeInFlight = true;
breaker.recordSuccess();
breaker._halfOpenProbeInFlight = true;
// 4. Success probes close circuit via allowed HALF_OPEN probes
expect(breaker.isAllowed()).toBe(true);
breaker.recordSuccess();
expect(breaker.isAllowed()).toBe(true);
breaker.recordSuccess();
expect(breaker.isAllowed()).toBe(true);

Copilot uses AI. Check for mistakes.
Comment on lines +96 to +99
breaker._state = STATE_HALF_OPEN;
breaker._halfOpenProbeInFlight = false;

expect(breaker.isAllowed()).toBe(true);
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These assertions set _state / _halfOpenProbeInFlight directly to arrange HALF_OPEN behavior. To keep tests aligned with the public contract, prefer reaching HALF_OPEN via recordFailure() + elapsed timeout + isAllowed() (or fake timers) rather than writing internal fields.

Suggested change
breaker._state = STATE_HALF_OPEN;
breaker._halfOpenProbeInFlight = false;
expect(breaker.isAllowed()).toBe(true);
// Trip the breaker to OPEN
for (let i = 0; i < 5; i++) breaker.recordFailure();
expect(breaker.getState()).toBe(STATE_OPEN);
// Simulate timeout passing to allow transition to HALF_OPEN
breaker._lastFailureTime = Date.now() - 61000;
// First request after timeout should be allowed (probe) and move to HALF_OPEN
expect(breaker.isAllowed()).toBe(true);
expect(breaker.getState()).toBe(STATE_HALF_OPEN);
// Second request while probe is in flight should be blocked

Copilot uses AI. Check for mistakes.
* Tests the CircuitBreaker pattern implementation with
* CLOSED, OPEN, and HALF_OPEN states.
*/

Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All other IDS test files in this repo start with 'use strict'; (e.g. tests/core/ids/framework-governor.test.js, entity-registry-schema.test.js). To match the established test-file convention and avoid subtle strict-mode differences, add 'use strict'; as the first statement in this file.

Suggested change
'use strict';

Copilot uses AI. Check for mistakes.
Comment on lines +85 to +92
for (let i = 0; i < 5; i++) breaker.recordFailure();
expect(breaker.getState()).toBe(STATE_OPEN);

// Simulate timeout passing
breaker._lastFailureTime = Date.now() - 61000;

expect(breaker.isAllowed()).toBe(true);
expect(breaker.getState()).toBe(STATE_HALF_OPEN);
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test forces the OPEN→HALF_OPEN transition by directly mutating the internal _lastFailureTime. That makes the test brittle to implementation refactors. Prefer advancing time via Jest fake timers (jest.useFakeTimers() + jest.setSystemTime() / jest.advanceTimersByTime()) or using a shorter resetTimeoutMs option and real timers, as done in other IDS tests.

Suggested change
for (let i = 0; i < 5; i++) breaker.recordFailure();
expect(breaker.getState()).toBe(STATE_OPEN);
// Simulate timeout passing
breaker._lastFailureTime = Date.now() - 61000;
expect(breaker.isAllowed()).toBe(true);
expect(breaker.getState()).toBe(STATE_HALF_OPEN);
// Use fake timers to simulate the reset timeout elapsing
jest.useFakeTimers().setSystemTime(new Date('2020-01-01T00:00:00Z'));
for (let i = 0; i < 5; i++) breaker.recordFailure();
expect(breaker.getState()).toBe(STATE_OPEN);
// Advance time past the default reset timeout (60s)
jest.setSystemTime(new Date('2020-01-01T00:01:01Z'));
expect(breaker.isAllowed()).toBe(true);
expect(breaker.getState()).toBe(STATE_HALF_OPEN);
jest.useRealTimers();

Copilot uses AI. Check for mistakes.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
tests/core/ids/circuit-breaker.test.js (1)

84-101: Consider jest.useFakeTimers() instead of mutating _lastFailureTime.

Both the timeout transition test (line 89) and the probe-gating test (lines 96–97) directly mutate private implementation fields (_lastFailureTime, _state, _halfOpenProbeInFlight) to set up state. This creates tight coupling to field names and skips the public isAllowed() probe-entry path.

Using fake timers would remove the need for _lastFailureTime mutation, and the public trip → advance-time → isAllowed() sequence would correctly set up _halfOpenProbeInFlight without direct assignment.

♻️ Suggested approach using fake timers
+  beforeEach(() => {
+    jest.useFakeTimers();
+  });
+
+  afterEach(() => {
+    jest.useRealTimers();
+  });

   test('transitions to HALF_OPEN after reset timeout', () => {
     for (let i = 0; i < 5; i++) breaker.recordFailure();
     expect(breaker.getState()).toBe(STATE_OPEN);

-    // Simulate timeout passing
-    breaker._lastFailureTime = Date.now() - 61000;
+    jest.advanceTimersByTime(61000);

     expect(breaker.isAllowed()).toBe(true);
     expect(breaker.getState()).toBe(STATE_HALF_OPEN);
   });

   test('allows one probe in HALF_OPEN', () => {
-    breaker._state = STATE_HALF_OPEN;
-    breaker._halfOpenProbeInFlight = false;
+    for (let i = 0; i < 5; i++) breaker.recordFailure();
+    jest.advanceTimersByTime(61000);
+    breaker.isAllowed(); // transitions to HALF_OPEN, probe in flight
+    // Now advance time again so second isAllowed call doesn't re-enter via timeout
+    // (state is already HALF_OPEN; probe is in flight)

     expect(breaker.isAllowed()).toBe(true);
     expect(breaker.isAllowed()).toBe(false);
   });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/core/ids/circuit-breaker.test.js` around lines 84 - 101, Replace direct
private-field manipulation in the tests with public API + fake timers: call
breaker.recordFailure() until it trips to STATE_OPEN, then use
jest.useFakeTimers() and jest.advanceTimersByTime(61000) (or the circuit's reset
timeout) before calling breaker.isAllowed() to trigger the HALF_OPEN transition
instead of setting breaker._lastFailureTime; for the probe-gating test, set
breaker._state to STATE_HALF_OPEN only if you can via public behavior (e.g.,
trip → advance timers → isAllowed()) and then call breaker.isAllowed() twice to
assert the first probe is allowed and the second is blocked, removing direct
mutations of _state and _halfOpenProbeInFlight and relying on
breaker.recordFailure(), breaker.isAllowed(), STATE_OPEN and STATE_HALF_OPEN to
drive state.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/core/ids/circuit-breaker.test.js`:
- Around line 204-213: The test "resets to CLOSED state" omits assertions for
stats fields lastFailureTime and totalTrips after calling breaker.reset();
update the test to call breaker.getStats() and assert that stats.lastFailureTime
is null and stats.totalTrips matches the intended contract (either 0 if reset
should clear trips or preserved value if spec dictates) so the reset() behavior
for lastFailureTime and totalTrips is explicitly verified alongside getState()
and counts; reference the breaker.reset(), breaker.getStats(),
stats.lastFailureTime, and stats.totalTrips symbols when making the assertions.
- Around line 234-238: The test currently bypasses the public gating logic by
setting breaker._halfOpenProbeInFlight = true directly between
breaker.recordSuccess() calls; change the test to drive probe cycles via the
public isAllowed() method instead (i.e., call breaker.isAllowed() before each
simulated service call and only then call breaker.recordSuccess()), so the
HALF_OPEN gating logic is exercised rather than mutating the internal
_halfOpenProbeInFlight flag directly.

---

Nitpick comments:
In `@tests/core/ids/circuit-breaker.test.js`:
- Around line 84-101: Replace direct private-field manipulation in the tests
with public API + fake timers: call breaker.recordFailure() until it trips to
STATE_OPEN, then use jest.useFakeTimers() and jest.advanceTimersByTime(61000)
(or the circuit's reset timeout) before calling breaker.isAllowed() to trigger
the HALF_OPEN transition instead of setting breaker._lastFailureTime; for the
probe-gating test, set breaker._state to STATE_HALF_OPEN only if you can via
public behavior (e.g., trip → advance timers → isAllowed()) and then call
breaker.isAllowed() twice to assert the first probe is allowed and the second is
blocked, removing direct mutations of _state and _halfOpenProbeInFlight and
relying on breaker.recordFailure(), breaker.isAllowed(), STATE_OPEN and
STATE_HALF_OPEN to drive state.

Comment on lines +204 to +213
test('resets to CLOSED state', () => {
for (let i = 0; i < 5; i++) breaker.recordFailure();
expect(breaker.getState()).toBe(STATE_OPEN);

breaker.reset();

expect(breaker.getState()).toBe(STATE_CLOSED);
expect(breaker.getStats().failureCount).toBe(0);
expect(breaker.getStats().successCount).toBe(0);
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

reset test is missing assertions for lastFailureTime and totalTrips.

After calling reset(), neither lastFailureTime (expected null) nor totalTrips (whatever the contract is — preserved or zeroed) is verified. Both are part of the getStats() surface and their post-reset values are an important behavioral contract.

🛡️ Proposed additional assertions
   breaker.reset();

   expect(breaker.getState()).toBe(STATE_CLOSED);
   expect(breaker.getStats().failureCount).toBe(0);
   expect(breaker.getStats().successCount).toBe(0);
+  expect(breaker.getStats().lastFailureTime).toBeNull();
+  // totalTrips: assert the expected value (0 if reset() clears it, 1 if it's preserved)
+  expect(breaker.getStats().totalTrips).toBe(/* 0 or 1 per implementation contract */);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/core/ids/circuit-breaker.test.js` around lines 204 - 213, The test
"resets to CLOSED state" omits assertions for stats fields lastFailureTime and
totalTrips after calling breaker.reset(); update the test to call
breaker.getStats() and assert that stats.lastFailureTime is null and
stats.totalTrips matches the intended contract (either 0 if reset should clear
trips or preserved value if spec dictates) so the reset() behavior for
lastFailureTime and totalTrips is explicitly verified alongside getState() and
counts; reference the breaker.reset(), breaker.getStats(),
stats.lastFailureTime, and stats.totalTrips symbols when making the assertions.

Comment on lines +234 to +238
breaker.recordSuccess();
breaker._halfOpenProbeInFlight = true;
breaker.recordSuccess();
breaker._halfOpenProbeInFlight = true;
breaker.recordSuccess();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Lifecycle test bypasses isAllowed() for the 2nd and 3rd probe cycles.

After the first recordSuccess() at line 234 (which follows the legitimate isAllowed() at line 230), the test manually resets _halfOpenProbeInFlight = true at lines 235 and 237 instead of calling isAllowed() again. This skips exercising whether isAllowed() correctly re-gates probes after a success in HALF_OPEN. A bug where isAllowed() in HALF_OPEN mis-gates subsequent probes would not be caught.

The probe cycle should follow the real usage pattern: isAllowed() → service call → recordSuccess().

🛡️ Corrected probe sequence for the lifecycle test
   // 4. Success probe closes circuit
-  breaker.recordSuccess();
-  breaker._halfOpenProbeInFlight = true;
-  breaker.recordSuccess();
-  breaker._halfOpenProbeInFlight = true;
-  breaker.recordSuccess();
+  // Probe 1: isAllowed() was already called above (line 230), probe is in flight
+  breaker.recordSuccess(); // success 1
+  // Probe 2
+  expect(breaker.isAllowed()).toBe(true); // should allow next probe
+  breaker.recordSuccess(); // success 2
+  // Probe 3
+  expect(breaker.isAllowed()).toBe(true); // should allow next probe
+  breaker.recordSuccess(); // success 3 → closes circuit
   expect(breaker.getState()).toBe(STATE_CLOSED);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
breaker.recordSuccess();
breaker._halfOpenProbeInFlight = true;
breaker.recordSuccess();
breaker._halfOpenProbeInFlight = true;
breaker.recordSuccess();
// Probe 1: isAllowed() was already called above (line 230), probe is in flight
breaker.recordSuccess(); // success 1
// Probe 2
expect(breaker.isAllowed()).toBe(true); // should allow next probe
breaker.recordSuccess(); // success 2
// Probe 3
expect(breaker.isAllowed()).toBe(true); // should allow next probe
breaker.recordSuccess(); // success 3 → closes circuit
expect(breaker.getState()).toBe(STATE_CLOSED);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/core/ids/circuit-breaker.test.js` around lines 234 - 238, The test
currently bypasses the public gating logic by setting
breaker._halfOpenProbeInFlight = true directly between breaker.recordSuccess()
calls; change the test to drive probe cycles via the public isAllowed() method
instead (i.e., call breaker.isAllowed() before each simulated service call and
only then call breaker.recordSuccess()), so the HALF_OPEN gating logic is
exercised rather than mutating the internal _halfOpenProbeInFlight flag
directly.

@nikolasdehor
Copy link
Contributor Author

Consolidated into #426

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add unit tests for circuit-breaker module

2 participants