diff --git a/api/server/routes/agents/__tests__/idempotencyLimiter.spec.js b/api/server/routes/agents/__tests__/idempotencyLimiter.spec.js index 373266a6c0..2624a654ee 100644 --- a/api/server/routes/agents/__tests__/idempotencyLimiter.spec.js +++ b/api/server/routes/agents/__tests__/idempotencyLimiter.spec.js @@ -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 }, () => diff --git a/api/server/routes/agents/index.js b/api/server/routes/agents/index.js index 1805d9e135..3ffe49835f 100644 --- a/api/server/routes/agents/index.js +++ b/api/server/routes/agents/index.js @@ -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) {