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>
118 lines
3.5 KiB
TypeScript
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);
|
|
});
|
|
});
|