Path: blob/main/extensions/copilot/src/extension/chronicle/common/test/circuitBreaker.spec.ts
13405 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';6import { CircuitBreaker, CircuitState } from '../circuitBreaker';78describe('CircuitBreaker', () => {9beforeEach(() => {10vi.useFakeTimers();11});1213afterEach(() => {14vi.useRealTimers();15});1617it('starts in CLOSED state', () => {18const cb = new CircuitBreaker();19expect(cb.getState()).toBe(CircuitState.CLOSED);20expect(cb.canRequest()).toBe(true);21});2223it('stays CLOSED below failure threshold', () => {24const cb = new CircuitBreaker({ failureThreshold: 5 });25for (let i = 0; i < 4; i++) {26cb.recordFailure();27}28expect(cb.getState()).toBe(CircuitState.CLOSED);29expect(cb.canRequest()).toBe(true);30});3132it('opens after reaching failure threshold', () => {33const cb = new CircuitBreaker({ failureThreshold: 3 });34cb.recordFailure();35cb.recordFailure();36cb.recordFailure();37expect(cb.getState()).toBe(CircuitState.OPEN);38expect(cb.canRequest()).toBe(false);39});4041it('transitions from OPEN to HALF_OPEN after reset timeout', () => {42const cb = new CircuitBreaker({ failureThreshold: 1, resetTimeoutMs: 10 });43cb.recordFailure();44expect(cb.getState()).toBe(CircuitState.OPEN);4546vi.advanceTimersByTime(10);47expect(cb.getState()).toBe(CircuitState.HALF_OPEN);48});4950it('allows one probe in HALF_OPEN state', () => {51const cb = new CircuitBreaker({ failureThreshold: 1, resetTimeoutMs: 0 });52cb.recordFailure();5354// Should be HALF_OPEN immediately with resetTimeoutMs=055expect(cb.getState()).toBe(CircuitState.HALF_OPEN);56expect(cb.canRequest()).toBe(true); // first probe57expect(cb.canRequest()).toBe(false); // second probe blocked58});5960it('closes on success after HALF_OPEN probe', () => {61const cb = new CircuitBreaker({ failureThreshold: 1, resetTimeoutMs: 0 });62cb.recordFailure();6364expect(cb.canRequest()).toBe(true); // probe65cb.recordSuccess();66expect(cb.getState()).toBe(CircuitState.CLOSED);67expect(cb.getFailureCount()).toBe(0);68});6970it('re-opens on failure during HALF_OPEN probe', () => {71const cb = new CircuitBreaker({ failureThreshold: 1, resetTimeoutMs: 10 });72cb.recordFailure();73expect(cb.getState()).toBe(CircuitState.OPEN);7475vi.advanceTimersByTime(10);76expect(cb.getState()).toBe(CircuitState.HALF_OPEN);77cb.canRequest(); // take probe78cb.recordFailure(); // probe fails → back to OPEN79expect(cb.getState()).toBe(CircuitState.OPEN);80});8182it('resets to CLOSED state', () => {83const cb = new CircuitBreaker({ failureThreshold: 1 });84cb.recordFailure();85expect(cb.getState()).toBe(CircuitState.OPEN);8687cb.reset();88expect(cb.getState()).toBe(CircuitState.CLOSED);89expect(cb.getFailureCount()).toBe(0);90expect(cb.canRequest()).toBe(true);91});9293it('resets failure count on success', () => {94const cb = new CircuitBreaker({ failureThreshold: 5 });95cb.recordFailure();96cb.recordFailure();97expect(cb.getFailureCount()).toBe(2);9899cb.recordSuccess();100expect(cb.getFailureCount()).toBe(0);101});102103it('applies exponential backoff on repeated HALF_OPEN failures', () => {104const cb = new CircuitBreaker({105failureThreshold: 1,106resetTimeoutMs: 10,107maxResetTimeoutMs: 100,108probeTimeoutMs: 5,109});110111// First failure → OPEN112cb.recordFailure();113expect(cb.getState()).toBe(CircuitState.OPEN);114115// Advance past first reset timeout (10ms)116vi.advanceTimersByTime(10);117expect(cb.getState()).toBe(CircuitState.HALF_OPEN);118cb.canRequest(); // take the probe119cb.recordFailure(); // probe fails → back to OPEN120121expect(cb.getState()).toBe(CircuitState.OPEN);122// Now timeout should be 20ms (doubled)123124// Advance 15ms — should still be OPEN (timeout is now 20ms)125vi.advanceTimersByTime(15);126expect(cb.getState()).toBe(CircuitState.OPEN);127128// Advance another 5ms — now past 20ms, should be HALF_OPEN129vi.advanceTimersByTime(5);130expect(cb.getState()).toBe(CircuitState.HALF_OPEN);131});132133it('probe timeout prevents permanent deadlock', () => {134const cb = new CircuitBreaker({135failureThreshold: 1,136resetTimeoutMs: 0,137probeTimeoutMs: 10,138});139cb.recordFailure();140141// Take probe142expect(cb.canRequest()).toBe(true);143// Probe is in-flight, second request blocked144expect(cb.canRequest()).toBe(false);145146// Advance past probe timeout147vi.advanceTimersByTime(10);148// Probe timed out, should allow another149expect(cb.canRequest()).toBe(true);150});151});152153154