From ec2b530be7a2656d68a072e60b6e7f8a7e42db0b Mon Sep 17 00:00:00 2001 From: wtclaude Date: Wed, 12 Aug 2026 08:04:56 -0500 Subject: [PATCH] test(login): stop the backoff-guard test racing its own one-second lock MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A single recordFailure() locks for BASE_MS * 2 ** 0 — exactly one second — and the test then does a real HTTP round trip against it. On CI that round trip took 1,456 ms and the guard correctly answered 200, failing the run for a reason that has nothing to do with what the test is about. Five failures lock for sixteen seconds. The subject is the guard's answer while locked out, which is unchanged. Co-Authored-By: Claude --- server/test/loginProtection.test.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/server/test/loginProtection.test.js b/server/test/loginProtection.test.js index 5d10b41..0163447 100644 --- a/server/test/loginProtection.test.js +++ b/server/test/loginProtection.test.js @@ -60,7 +60,13 @@ test('backoffGuard returns a generic 429 while locked out', async () => { a.post('/login', lp.backoffGuard, (req, res) => res.json({ ok: true })) }) try { - lp.recordFailure('203.0.113.40') // lock the test client IP + // Lock the test client IP. FIVE failures, not one: the lock is + // `BASE_MS * 2 ** (count - 1)`, so a single failure locks for exactly one + // second and this test then races the round trip. It lost that race on CI + // (200 instead of 429, request arriving 1,456 ms after the lock). Five + // failures lock for sixteen seconds, which is not a race. What is under + // test is the guard's ANSWER while locked out, and that is unchanged. + for (let i = 0; i < 5; i += 1) lp.recordFailure('203.0.113.40') const res = await fetch(`${app.url}/login`, { method: 'POST', headers: { 'X-Forwarded-For': '203.0.113.40' },