Skip to content

test: stop rate-limit tests straddling a fixed-window boundary - #170

Merged
yo1t merged 2 commits into
mainfrom
fix/rate-limit-window-boundary-flake
Aug 3, 2026
Merged

test: stop rate-limit tests straddling a fixed-window boundary#170
yo1t merged 2 commits into
mainfrom
fix/rate-limit-window-boundary-flake

Conversation

@yo1t

@yo1t yo1t commented Aug 3, 2026

Copy link
Copy Markdown
Owner

The failure

accepts Keycloak azp as the client identifier failed on main under Node 24 while passing under Node 22 — which reads like a version problem and is not one.

✖ accepts Keycloak azp as the client identifier (45.24303ms)
  AssertionError: azp must identify the client when client_id is absent

Why it happened

The limiter counts into a window keyed on Math.floor(now / 60_000) (src/mcp-rate-limit.js:35), so the window is aligned to the wall clock, not to a caller's first request. Two calls a millisecond apart land in different windows when they straddle a minute boundary, and the second starts a fresh count — so "the next call is 429" sees 200.

The failing run reported the case at 14:47:00.037 after 45 ms, meaning it started at 14:46:59.99 and crossed the boundary mid-test. Node 24 was coincidence: that job happened to reach the case at the wrong moment.

Changes

1. Pin the boundary behaviour deterministically in the rate-limit unit tests, using the injectable clock the limiter already accepts. This is the accepted trade-off of a fixed window, not a defect — but it should be documented rather than rediscovered through a flake. A retryAfterSeconds case also shows the value points at the reset rather than a full window from now.

2. Guard the seven HTTP-boundary tests whose assertions need two or more calls to share a window. They wait only when the current window has less than five seconds left, so the common path costs nothing.

The product is unchanged

Adding a window-length knob purely so tests could sidestep this would have put test convenience into the rate limiter.

Scope note

The limiter-level reset is proven deterministically and the CI timestamp matches it exactly, but I could not reproduce the HTTP-level flake on demand — it needs the boundary to fall between two specific calls, a millisecond-wide target. Running the whole suite across a boundary passes either way, because the affected cases sit late enough in the file to already be past it.

Gate Result
Unit 1,941 pass / 0 fail
Affected suites, 3 consecutive runs 37 pass each
ESLint clean

🤖 Generated with Claude Code

yo1t added 2 commits August 4, 2026 07:10
`accepts Keycloak azp as the client identifier` failed on main under Node
24 while passing under Node 22, which reads like a version problem and is
not one.

The limiter counts into a window keyed on Math.floor(now / 60_000), so
the window is aligned to the wall clock rather than to a caller's first
request. Two calls a millisecond apart land in different windows when
they straddle a minute boundary, and the second starts a fresh count --
so an assertion that the next call is 429 sees 200 instead.

The failing run reported the case at 14:47:00.037 after 45 ms, meaning it
started at 14:46:59.99 and crossed the boundary mid-test. Node 24 was
coincidence: that job happened to reach the case at the wrong moment.

Two changes:

- Pin the boundary behaviour deterministically in the rate-limit unit
  tests using the injectable clock the limiter already accepts, so this
  property is documented rather than rediscovered through a flake. It is
  the accepted trade-off of a fixed window, not a defect, and the
  retryAfterSeconds case shows the value points at the reset.

- Guard the seven HTTP-boundary tests whose assertions need two or more
  calls to share a window. They now wait only when the current window has
  less than five seconds left, so the common path costs nothing.

The product is unchanged. Adding a window-length knob purely so tests
could avoid this would have put test convenience into the rate limiter.

Honest scope note: the limiter-level reset is proven deterministically,
and the CI timestamp matches it exactly, but I could not reproduce the
HTTP-level flake on demand -- it needs the boundary to fall between two
specific calls, a millisecond-wide target. Running the suite across a
boundary passes either way, because the affected cases sit late enough in
the file to be past it.

Unit 1,941 pass, lint clean.
@yo1t
yo1t merged commit f456e3b into main Aug 3, 2026
11 checks passed
@yo1t
yo1t deleted the fix/rate-limit-window-boundary-flake branch August 3, 2026 22:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant