Files
wehub-resource-sync 0d3cb498a3
Deploy local.promptfoo.app / Deploy to Cloudflare Pages (push) Waiting to run
Test and Publish Multi-arch Docker Image / test (push) Waiting to run
Test and Publish Multi-arch Docker Image / build-docker-and-push-digests (map[digest-suffix:linux-amd64 platform:linux/amd64 runner:ubuntu-latest]) (push) Blocked by required conditions
Test and Publish Multi-arch Docker Image / build-docker-and-push-digests (map[digest-suffix:linux-arm64 platform:linux/arm64 runner:ubuntu-24.04-arm]) (push) Blocked by required conditions
Test and Publish Multi-arch Docker Image / merge-docker-digests (push) Blocked by required conditions
Test and Publish Multi-arch Docker Image / Attest Multi-arch Image (push) Blocked by required conditions
Validate Renovate Config / Validate Renovate Configuration (push) Waiting to run
CI / Shell Format Check (push) Has been cancelled
CI / Check Ruby (3.4) (push) Has been cancelled
CI / CI Config (push) Has been cancelled
CI / Test on Node ${{ matrix.node }} and ${{ matrix.os }}${{ matrix.shard && format(' (shard {0}/3)', matrix.shard) || '' }} (push) Has been cancelled
CI / Build on Node ${{ matrix.node }} (push) Has been cancelled
CI / Style Check (push) Has been cancelled
CI / Generate Assets (push) Has been cancelled
CI / Check Python (3.14) (push) Has been cancelled
CI / Check Python (3.9) (push) Has been cancelled
CI / Build Docs (push) Has been cancelled
CI / Code Scan Action (push) Has been cancelled
CI / Site tests (push) Has been cancelled
CI / webui tests (push) Has been cancelled
CI / Run Integration Tests (push) Has been cancelled
CI / Run Smoke Tests (push) Has been cancelled
CI / Go Tests (push) Has been cancelled
CI / Share Test (push) Has been cancelled
CI / Redteam (Production API) (push) Has been cancelled
CI / Redteam (Staging API) (push) Has been cancelled
CI / GitHub Actions Lint (push) Has been cancelled
CI / Check Ruby (3.0) (push) Has been cancelled
release-please / release-please (push) Has been cancelled
release-please / build (push) Has been cancelled
release-please / publish-npm (push) Has been cancelled
release-please / publish-npm-backfill (push) Has been cancelled
release-please / docker (push) Has been cancelled
release-please / publish-code-scan-action (push) Has been cancelled
release-please / attest-code-scan-action (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:24:08 +08:00

294 lines
9.9 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import {
getProviderResponseHeaders,
isProviderResponseRateLimited,
isTransientConnectionError,
} from '../../src/scheduler/types';
import type { ProviderResponse } from '../../src/types/providers';
describe('isProviderResponseRateLimited', () => {
describe('HTTP status detection', () => {
it('should detect 429 status in metadata.http.status', () => {
const result: ProviderResponse = {
output: 'error',
metadata: { http: { status: 429, statusText: 'Too Many Requests', headers: {} } },
};
expect(isProviderResponseRateLimited(result, undefined)).toBe(true);
});
it('should not flag other status codes', () => {
const result: ProviderResponse = {
output: 'success',
metadata: { http: { status: 200, statusText: 'OK', headers: {} } },
};
expect(isProviderResponseRateLimited(result, undefined)).toBe(false);
});
});
describe('Error field detection', () => {
it('should detect 429 in result.error string', () => {
const result: ProviderResponse = {
output: '',
error: 'HTTP 429: Too Many Requests',
};
expect(isProviderResponseRateLimited(result, undefined)).toBe(true);
});
it('should detect "rate limit" in result.error (case insensitive)', () => {
const result: ProviderResponse = {
output: '',
error: 'Rate Limit Exceeded',
};
expect(isProviderResponseRateLimited(result, undefined)).toBe(true);
});
it('should handle undefined result.error gracefully', () => {
const result: ProviderResponse = {
output: 'success',
};
expect(isProviderResponseRateLimited(result, undefined)).toBe(false);
});
});
describe('Thrown error detection', () => {
it('should detect 429 in error.message', () => {
const error = new Error('Request failed with status 429');
expect(isProviderResponseRateLimited(undefined, error)).toBe(true);
});
it('should detect "rate limit" in error.message (case insensitive)', () => {
const error = new Error('API rate limit exceeded');
expect(isProviderResponseRateLimited(undefined, error)).toBe(true);
});
it('should detect "too many requests" in error.message', () => {
const error = new Error('Too many requests, please slow down');
expect(isProviderResponseRateLimited(undefined, error)).toBe(true);
});
it('should handle error with undefined message gracefully', () => {
const error = new Error('placeholder');
(error as { message: string | undefined }).message = undefined;
expect(isProviderResponseRateLimited(undefined, error)).toBe(false);
});
it('should handle undefined error gracefully', () => {
expect(isProviderResponseRateLimited(undefined, undefined)).toBe(false);
});
});
describe('Combined detection', () => {
it('should detect rate limit in result even with error present', () => {
const result: ProviderResponse = {
output: '',
error: '429 rate limited',
};
const error = new Error('Some other error');
expect(isProviderResponseRateLimited(result, error)).toBe(true);
});
it('should detect rate limit in error even with result present', () => {
const result: ProviderResponse = {
output: 'success',
};
const error = new Error('Rate limit exceeded');
expect(isProviderResponseRateLimited(result, error)).toBe(true);
});
});
describe('Edge cases', () => {
it('should not false-positive on similar strings', () => {
const result: ProviderResponse = {
output: 'The rate of change is 42.9%',
error: 'Rating limit exceeded expectations',
};
// "Rating limit" is not "rate limit" - should not match
// But actually... "rating limit" contains "rate" and "limit" separately
// Let me check the actual logic - it uses .includes('rate limit')
expect(isProviderResponseRateLimited(result, undefined)).toBe(false);
});
it('should handle empty result object', () => {
const result: ProviderResponse = {};
expect(isProviderResponseRateLimited(result, undefined)).toBe(false);
});
it('should handle result with nested undefined metadata', () => {
const result: ProviderResponse = {
output: 'success',
metadata: {},
};
expect(isProviderResponseRateLimited(result, undefined)).toBe(false);
});
});
});
describe('getProviderResponseHeaders', () => {
it('should extract headers from metadata.http.headers', () => {
const result: ProviderResponse = {
output: 'success',
metadata: {
http: {
status: 200,
statusText: 'OK',
headers: {
'x-ratelimit-remaining': '10',
'x-ratelimit-limit': '100',
},
},
},
};
const headers = getProviderResponseHeaders(result);
expect(headers).toEqual({
'x-ratelimit-remaining': '10',
'x-ratelimit-limit': '100',
});
});
it('should extract headers from metadata.headers as fallback', () => {
const result: ProviderResponse = {
output: 'success',
metadata: {
headers: {
'retry-after': '60',
},
},
};
const headers = getProviderResponseHeaders(result);
expect(headers).toEqual({
'retry-after': '60',
});
});
it('should prefer metadata.http.headers over metadata.headers', () => {
const result: ProviderResponse = {
output: 'success',
metadata: {
http: {
status: 200,
statusText: 'OK',
headers: { 'x-from-http': 'true' },
},
headers: { 'x-from-meta': 'true' },
},
};
const headers = getProviderResponseHeaders(result);
expect(headers).toEqual({ 'x-from-http': 'true' });
});
it('should return undefined for result without headers', () => {
const result: ProviderResponse = {
output: 'success',
};
expect(getProviderResponseHeaders(result)).toBeUndefined();
});
it('should return undefined for undefined result', () => {
expect(getProviderResponseHeaders(undefined)).toBeUndefined();
});
it('should return undefined for result with empty metadata', () => {
const result: ProviderResponse = {
output: 'success',
metadata: {},
};
expect(getProviderResponseHeaders(result)).toBeUndefined();
});
});
describe('isTransientConnectionError', () => {
it('should detect bad record mac errors', () => {
expect(isTransientConnectionError(new Error('bad record mac'))).toBe(true);
});
it('should detect EPROTO errors', () => {
expect(isTransientConnectionError(new Error('write EPROTO 00000000:error:0A000126'))).toBe(
true,
);
});
it('should detect ECONNRESET errors', () => {
expect(isTransientConnectionError(new Error('ECONNRESET'))).toBe(true);
});
it('should detect socket hang up errors', () => {
expect(isTransientConnectionError(new Error('socket hang up'))).toBe(true);
});
it('should return false for undefined error', () => {
expect(isTransientConnectionError(undefined)).toBe(false);
});
it('should return false for non-connection errors', () => {
expect(isTransientConnectionError(new Error('Invalid API key'))).toBe(false);
});
it('should return false for rate limit errors', () => {
expect(isTransientConnectionError(new Error('429 Too Many Requests'))).toBe(false);
});
it('should not match EPROTO with permanent certificate/config errors', () => {
// wrong version number (HTTPS→HTTP mismatch)
expect(
isTransientConnectionError(
new Error('write EPROTO 00000000:error:0A000102:SSL routines::wrong version number'),
),
).toBe(false);
// self-signed certificate
expect(
isTransientConnectionError(
new Error('write EPROTO: self signed certificate in certificate chain'),
),
).toBe(false);
// unable to verify
expect(
isTransientConnectionError(new Error('write EPROTO: unable to verify the first certificate')),
).toBe(false);
// unknown CA
expect(isTransientConnectionError(new Error('write EPROTO: unknown ca'))).toBe(false);
// expired certificate
expect(isTransientConnectionError(new Error('write EPROTO: certificate has expired'))).toBe(
false,
);
// cert keyword
expect(isTransientConnectionError(new Error('write EPROTO: cert_untrusted'))).toBe(false);
});
it('should still match plain EPROTO without permanent phrases', () => {
expect(isTransientConnectionError(new Error('write EPROTO 00000000:error:0A000126'))).toBe(
true,
);
});
it('should detect ECONNRESET via error.code', () => {
const error = new Error('read failed');
(error as any).code = 'ECONNRESET';
expect(isTransientConnectionError(error)).toBe(true);
});
it('should detect EPIPE via error.code', () => {
const error = new Error('write failed');
(error as any).code = 'EPIPE';
expect(isTransientConnectionError(error)).toBe(true);
});
it('should not match ECONNREFUSED via error.code', () => {
const error = new Error('connect failed');
(error as any).code = 'ECONNREFUSED';
expect(isTransientConnectionError(error)).toBe(false);
});
it('should not match permanent SSL/TLS errors', () => {
expect(isTransientConnectionError(new Error('self signed certificate'))).toBe(false);
expect(isTransientConnectionError(new Error('unable to verify the first certificate'))).toBe(
false,
);
expect(isTransientConnectionError(new Error('certificate has expired'))).toBe(false);
expect(isTransientConnectionError(new Error('UNABLE_TO_GET_ISSUER_CERT'))).toBe(false);
expect(isTransientConnectionError(new Error('ssl routines:ssl3_read_bytes'))).toBe(false);
expect(isTransientConnectionError(new Error('tls alert unknown ca'))).toBe(false);
expect(isTransientConnectionError(new Error('wrong version number'))).toBe(false);
});
});