mirror of
https://github.com/proffesor-for-testing/agentic-qe.git
synced 2026-09-19 08:45:47 +08:00
2253d05618
Remove the nested v3/ subdirectory by moving all active source code, tests, configs, and assets directly to the project root. Archive v2/ (dead code, 847MB) to v2-archive.tar.gz. Key changes: - Move v3/src/ → src/, v3/tests/ → tests/, v3/packages/ → packages/ - Move v3/tsconfig.json, v3/vitest.config.ts to root - Merge v3/package.json into root package.json (ESM, scripts, deps, exports) - Merge v3/scripts/ into scripts/, v3/implementation/ into docs/implementation/ - Update all CI workflows to remove "cd v3 &&" and v3/ path prefixes - Fix all init installer path resolution (3-level-up → 2-level-up) - Fix hooks to use "node ./dist/cli/bundle.js" instead of bare "aqe" - Update settings.json ADR/DDD directories to docs/implementation/ - Fix helper scripts (statusline, adr-compliance, ddd-tracker, guidance-hooks) - Update 58 agent definitions: aqe/v3/ → aqe/ memory namespaces - Update 15 guidance shards: v3/src/ → src/ path references - Fix sync interfaces: remove v3/ and v2/ path references - Fix test path calculations (process.cwd() + '..' no longer needed) - Fix flaky perf test threshold (500ms → 1000ms for CI environments) - Fix complexity-analyzer test to match ADR-051 keyword-based eligibility Build passes, 17,901 tests pass, CLI and MCP bundles verified. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
720 lines
22 KiB
TypeScript
720 lines
22 KiB
TypeScript
/**
|
|
* Agentic QE v3 - HTTP Client Unit Tests
|
|
*/
|
|
|
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
import {
|
|
HttpClient,
|
|
createHttpClient,
|
|
getHttpClient,
|
|
} from '../../../../src/shared/http';
|
|
|
|
// Mock fetch globally
|
|
const mockFetch = vi.fn();
|
|
global.fetch = mockFetch;
|
|
|
|
describe('HttpClient', () => {
|
|
let client: HttpClient;
|
|
|
|
beforeEach(() => {
|
|
client = createHttpClient();
|
|
mockFetch.mockReset();
|
|
vi.useFakeTimers({ shouldAdvanceTime: true });
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
// ============================================================================
|
|
// Basic Request Tests
|
|
// ============================================================================
|
|
|
|
describe('get', () => {
|
|
it('should make a GET request successfully', async () => {
|
|
const mockResponse = new Response(JSON.stringify({ data: 'test' }), {
|
|
status: 200,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
});
|
|
mockFetch.mockResolvedValueOnce(mockResponse);
|
|
|
|
const result = await client.get('https://api.example.com/data');
|
|
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.value.status).toBe(200);
|
|
}
|
|
expect(mockFetch).toHaveBeenCalledWith(
|
|
'https://api.example.com/data',
|
|
expect.objectContaining({ method: 'GET' })
|
|
);
|
|
});
|
|
|
|
it('should include custom headers', async () => {
|
|
mockFetch.mockResolvedValueOnce(new Response('ok'));
|
|
|
|
await client.get('https://api.example.com/data', {
|
|
headers: { Authorization: 'Bearer token123' },
|
|
});
|
|
|
|
expect(mockFetch).toHaveBeenCalledWith(
|
|
'https://api.example.com/data',
|
|
expect.objectContaining({
|
|
headers: expect.objectContaining({
|
|
Authorization: 'Bearer token123',
|
|
}),
|
|
})
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('post', () => {
|
|
it('should make a POST request with JSON body', async () => {
|
|
mockFetch.mockResolvedValueOnce(new Response('{"id": 1}', { status: 201 }));
|
|
|
|
const body = { name: 'test', value: 42 };
|
|
const result = await client.post('https://api.example.com/items', body);
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(mockFetch).toHaveBeenCalledWith(
|
|
'https://api.example.com/items',
|
|
expect.objectContaining({
|
|
method: 'POST',
|
|
body: JSON.stringify(body),
|
|
headers: expect.objectContaining({
|
|
'Content-Type': 'application/json',
|
|
}),
|
|
})
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('put', () => {
|
|
it('should make a PUT request with JSON body', async () => {
|
|
mockFetch.mockResolvedValueOnce(new Response('{"updated": true}'));
|
|
|
|
const body = { name: 'updated' };
|
|
const result = await client.put('https://api.example.com/items/1', body);
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(mockFetch).toHaveBeenCalledWith(
|
|
'https://api.example.com/items/1',
|
|
expect.objectContaining({
|
|
method: 'PUT',
|
|
body: JSON.stringify(body),
|
|
})
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('delete', () => {
|
|
it('should make a DELETE request', async () => {
|
|
mockFetch.mockResolvedValueOnce(new Response(null, { status: 204 }));
|
|
|
|
const result = await client.delete('https://api.example.com/items/1');
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(mockFetch).toHaveBeenCalledWith(
|
|
'https://api.example.com/items/1',
|
|
expect.objectContaining({ method: 'DELETE' })
|
|
);
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Timeout Tests
|
|
// ============================================================================
|
|
|
|
describe('timeout', () => {
|
|
it('should timeout after specified duration', async () => {
|
|
// Simulate AbortError when signal is aborted
|
|
const abortError = new Error('The operation was aborted');
|
|
abortError.name = 'AbortError';
|
|
|
|
mockFetch.mockImplementation((_url: string, options: RequestInit) => {
|
|
return new Promise((resolve, reject) => {
|
|
// Check if signal is already aborted
|
|
if (options.signal?.aborted) {
|
|
reject(abortError);
|
|
return;
|
|
}
|
|
// Listen for abort event
|
|
options.signal?.addEventListener('abort', () => {
|
|
reject(abortError);
|
|
});
|
|
});
|
|
});
|
|
|
|
const resultPromise = client.get('https://api.example.com/slow', {
|
|
timeout: 1000,
|
|
retries: 0,
|
|
circuitBreaker: false,
|
|
});
|
|
|
|
// Advance timers to trigger the AbortController timeout
|
|
await vi.advanceTimersByTimeAsync(1500);
|
|
|
|
const result = await resultPromise;
|
|
|
|
expect(result.success).toBe(false);
|
|
if (!result.success) {
|
|
expect(result.error.code).toBe('TIMEOUT');
|
|
}
|
|
});
|
|
|
|
it('should use default timeout of 30000ms', async () => {
|
|
mockFetch.mockResolvedValueOnce(new Response('ok'));
|
|
|
|
await client.get('https://api.example.com/data');
|
|
|
|
// Verify AbortController signal was passed
|
|
expect(mockFetch).toHaveBeenCalledWith(
|
|
expect.any(String),
|
|
expect.objectContaining({
|
|
signal: expect.any(AbortSignal),
|
|
})
|
|
);
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Retry Tests
|
|
// ============================================================================
|
|
|
|
describe('retry', () => {
|
|
it('should retry failed requests with exponential backoff', async () => {
|
|
mockFetch
|
|
.mockRejectedValueOnce(new Error('Network error'))
|
|
.mockRejectedValueOnce(new Error('Network error'))
|
|
.mockResolvedValueOnce(new Response('ok'));
|
|
|
|
const resultPromise = client.get('https://api.example.com/flaky', {
|
|
retries: 3,
|
|
retryDelay: 100,
|
|
circuitBreaker: false,
|
|
});
|
|
|
|
// First retry after 100ms (100 * 2^0)
|
|
await vi.advanceTimersByTimeAsync(100);
|
|
// Second retry after 200ms (100 * 2^1)
|
|
await vi.advanceTimersByTimeAsync(200);
|
|
|
|
const result = await resultPromise;
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(mockFetch).toHaveBeenCalledTimes(3);
|
|
});
|
|
|
|
it('should return error after all retries exhausted', async () => {
|
|
mockFetch.mockRejectedValue(new Error('Persistent error'));
|
|
|
|
const resultPromise = client.get('https://api.example.com/broken', {
|
|
retries: 2,
|
|
retryDelay: 50,
|
|
circuitBreaker: false,
|
|
});
|
|
|
|
// Advance through all retries
|
|
await vi.advanceTimersByTimeAsync(50); // First retry
|
|
await vi.advanceTimersByTimeAsync(100); // Second retry
|
|
|
|
const result = await resultPromise;
|
|
|
|
expect(result.success).toBe(false);
|
|
if (!result.success) {
|
|
expect(result.error.message).toBe('Persistent error');
|
|
expect(result.error.timing?.retryCount).toBe(2);
|
|
}
|
|
expect(mockFetch).toHaveBeenCalledTimes(3); // Initial + 2 retries
|
|
});
|
|
|
|
it('should not retry when retries is 0', async () => {
|
|
mockFetch.mockRejectedValue(new Error('Error'));
|
|
|
|
const result = await client.get('https://api.example.com/data', {
|
|
retries: 0,
|
|
circuitBreaker: false,
|
|
});
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('should apply exponential backoff with correct delays', async () => {
|
|
mockFetch
|
|
.mockRejectedValueOnce(new Error('Error 1'))
|
|
.mockRejectedValueOnce(new Error('Error 2'))
|
|
.mockRejectedValueOnce(new Error('Error 3'))
|
|
.mockResolvedValueOnce(new Response('ok'));
|
|
|
|
const resultPromise = client.get('https://api.example.com/data', {
|
|
retries: 3,
|
|
retryDelay: 1000,
|
|
circuitBreaker: false,
|
|
});
|
|
|
|
// First retry: 1000 * 2^0 = 1000ms
|
|
await vi.advanceTimersByTimeAsync(1000);
|
|
expect(mockFetch).toHaveBeenCalledTimes(2);
|
|
|
|
// Second retry: 1000 * 2^1 = 2000ms
|
|
await vi.advanceTimersByTimeAsync(2000);
|
|
expect(mockFetch).toHaveBeenCalledTimes(3);
|
|
|
|
// Third retry: 1000 * 2^2 = 4000ms
|
|
await vi.advanceTimersByTimeAsync(4000);
|
|
expect(mockFetch).toHaveBeenCalledTimes(4);
|
|
|
|
const result = await resultPromise;
|
|
expect(result.success).toBe(true);
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Circuit Breaker Tests
|
|
// ============================================================================
|
|
|
|
describe('circuit breaker', () => {
|
|
it('should open circuit after threshold failures', async () => {
|
|
mockFetch.mockRejectedValue(new Error('Service unavailable'));
|
|
|
|
// Make 5 failing requests to open the circuit
|
|
for (let i = 0; i < 5; i++) {
|
|
await client.get('https://api.example.com/unstable', {
|
|
retries: 0,
|
|
circuitBreaker: true,
|
|
});
|
|
}
|
|
|
|
const state = client.getCircuitState('https://api.example.com/unstable');
|
|
expect(state.state).toBe('open');
|
|
expect(state.failures).toBe(5);
|
|
});
|
|
|
|
it('should reject requests when circuit is open', async () => {
|
|
mockFetch.mockRejectedValue(new Error('Service unavailable'));
|
|
|
|
// Open the circuit
|
|
for (let i = 0; i < 5; i++) {
|
|
await client.get('https://api.example.com/unstable', {
|
|
retries: 0,
|
|
circuitBreaker: true,
|
|
});
|
|
}
|
|
|
|
// Next request should fail immediately
|
|
mockFetch.mockClear();
|
|
const result = await client.get('https://api.example.com/unstable', {
|
|
retries: 0,
|
|
circuitBreaker: true,
|
|
});
|
|
|
|
expect(result.success).toBe(false);
|
|
if (!result.success) {
|
|
expect(result.error.code).toBe('CIRCUIT_OPEN');
|
|
}
|
|
expect(mockFetch).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should transition to half-open after reset timeout', async () => {
|
|
mockFetch.mockRejectedValue(new Error('Service unavailable'));
|
|
|
|
// Open the circuit
|
|
for (let i = 0; i < 5; i++) {
|
|
await client.get('https://api.example.com/unstable', {
|
|
retries: 0,
|
|
circuitBreaker: true,
|
|
});
|
|
}
|
|
|
|
// Advance time past the reset timeout (30 seconds)
|
|
await vi.advanceTimersByTimeAsync(31000);
|
|
|
|
// Next request should be allowed (half-open)
|
|
mockFetch.mockResolvedValueOnce(new Response('ok'));
|
|
const result = await client.get('https://api.example.com/unstable', {
|
|
retries: 0,
|
|
circuitBreaker: true,
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(mockFetch).toHaveBeenCalled();
|
|
});
|
|
|
|
it('should close circuit after successful request in half-open state', async () => {
|
|
mockFetch.mockRejectedValue(new Error('Service unavailable'));
|
|
|
|
// Open the circuit
|
|
for (let i = 0; i < 5; i++) {
|
|
await client.get('https://api.example.com/unstable', {
|
|
retries: 0,
|
|
circuitBreaker: true,
|
|
});
|
|
}
|
|
|
|
// Advance time to half-open
|
|
await vi.advanceTimersByTimeAsync(31000);
|
|
|
|
// Successful request should close the circuit
|
|
mockFetch.mockResolvedValueOnce(new Response('ok'));
|
|
await client.get('https://api.example.com/unstable', {
|
|
retries: 0,
|
|
circuitBreaker: true,
|
|
});
|
|
|
|
const state = client.getCircuitState('https://api.example.com/unstable');
|
|
expect(state.state).toBe('closed');
|
|
expect(state.failures).toBe(0);
|
|
});
|
|
|
|
it('should allow bypassing circuit breaker with option', async () => {
|
|
mockFetch.mockRejectedValue(new Error('Service unavailable'));
|
|
|
|
// Open the circuit
|
|
for (let i = 0; i < 5; i++) {
|
|
await client.get('https://api.example.com/unstable', {
|
|
retries: 0,
|
|
circuitBreaker: true,
|
|
});
|
|
}
|
|
|
|
// Request with circuit breaker disabled should go through
|
|
mockFetch.mockClear();
|
|
mockFetch.mockResolvedValueOnce(new Response('ok'));
|
|
|
|
const result = await client.get('https://api.example.com/unstable', {
|
|
retries: 0,
|
|
circuitBreaker: false,
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(mockFetch).toHaveBeenCalled();
|
|
});
|
|
|
|
it('should reset circuit breaker manually', async () => {
|
|
mockFetch.mockRejectedValue(new Error('Service unavailable'));
|
|
|
|
// Open the circuit
|
|
for (let i = 0; i < 5; i++) {
|
|
await client.get('https://api.example.com/unstable', {
|
|
retries: 0,
|
|
circuitBreaker: true,
|
|
});
|
|
}
|
|
|
|
client.resetCircuit('https://api.example.com/unstable');
|
|
|
|
const state = client.getCircuitState('https://api.example.com/unstable');
|
|
expect(state.state).toBe('closed');
|
|
expect(state.failures).toBe(0);
|
|
});
|
|
|
|
it('should reset all circuits when called without URL', async () => {
|
|
mockFetch.mockRejectedValue(new Error('Service unavailable'));
|
|
|
|
// Open circuits for multiple domains
|
|
for (let i = 0; i < 5; i++) {
|
|
await client.get('https://api1.example.com/data', {
|
|
retries: 0,
|
|
circuitBreaker: true,
|
|
});
|
|
await client.get('https://api2.example.com/data', {
|
|
retries: 0,
|
|
circuitBreaker: true,
|
|
});
|
|
}
|
|
|
|
client.resetCircuit();
|
|
|
|
const state1 = client.getCircuitState('https://api1.example.com');
|
|
const state2 = client.getCircuitState('https://api2.example.com');
|
|
|
|
expect(state1.state).toBe('closed');
|
|
expect(state2.state).toBe('closed');
|
|
});
|
|
|
|
it('should track circuits per origin', async () => {
|
|
mockFetch.mockRejectedValue(new Error('Service unavailable'));
|
|
|
|
// Fail requests to one domain
|
|
for (let i = 0; i < 5; i++) {
|
|
await client.get('https://api1.example.com/data', {
|
|
retries: 0,
|
|
circuitBreaker: true,
|
|
});
|
|
}
|
|
|
|
// Other domain should still work
|
|
mockFetch.mockResolvedValueOnce(new Response('ok'));
|
|
const result = await client.get('https://api2.example.com/data', {
|
|
retries: 0,
|
|
circuitBreaker: true,
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Health Check Tests
|
|
// ============================================================================
|
|
|
|
describe('healthCheck', () => {
|
|
it('should return true for healthy endpoint', async () => {
|
|
mockFetch.mockResolvedValueOnce(new Response('OK', { status: 200 }));
|
|
|
|
const isHealthy = await client.healthCheck('https://api.example.com/health');
|
|
|
|
expect(isHealthy).toBe(true);
|
|
});
|
|
|
|
it('should return false for unhealthy endpoint', async () => {
|
|
mockFetch.mockResolvedValueOnce(
|
|
new Response('Service Unavailable', { status: 503 })
|
|
);
|
|
|
|
const isHealthy = await client.healthCheck('https://api.example.com/health');
|
|
|
|
expect(isHealthy).toBe(false);
|
|
});
|
|
|
|
it('should return false when request fails', async () => {
|
|
// Health check has retries: 1, so we need to fail twice
|
|
mockFetch
|
|
.mockRejectedValueOnce(new Error('Connection refused'))
|
|
.mockRejectedValueOnce(new Error('Connection refused'));
|
|
|
|
const resultPromise = client.healthCheck('https://api.example.com/health');
|
|
|
|
// Advance timer for the retry delay
|
|
await vi.advanceTimersByTimeAsync(1000);
|
|
|
|
const isHealthy = await resultPromise;
|
|
|
|
expect(isHealthy).toBe(false);
|
|
});
|
|
|
|
it('should use short timeout for health checks', async () => {
|
|
// Simulate AbortError when signal is aborted
|
|
const abortError = new Error('The operation was aborted');
|
|
abortError.name = 'AbortError';
|
|
|
|
mockFetch.mockImplementation((_url: string, options: RequestInit) => {
|
|
return new Promise((resolve, reject) => {
|
|
if (options.signal?.aborted) {
|
|
reject(abortError);
|
|
return;
|
|
}
|
|
options.signal?.addEventListener('abort', () => {
|
|
reject(abortError);
|
|
});
|
|
});
|
|
});
|
|
|
|
const resultPromise = client.healthCheck('https://api.example.com/health');
|
|
|
|
// Health check uses 5000ms timeout, advance past it
|
|
await vi.advanceTimersByTimeAsync(6000);
|
|
// Also advance for the retry
|
|
await vi.advanceTimersByTimeAsync(2000);
|
|
|
|
const isHealthy = await resultPromise;
|
|
|
|
expect(isHealthy).toBe(false);
|
|
});
|
|
|
|
it('should disable circuit breaker for health checks', async () => {
|
|
mockFetch.mockRejectedValue(new Error('Service unavailable'));
|
|
|
|
// Open the circuit through regular requests
|
|
for (let i = 0; i < 5; i++) {
|
|
await client.get('https://api.example.com/data', {
|
|
retries: 0,
|
|
circuitBreaker: true,
|
|
});
|
|
}
|
|
|
|
// Health check should still go through (circuit breaker disabled)
|
|
mockFetch.mockClear();
|
|
mockFetch.mockResolvedValueOnce(new Response('OK'));
|
|
|
|
const isHealthy = await client.healthCheck('https://api.example.com/health');
|
|
|
|
expect(isHealthy).toBe(true);
|
|
expect(mockFetch).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Request Timing Tests
|
|
// ============================================================================
|
|
|
|
describe('request timing', () => {
|
|
it('should include timing metrics in successful response', async () => {
|
|
mockFetch.mockResolvedValueOnce(new Response('ok'));
|
|
|
|
const result = await client.get('https://api.example.com/data', {
|
|
circuitBreaker: false,
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
// Note: Timing is only exposed in error responses for Result pattern
|
|
});
|
|
|
|
it('should include timing metrics in error response', async () => {
|
|
mockFetch.mockRejectedValue(new Error('Network error'));
|
|
|
|
const result = await client.get('https://api.example.com/data', {
|
|
retries: 0,
|
|
circuitBreaker: false,
|
|
});
|
|
|
|
expect(result.success).toBe(false);
|
|
if (!result.success) {
|
|
expect(result.error.timing).toBeDefined();
|
|
expect(result.error.timing?.duration).toBeGreaterThanOrEqual(0);
|
|
expect(result.error.timing?.startTime).toBeLessThanOrEqual(
|
|
result.error.timing?.endTime ?? 0
|
|
);
|
|
}
|
|
});
|
|
|
|
it('should track retry count in timing', async () => {
|
|
mockFetch
|
|
.mockRejectedValueOnce(new Error('Error 1'))
|
|
.mockRejectedValueOnce(new Error('Error 2'))
|
|
.mockRejectedValueOnce(new Error('Error 3'));
|
|
|
|
const resultPromise = client.get('https://api.example.com/data', {
|
|
retries: 2,
|
|
retryDelay: 100,
|
|
circuitBreaker: false,
|
|
});
|
|
|
|
await vi.advanceTimersByTimeAsync(100);
|
|
await vi.advanceTimersByTimeAsync(200);
|
|
|
|
const result = await resultPromise;
|
|
|
|
expect(result.success).toBe(false);
|
|
if (!result.success) {
|
|
expect(result.error.timing?.retryCount).toBe(2);
|
|
}
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Error Code Tests
|
|
// ============================================================================
|
|
|
|
describe('error codes', () => {
|
|
it('should return TIMEOUT for AbortError', async () => {
|
|
const abortError = new Error('Aborted');
|
|
abortError.name = 'AbortError';
|
|
mockFetch.mockRejectedValueOnce(abortError);
|
|
|
|
const result = await client.get('https://api.example.com/data', {
|
|
retries: 0,
|
|
circuitBreaker: false,
|
|
});
|
|
|
|
expect(result.success).toBe(false);
|
|
if (!result.success) {
|
|
expect(result.error.code).toBe('TIMEOUT');
|
|
}
|
|
});
|
|
|
|
it('should return CONNECTION_REFUSED for ECONNREFUSED', async () => {
|
|
mockFetch.mockRejectedValueOnce(new Error('connect ECONNREFUSED'));
|
|
|
|
const result = await client.get('https://api.example.com/data', {
|
|
retries: 0,
|
|
circuitBreaker: false,
|
|
});
|
|
|
|
expect(result.success).toBe(false);
|
|
if (!result.success) {
|
|
expect(result.error.code).toBe('CONNECTION_REFUSED');
|
|
}
|
|
});
|
|
|
|
it('should return DNS_ERROR for ENOTFOUND', async () => {
|
|
mockFetch.mockRejectedValueOnce(new Error('getaddrinfo ENOTFOUND'));
|
|
|
|
const result = await client.get('https://api.example.com/data', {
|
|
retries: 0,
|
|
circuitBreaker: false,
|
|
});
|
|
|
|
expect(result.success).toBe(false);
|
|
if (!result.success) {
|
|
expect(result.error.code).toBe('DNS_ERROR');
|
|
}
|
|
});
|
|
|
|
it('should return NETWORK_TIMEOUT for ETIMEDOUT', async () => {
|
|
mockFetch.mockRejectedValueOnce(new Error('connect ETIMEDOUT'));
|
|
|
|
const result = await client.get('https://api.example.com/data', {
|
|
retries: 0,
|
|
circuitBreaker: false,
|
|
});
|
|
|
|
expect(result.success).toBe(false);
|
|
if (!result.success) {
|
|
expect(result.error.code).toBe('NETWORK_TIMEOUT');
|
|
}
|
|
});
|
|
|
|
it('should return REQUEST_FAILED for unknown errors', async () => {
|
|
mockFetch.mockRejectedValueOnce(new Error('Unknown error'));
|
|
|
|
const result = await client.get('https://api.example.com/data', {
|
|
retries: 0,
|
|
circuitBreaker: false,
|
|
});
|
|
|
|
expect(result.success).toBe(false);
|
|
if (!result.success) {
|
|
expect(result.error.code).toBe('REQUEST_FAILED');
|
|
}
|
|
});
|
|
|
|
it('should include original error as cause', async () => {
|
|
const originalError = new Error('Original error message');
|
|
mockFetch.mockRejectedValueOnce(originalError);
|
|
|
|
const result = await client.get('https://api.example.com/data', {
|
|
retries: 0,
|
|
circuitBreaker: false,
|
|
});
|
|
|
|
expect(result.success).toBe(false);
|
|
if (!result.success) {
|
|
expect(result.error.cause).toBe(originalError);
|
|
expect(result.error.message).toBe('Original error message');
|
|
}
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Singleton Tests
|
|
// ============================================================================
|
|
|
|
describe('singleton', () => {
|
|
it('should return the same instance from getHttpClient', () => {
|
|
const client1 = getHttpClient();
|
|
const client2 = getHttpClient();
|
|
|
|
expect(client1).toBe(client2);
|
|
});
|
|
|
|
it('should return new instance from createHttpClient', () => {
|
|
const client1 = createHttpClient();
|
|
const client2 = createHttpClient();
|
|
|
|
expect(client1).not.toBe(client2);
|
|
});
|
|
});
|
|
});
|