🧿 fix: Restore Shared IP Limits for Generation Retries (#15427)

* fix: Preserve IP Limits for Generation Retries

* test: Cover Scheduled Retry IP Limiting

* style: format retry limiter route
This commit is contained in:
Danny Avila 2026-08-31 14:11:12 -04:00 • committed by GitHub
parent 982a9a5141
commit 98eb3f8b1c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 20 additions and 13 deletions

View file

@ -87,15 +87,16 @@ describe('start-generation idempotency before message limiters', () => {
mockExemptSchedule.mockReturnValue(false);
});
it('lets a confirmed retry reach the controller without consuming either limiter', async () => {
it('keeps a confirmed retry behind the shared IP limiter', async () => {
mockHasGenerationClaim.mockResolvedValue(true);
mockIpLimiter.mockImplementationOnce((_req, _res, next) => next());
const response = await request(app).post('/agents/chat').send({ clientRequestId: 'request-1' });
expect(response.status).toBe(201);
expect(mockRetryProbeLimiter).toHaveBeenCalledTimes(1);
expect(mockRetryLimiter).toHaveBeenCalledTimes(1);
expect(mockIpLimiter).not.toHaveBeenCalled();
expect(mockIpLimiter).toHaveBeenCalledTimes(1);
expect(mockUserLimiter).not.toHaveBeenCalled();
});
@ -140,12 +141,9 @@ describe('start-generation idempotency before message limiters', () => {
expect(mockUserLimiter).not.toHaveBeenCalled();
});
it.each([
['an agent-trigger delivery', mockExemptAgentTrigger],
['a scheduled delivery', mockExemptSchedule],
])('keeps %s outside the human retry bucket', async (_label, exemption) => {
it('keeps an agent-trigger delivery outside the human retry bucket', async () => {
mockHasGenerationClaim.mockResolvedValue(true);
exemption.mockReturnValue(true);
mockExemptAgentTrigger.mockReturnValue(true);
const response = await request(app).post('/agents/chat').send({ clientRequestId: 'request-4' });
@ -156,6 +154,20 @@ describe('start-generation idempotency before message limiters', () => {
expect(mockUserLimiter).not.toHaveBeenCalled();
});
it('keeps a scheduled delivery outside the user retry bucket', async () => {
mockHasGenerationClaim.mockResolvedValue(true);
mockExemptSchedule.mockReturnValue(true);
const response = await request(app).post('/agents/chat').send({ clientRequestId: 'request-4' });
expect(response.status).toBe(429);
expect(response.body).toEqual({ limited: 'ip' });
expect(mockRetryProbeLimiter).not.toHaveBeenCalled();
expect(mockRetryLimiter).not.toHaveBeenCalled();
expect(mockIpLimiter).toHaveBeenCalledTimes(1);
expect(mockUserLimiter).not.toHaveBeenCalled();
});
it('keeps read-only queued-turn polling outside message admission limiters', async () => {
const responses = await Promise.all(
Array.from({ length: 3 }, () =>

View file

@ -1135,12 +1135,7 @@ if (useMessageIpLimiter || useMessageUserLimiter) {
}
if (useMessageIpLimiter) {
chatRouter.use(
unless(
(req) => exemptAgentTriggerFromIpLimiter(req) || isConfirmedGenerationRetry(req),
messageIpLimiter,
),
);
chatRouter.use(unless(exemptAgentTriggerFromIpLimiter, messageIpLimiter));
}
if (useMessageUserLimiter) {