(motir-core) `rateLimit/guard.test.ts` never aligns its window — the LAST unswept member of the epoch-boundary class, and the alignment guard is structurally blind to it
Repo: motir-core. One PR. Runtime tests only — no production code changes. Found by motir run MOTIR-2903 (PR #2120, run 32170310628, job 95819607587), on a diff that touches no limiter, route, store or middleware file.
The failure
FAIL tests/rateLimit/guard.test.ts > the 429 is spec-correct > carries Retry-After AND the X-RateLimit triple
AssertionError: expected null not to be null
❯ tests/rateLimit/guard.test.ts:47:21
tests/rateLimit/guard.test.ts:21 declares const WINDOW = 60_000 and every case builds its budgets from it. The failing case takes limit: 1, calls enforceRateLimit twice back to back, and expects the second to be refused — with no boundary alignment anywhere in the file.
lib/rateLimit/limiter.ts buckets on an epoch-aligned fixed grid (windowStart = Math.floor(now / windowMs) * windowMs), so the window does not open at the first call. When a minute boundary falls between the pair the counter resets, the second call is allowed, and second.response is null. Unlucky PHASE, not a slow runner — which is why it clears on a re-run and is invisible locally.
It is the WINDOW, not the fail-open — the discriminator was run
MOTIR-2658 established that a store timeout produces a byte-identical symptom. Checked, and it is not that: the job log contains exactly one [rateLimit] store unavailable; allowing the request line, at 18:28:23, and vitest's stderr | <file> > <test name> header attributes it to tests/api-ai-chat-route.test.ts. guard.test.ts ran at 18:34:00. Absent for this test ⇒ the window.
Why the existing fix did not reach it
MOTIR-2224 lifted the arithmetic into tests/helpers/rateLimitWindow.ts and swept the accumulating sites, and tests/api/v1/rate-limit-window-alignment.test.ts guards against regression. Neither reaches this file, for two separate reasons:
- The sweep was scoped to
tests/api/v1/**.tests/rateLimit/was not in it. Its siblingtests/rateLimit/surfaceGuards.test.tsdoes align (ALIGNED_WINDOW_MS+waitForWindowHeadroom, with a header explaining why) — so the directory is half-converted and this file is the half that was missed. - The guard cannot see it.
rate-limit-window-alignment.test.tsfails a file that recomputes a window phase outside the helper. This file does not recompute anything — it simply never aligns at all, and an absent call matches no grep. That is the same "sweep on the SYMPTOM, not on the helper name" lessonMOTIR-2224itself recorded, one turn later.
Acceptance criteria
tests/rateLimit/guard.test.tspins its window toALIGNED_WINDOW_MSand waits for headroom before every case that ACCUMULATES a count across two or more calls, followingtests/rateLimit/surfaceGuards.test.ts's existing shape in the same directory rather than introducing a third idiom. Cases that make a single call and assert only on shape need no wait; say per case which it is.- The
WINDOW = 60_000constant is replaced, not merely supplemented — a file holding both an aligned helper and a raw 60 s constant is the state this defect grew in. - The straddle is proven CONVERTED, not thinned, by measurement in the PR body: run the accumulating pair in a loop for at least one full cell, count the iterations whose start and end fall in different
Math.floor(t / windowMs)buckets, and report straddles → failures before and after.MOTIR-2224's numbers (14 straddles → 14 failures unaligned; 0 → 0 aligned) are the template. - The GUARD is widened to catch an absent alignment, not only a recomputed phase — that is what makes this the last instance instead of the next-to-last. Assert over
tests/**that every file which builds a rate-limit budget with an accumulating assertion imports the helper; a file that legitimately does not accumulate opts out by a named predicate or an enumerated allowlist with a reason per entry, never by silence. tests/rateLimit/is swept whole: report per file whether it accumulates, and whether it aligns.
Out of scope
lib/rateLimit/**— the limiter is correct. A fixed epoch grid is the intended design; the tests assert a race they do not control.MOTIR-2658's store-timeout override, which is a different mechanism with the same symptom.
Context refs
tests/rateLimit/guard.test.ts:21(the rawWINDOW),:35,:47(the failing pair).tests/rateLimit/surfaceGuards.test.ts:31-89— the aligned shape to copy, in the same directory.tests/helpers/rateLimitWindow.ts—ALIGNED_WINDOW_MS,waitForWindowBoundary,waitForWindowHeadroom.tests/api/v1/rate-limit-window-alignment.test.ts— the guard to widen.lib/rateLimit/limiter.ts—windowStart = Math.floor(now / windowMs) * windowMs.