Files
wehub-resource-sync 0d3cb498a3
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
Deploy local.promptfoo.app / Deploy to Cloudflare Pages (push) Has been cancelled
Test and Publish Multi-arch Docker Image / test (push) Has been cancelled
Test and Publish Multi-arch Docker Image / build-docker-and-push-digests (map[digest-suffix:linux-amd64 platform:linux/amd64 runner:ubuntu-latest]) (push) Has been cancelled
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) Has been cancelled
Test and Publish Multi-arch Docker Image / merge-docker-digests (push) Has been cancelled
Test and Publish Multi-arch Docker Image / Attest Multi-arch Image (push) Has been cancelled
Validate Renovate Config / Validate Renovate Configuration (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:24:08 +08:00

132 lines
4.8 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import { DEFAULT_RETRY_POLICY, getRetryDelay, shouldRetry } from '../../src/scheduler/retryPolicy';
import { HttpRateLimitError } from '../../src/util/fetch/errors';
describe('getRetryDelay', () => {
const policy = { ...DEFAULT_RETRY_POLICY, jitterFactor: 0 }; // No jitter for predictable tests
it('should return 0 for immediate retry when serverRetryAfterMs is 0', () => {
const delay = getRetryDelay(1, policy, 0);
expect(delay).toBe(0);
});
it('should use server retry-after when provided', () => {
const delay = getRetryDelay(1, policy, 5000);
expect(delay).toBe(5000);
});
it('should cap delay at maxDelayMs', () => {
const delay = getRetryDelay(1, policy, 120000); // 2 minutes
expect(delay).toBe(policy.maxDelayMs);
});
it('should use exponential backoff when no server retry-after', () => {
const delay1 = getRetryDelay(1, policy, undefined);
const delay2 = getRetryDelay(2, policy, undefined);
const delay3 = getRetryDelay(3, policy, undefined);
// Exponential: baseDelay * 2^attempt
expect(delay1).toBe(policy.baseDelayMs * 2);
expect(delay2).toBe(policy.baseDelayMs * 4);
expect(delay3).toBe(policy.baseDelayMs * 8);
});
it('should cap exponential backoff at maxDelayMs', () => {
const delay = getRetryDelay(10, policy, undefined); // 2^10 = 1024x base
expect(delay).toBe(policy.maxDelayMs);
});
});
describe('shouldRetry', () => {
it('should retry on rate limit error', () => {
const result = shouldRetry(0, undefined, true, DEFAULT_RETRY_POLICY);
expect(result).toBe(true);
});
it('should not retry after max retries exceeded', () => {
const result = shouldRetry(
DEFAULT_RETRY_POLICY.maxRetries,
undefined,
true,
DEFAULT_RETRY_POLICY,
);
expect(result).toBe(false);
});
it('should retry on rate limit (429 should be detected externally and set isRateLimited=true)', () => {
// Note: 429 detection happens in the caller, not in shouldRetry
// The caller should pass isRateLimited=true for 429 errors
const error = new Error('HTTP 429: Too Many Requests');
const result = shouldRetry(0, error, true, DEFAULT_RETRY_POLICY);
expect(result).toBe(true);
});
it('should retry on 503 error message', () => {
const error = new Error('HTTP 503: Service Unavailable');
const result = shouldRetry(0, error, false, DEFAULT_RETRY_POLICY);
expect(result).toBe(true);
});
it('should not retry on generic error', () => {
const error = new Error('Some random error');
const result = shouldRetry(0, error, false, DEFAULT_RETRY_POLICY);
expect(result).toBe(false);
});
it('should handle error with undefined message gracefully', () => {
// Test edge case where error.message could be undefined
const error = new Error('placeholder');
(error as { message: string | undefined }).message = undefined;
const result = shouldRetry(0, error, false, DEFAULT_RETRY_POLICY);
expect(result).toBe(false);
});
it('should retry on timeout error', () => {
const error = new Error('Request timeout after 30000ms');
const result = shouldRetry(0, error, false, DEFAULT_RETRY_POLICY);
expect(result).toBe(true);
});
it('should retry on network error', () => {
const error = new Error('ECONNRESET');
const result = shouldRetry(0, error, false, DEFAULT_RETRY_POLICY);
expect(result).toBe(true);
});
it('should retry on bad record mac error', () => {
const error = new Error('bad record mac');
const result = shouldRetry(0, error, false, DEFAULT_RETRY_POLICY);
expect(result).toBe(true);
});
it('should retry on EPROTO error', () => {
const error = new Error('write EPROTO 00000000:error:0A000126');
const result = shouldRetry(0, error, false, DEFAULT_RETRY_POLICY);
expect(result).toBe(true);
});
it('should not retry on permanent certificate errors', () => {
const error = new Error('self signed certificate');
const result = shouldRetry(0, error, false, DEFAULT_RETRY_POLICY);
expect(result).toBe(false);
});
it('should not retry HttpRateLimitError with kind="quota" even when isRateLimited=true', () => {
// The transport-level fail-fast must not be undermined by the
// scheduler's retry-on-429 logic. Without this, a hard quota would
// get amplified by `policy.maxRetries` calls against an exhausted
// account.
const error = new HttpRateLimitError({ status: 429, code: 'insufficient_quota' });
expect(shouldRetry(0, error, true, DEFAULT_RETRY_POLICY)).toBe(false);
});
it('should retry HttpRateLimitError with kind="rate_limit"', () => {
const error = new HttpRateLimitError({
status: 429,
code: 'rate_limit_exceeded',
retryAfterMs: 5000,
});
expect(shouldRetry(0, error, true, DEFAULT_RETRY_POLICY)).toBe(true);
});
});