Files
telegram-ai-support/tests/utils/circuit-breaker.test.ts
kappa 1d6b64c9e4 Initial implementation of Telegram AI customer support bot
Cloudflare Workers + Hono + D1 + KV + R2 stack with 4 specialized AI agents
(onboarding, troubleshoot, asset, billing), OpenAI function calling with
7 tool definitions, human escalation, pending action approval workflow,
feedback collection, audit logging, i18n (ko/en), and Workers AI fallback.

43 source files, 45 tests passing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 13:21:38 +09:00

118 lines
3.5 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import { CircuitBreaker, CircuitBreakerError, CircuitState } from '../../src/utils/circuit-breaker';
function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
describe('CircuitBreaker', () => {
it('starts in CLOSED state', () => {
const cb = new CircuitBreaker({ serviceName: 'test' });
expect(cb.getState()).toBe(CircuitState.CLOSED);
});
it('passes through successful executions', async () => {
const cb = new CircuitBreaker({ serviceName: 'test' });
const result = await cb.execute(async () => 'ok');
expect(result).toBe('ok');
expect(cb.getState()).toBe(CircuitState.CLOSED);
});
it('opens after failure threshold is exceeded', async () => {
const cb = new CircuitBreaker({
serviceName: 'test',
failureThreshold: 3,
resetTimeoutMs: 100,
});
for (let i = 0; i < 3; i++) {
await expect(cb.execute(async () => { throw new Error('fail'); })).rejects.toThrow('fail');
}
expect(cb.getState()).toBe(CircuitState.OPEN);
});
it('rejects requests while OPEN', async () => {
const cb = new CircuitBreaker({
serviceName: 'test',
failureThreshold: 2,
resetTimeoutMs: 5000,
});
// Trip the breaker
for (let i = 0; i < 2; i++) {
await expect(cb.execute(async () => { throw new Error('fail'); })).rejects.toThrow();
}
expect(cb.getState()).toBe(CircuitState.OPEN);
// Should throw CircuitBreakerError
await expect(cb.execute(async () => 'ok')).rejects.toThrow(CircuitBreakerError);
});
it('transitions to HALF_OPEN after reset timeout', async () => {
const cb = new CircuitBreaker({
serviceName: 'test',
failureThreshold: 2,
resetTimeoutMs: 100,
});
for (let i = 0; i < 2; i++) {
await expect(cb.execute(async () => { throw new Error('fail'); })).rejects.toThrow();
}
expect(cb.getState()).toBe(CircuitState.OPEN);
await sleep(150);
// Next execute call checks the timeout and transitions to HALF_OPEN
const result = await cb.execute(async () => 'recovered');
expect(result).toBe('recovered');
// Successful test in HALF_OPEN closes the circuit
expect(cb.getState()).toBe(CircuitState.CLOSED);
});
it('closes after successful test in HALF_OPEN', async () => {
const cb = new CircuitBreaker({
serviceName: 'test',
failureThreshold: 2,
resetTimeoutMs: 100,
});
// Open the circuit
for (let i = 0; i < 2; i++) {
await expect(cb.execute(async () => { throw new Error('fail'); })).rejects.toThrow();
}
expect(cb.getState()).toBe(CircuitState.OPEN);
// Wait for reset timeout
await sleep(150);
// Successful execution transitions HALF_OPEN -> CLOSED
await cb.execute(async () => 'success');
expect(cb.getState()).toBe(CircuitState.CLOSED);
// Verify circuit is fully operational again
const result = await cb.execute(async () => 'working');
expect(result).toBe('working');
});
it('re-opens if HALF_OPEN test fails', async () => {
const cb = new CircuitBreaker({
serviceName: 'test',
failureThreshold: 2,
resetTimeoutMs: 100,
});
// Open the circuit
for (let i = 0; i < 2; i++) {
await expect(cb.execute(async () => { throw new Error('fail'); })).rejects.toThrow();
}
await sleep(150);
// Fail during HALF_OPEN
await expect(cb.execute(async () => { throw new Error('still broken'); })).rejects.toThrow();
expect(cb.getState()).toBe(CircuitState.OPEN);
});
});