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>
This commit is contained in:
117
tests/utils/circuit-breaker.test.ts
Normal file
117
tests/utils/circuit-breaker.test.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user