moving
Some checks failed
Installer Smoke / installer-smoke (push) Has been cancelled

This commit is contained in:
Oleg Maslov
2026-09-02 10:10:29 +02:00
commit 0c3e2ead3b
3841 changed files with 970576 additions and 0 deletions

View File

@@ -0,0 +1,86 @@
import { describe, it, expect } from 'vitest';
import { handleNonOkResponse, initialRetryState } from '../src/retry-policy.js';
// R3-003: A 429 response can carry Retry-After as an HTTP-date
// (RFC 7231 §7.1.3) instead of delta-seconds. `parseInt` on the date string
// yields NaN, which propagates to waitMs (NaN * 1000 = NaN, Math.min(NaN, cap)
// = NaN). The agent loop then calls `setTimeout(r, NaN)`, which fires
// immediately — hammering the endpoint and burning the retry budget.
function rateLimitedResponse(retryAfter: string | null): Response {
const headers = new Headers();
if (retryAfter !== null) headers.set('retry-after', retryAfter);
return new Response('rate limited', { status: 429, headers });
}
describe('handleNonOkResponse — 429 Retry-After parsing (R3-003)', () => {
it('does not produce a NaN wait when Retry-After is an HTTP-date', async () => {
// A future HTTP-date should resolve to a finite, positive wait
// (seconds until that date), capped at MAX_RATE_LIMIT_WAIT_MS (60s).
const futureDate = new Date(Date.now() + 30_000).toUTCString();
const action = await handleNonOkResponse(
rateLimitedResponse(futureDate),
initialRetryState(),
);
expect(action.kind).toBe('retry');
if (action.kind !== 'retry') throw new Error('expected retry');
// The core bug: waitMs must be a finite number, never NaN.
expect(Number.isFinite(action.waitMs)).toBe(true);
// ~30s out, allowing a little slack for clock/rounding.
expect(action.waitMs).toBeGreaterThan(25_000);
expect(action.waitMs).toBeLessThanOrEqual(60_000);
// And the human-readable notice must not say "waiting NaNs".
expect(action.notice).not.toContain('NaN');
});
it('clamps a past HTTP-date to a finite non-negative wait', async () => {
const action = await handleNonOkResponse(
rateLimitedResponse('Wed, 21 Oct 2020 07:28:00 GMT'),
initialRetryState(),
);
expect(action.kind).toBe('retry');
if (action.kind !== 'retry') throw new Error('expected retry');
// Past date → retry now (0ms), but crucially finite — not NaN.
expect(Number.isFinite(action.waitMs)).toBe(true);
expect(action.waitMs).toBeGreaterThanOrEqual(0);
expect(action.notice).not.toContain('NaN');
});
it('falls back to the sane default for a non-numeric Retry-After', async () => {
const action = await handleNonOkResponse(
rateLimitedResponse('garbage'),
initialRetryState(),
);
expect(action.kind).toBe('retry');
if (action.kind !== 'retry') throw new Error('expected retry');
// Default backoff is 5s.
expect(action.waitMs).toBe(5_000);
});
it('still honours a plain numeric (delta-seconds) Retry-After', async () => {
const action = await handleNonOkResponse(
rateLimitedResponse('12'),
initialRetryState(),
);
expect(action.kind).toBe('retry');
if (action.kind !== 'retry') throw new Error('expected retry');
expect(action.waitMs).toBe(12_000);
});
it('still uses the default when Retry-After is absent', async () => {
const action = await handleNonOkResponse(
rateLimitedResponse(null),
initialRetryState(),
);
expect(action.kind).toBe('retry');
if (action.kind !== 'retry') throw new Error('expected retry');
expect(action.waitMs).toBe(5_000);
});
});