-
-
Notifications
You must be signed in to change notification settings - Fork 938
Add unit tests for circuit-breaker module #269
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,252 @@ | ||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||
| * Unit tests for circuit-breaker module | ||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||
| * Tests the CircuitBreaker pattern implementation with | ||||||||||||||||||||||||||||||||||||||||||||
| * CLOSED, OPEN, and HALF_OPEN states. | ||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| const { | ||||||||||||||||||||||||||||||||||||||||||||
| CircuitBreaker, | ||||||||||||||||||||||||||||||||||||||||||||
| STATE_CLOSED, | ||||||||||||||||||||||||||||||||||||||||||||
| STATE_OPEN, | ||||||||||||||||||||||||||||||||||||||||||||
| STATE_HALF_OPEN, | ||||||||||||||||||||||||||||||||||||||||||||
| DEFAULT_FAILURE_THRESHOLD, | ||||||||||||||||||||||||||||||||||||||||||||
| DEFAULT_SUCCESS_THRESHOLD, | ||||||||||||||||||||||||||||||||||||||||||||
| DEFAULT_RESET_TIMEOUT_MS, | ||||||||||||||||||||||||||||||||||||||||||||
| } = require('../../../.aios-core/core/ids/circuit-breaker'); | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| describe('CircuitBreaker', () => { | ||||||||||||||||||||||||||||||||||||||||||||
| let breaker; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| beforeEach(() => { | ||||||||||||||||||||||||||||||||||||||||||||
| breaker = new CircuitBreaker(); | ||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| // ============================================================ | ||||||||||||||||||||||||||||||||||||||||||||
| // Constants | ||||||||||||||||||||||||||||||||||||||||||||
| // ============================================================ | ||||||||||||||||||||||||||||||||||||||||||||
| describe('constants', () => { | ||||||||||||||||||||||||||||||||||||||||||||
| test('state constants are defined', () => { | ||||||||||||||||||||||||||||||||||||||||||||
| expect(STATE_CLOSED).toBe('CLOSED'); | ||||||||||||||||||||||||||||||||||||||||||||
| expect(STATE_OPEN).toBe('OPEN'); | ||||||||||||||||||||||||||||||||||||||||||||
| expect(STATE_HALF_OPEN).toBe('HALF_OPEN'); | ||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| test('default thresholds are defined', () => { | ||||||||||||||||||||||||||||||||||||||||||||
| expect(DEFAULT_FAILURE_THRESHOLD).toBe(5); | ||||||||||||||||||||||||||||||||||||||||||||
| expect(DEFAULT_SUCCESS_THRESHOLD).toBe(3); | ||||||||||||||||||||||||||||||||||||||||||||
| expect(DEFAULT_RESET_TIMEOUT_MS).toBe(60000); | ||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| // ============================================================ | ||||||||||||||||||||||||||||||||||||||||||||
| // Constructor | ||||||||||||||||||||||||||||||||||||||||||||
| // ============================================================ | ||||||||||||||||||||||||||||||||||||||||||||
| describe('constructor', () => { | ||||||||||||||||||||||||||||||||||||||||||||
| test('starts in CLOSED state', () => { | ||||||||||||||||||||||||||||||||||||||||||||
| expect(breaker.getState()).toBe(STATE_CLOSED); | ||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| test('uses default thresholds', () => { | ||||||||||||||||||||||||||||||||||||||||||||
| const stats = breaker.getStats(); | ||||||||||||||||||||||||||||||||||||||||||||
| expect(stats.failureCount).toBe(0); | ||||||||||||||||||||||||||||||||||||||||||||
| expect(stats.successCount).toBe(0); | ||||||||||||||||||||||||||||||||||||||||||||
| expect(stats.totalTrips).toBe(0); | ||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| test('accepts custom options', () => { | ||||||||||||||||||||||||||||||||||||||||||||
| const custom = new CircuitBreaker({ | ||||||||||||||||||||||||||||||||||||||||||||
| failureThreshold: 3, | ||||||||||||||||||||||||||||||||||||||||||||
| successThreshold: 2, | ||||||||||||||||||||||||||||||||||||||||||||
| resetTimeoutMs: 30000, | ||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||
| // Trip it to verify custom threshold | ||||||||||||||||||||||||||||||||||||||||||||
| for (let i = 0; i < 3; i++) custom.recordFailure(); | ||||||||||||||||||||||||||||||||||||||||||||
| expect(custom.getState()).toBe(STATE_OPEN); | ||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| // ============================================================ | ||||||||||||||||||||||||||||||||||||||||||||
| // isAllowed | ||||||||||||||||||||||||||||||||||||||||||||
| // ============================================================ | ||||||||||||||||||||||||||||||||||||||||||||
| describe('isAllowed', () => { | ||||||||||||||||||||||||||||||||||||||||||||
| test('allows requests when CLOSED', () => { | ||||||||||||||||||||||||||||||||||||||||||||
| expect(breaker.isAllowed()).toBe(true); | ||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| test('blocks requests when OPEN', () => { | ||||||||||||||||||||||||||||||||||||||||||||
| // Trip the breaker | ||||||||||||||||||||||||||||||||||||||||||||
| for (let i = 0; i < 5; i++) breaker.recordFailure(); | ||||||||||||||||||||||||||||||||||||||||||||
| expect(breaker.getState()).toBe(STATE_OPEN); | ||||||||||||||||||||||||||||||||||||||||||||
| expect(breaker.isAllowed()).toBe(false); | ||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| 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; | ||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
| breaker._lastFailureTime = Date.now() - 61000; | |
| breaker._lastFailureTime = Date.now() - (DEFAULT_RESET_TIMEOUT_MS + 1000); |
Copilot
AI
Feb 18, 2026
There was a problem hiding this comment.
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.
| 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
AI
Feb 18, 2026
There was a problem hiding this comment.
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.
| 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Copilot
AI
Feb 18, 2026
There was a problem hiding this comment.
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.
| // 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); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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.
There was a problem hiding this comment.
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.