test: property-check the auth rate limiter against a sliding-window oracle - #593
Conversation
…racle Three fast-check properties over the real module under fake timers (maxAttempts 3 / 1s window so sequences cross the expiry boundary): - model-based: for any record/reset/advance interleaving across three accounts with decorated id spellings (case/whitespace variants), getAttemptsRemaining and canAttemptAuth match a trivial timestamps-in-window oracle keyed by canonical id - pinning both the sliding window and the trim+lowercase bucket mapping at once - checkAuthRateLimit throws AuthRateLimitError exactly when blocked, carrying the canonical accountId, zero attemptsRemaining, and a resetAfterMs that agrees with the live getTimeUntilReset - over-recording past maxAttempts can never wedge a bucket: any burst unblocks fully after one quiet window, with getTimeUntilReset bounded by windowMs throughout Config and registry are module state, so each property restores the documented defaults and clears buckets, and ids are namespaced per iteration. Companion to #574/#575/#579/#592; same conventions. https://claude.ai/code/session_01XNtnkLbBiXZxfQQYLMpucB
|
Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits. |
|
Caution Review failedPull request was closed or merged during review 📝 WalkthroughThis pull request introduces a comprehensive property-based test suite for the auth rate limiter module using Vitest and fast-check. The addition is test-only (no production code changes) and strengthens security validation by covering edge cases across interleaved rate-limit operations, decorated account ID variants, and boundary conditions. All existing tests remain unchanged, ensuring regression coverage is maintained. The suite validates three core properties: (1) a sliding-window oracle that tracks attempt timestamps and verifies both Walkthroughadds a single property-based test file at ChangesAuth Rate Limit Property Tests
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes a few things to flag explicitly: missing edge cases at
no regression tests flagged for windows-style environments: if 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
✨ Simplify code
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
test/property/auth-rate-limit.property.test.tsOops! Something went wrong! :( ESLint: 10.0.0 Error: The 'jiti' library is required for loading TypeScript configuration files. Make sure to install it. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Greptile flagged the bare else branches (a future event kind would silently model as a reset) and the literal 11 coupled to MAX_ATTEMPTS * 4 - 1. https://claude.ai/code/session_01XNtnkLbBiXZxfQQYLMpucB
…contracts (#594) * test: property-check SessionAffinityStore TTL, eviction, and reindex contracts Five fast-check properties over the real store using its injectable now parameters (1s TTL floor so sequences cross expiry often): - model-based TTL/upsert equivalence: for any remember/update/forget/ advance interleaving through whitespace-decorated key spellings, getPreferredAccountIndex and getLastResponseId match a trivial TTL map (remember preserves the continuation id, response-id writes refresh expiry and never create entries) - capacity: size() never exceeds maxEntries, and LRU eviction can never evict the entry just written - write-version conflicts: a stale version loses to a live entry on both the index and response-id channels, but may rebind once the entry expires - forgetAccount + reindexAfterRemoval mirror an account-array splice, with both return counts pinned against the model - prune removes exactly the expired entries; lazily-reaped sessions (touched while expired) correctly do not count as prunable The prune model initially missed that updateLastResponseId deletes an expired entry outright; fast-check found the 8-event counterexample and the model now mirrors the lazy reap. Companion to #574/#575/#579/#592/#593; same conventions. https://claude.ai/code/session_01XNtnkLbBiXZxfQQYLMpucB * test: fix remember model after lazy reap; cover clearAll; decorate prune keys Greptile P1: the model carried a responseId across expiry, but the assertion block's reads lazily reap expired entries from the store, so a remember after expiry finds no existing entry and the id is gone - the model now inherits the id only from a live entry (verified at FAST_CHECK_NUM_RUNS=1000, where the original 4-event counterexample sequence reproduces without the fix). P2s: a sixth property pins clearAll (#474 invalidation) - size drops to zero, every read goes null, and the store stays usable - and the prune property now routes remember/forget/updateLastResponseId through decorated key spellings like the model property does. https://claude.ai/code/session_01XNtnkLbBiXZxfQQYLMpucB --------- Co-authored-by: Claude <[email protected]>
Summary
lib/auth-rate-limit.ts— the sliding-window limiter that gates OAuth attempts per account. The natural companion to test: property-check the circuit breaker's availability contract #592's circuit-breaker suite: this module is the L3 convention's canonical module-state example, and its oracle (timestamps-in-window) is trivial enough for a clean model-based test.What Changed
New
test/property/auth-rate-limit.property.test.ts(3 properties, real module undervi.useFakeTimers()with a 3-attempt / 1-second config so sequences cross the expiry boundary):getAttemptsRemainingandcanAttemptAuthmatch a trivial timestamps-in-window oracle keyed by canonical id. One property pins both the sliding window arithmetic and the trim+lowercase bucket mapping.checkAuthRateLimitthrowsAuthRateLimitErrorexactly whencanAttemptAuthis false, and the error's payload is live: the canonical accountId,attemptsRemaining === 0, andresetAfterMsagreeing withgetTimeUntilReset(and strictly positive).recordAuthAttemptdoesn't cap atmaxAttempts, so a burst can stack more timestamps than the limit; the property proves any burst (up to 4× the limit, arbitrary intra-burst gaps) still unblocks completely after one quiet window, withgetTimeUntilResetbounded bywindowMsthroughout.Validation
npm test -- test/property/auth-rate-limit.property.test.ts test/auth-rate-limit.test.ts— 25/25 (new 3 + existing 22 untouched)npm run typecheck(also via pre-commit hook)npx eslint test/property/auth-rate-limit.property.test.ts --max-warnings=0afterEach; ids namespaced per fc iteration so no run can observe another's bucketsDocs and Governance Checklist
Risk and Rollback
test/property/suites (explicit vitest imports, plainfc.assert, fake timers scoped withtry/finally).https://claude.ai/code/session_01XNtnkLbBiXZxfQQYLMpucB
Generated by Claude Code
note: greptile review for oc-chatgpt-multi-auth. cite files like
lib/foo.ts:123. confirm regression tests + windows concurrency/token redaction coverage.Greptile Summary
adds
test/property/auth-rate-limit.property.test.ts, a three-property fast-check suite for the sliding-window oauth rate limiter, covering model-based oracle equivalence, gate-throw fidelity, and burst-then-expiry unblocking.getAttemptsRemaining/canAttemptAuthagainst a timestamp-filter oracle after every step.checkAuthRateLimitthrowsAuthRateLimitErrorexactly whencanAttemptAuthis false and that the error fields match live module state — but only exercises a pre-canonical id, leavinggateError.accountIdunverified for decorated inputs.MAX_ATTEMPTS) unblocks completely after one quiet window, withgetTimeUntilResetbounded throughout.Confidence Score: 4/5
safe to merge as-is, but property 2 contains a coverage gap that lets a real module behaviour (raw accountId echoed in AuthRateLimitError) go undetected
the suite is additive and the first and third properties are logically sound. the second property's gateError.accountId assertion uses a pre-canonical id, so it cannot catch the module storing the raw caller-supplied string in the error. this is a live gap between what the PR description claims is tested and what is actually exercised — downstream consumers of AuthRateLimitError.accountId who expect the canonical form could receive a decorated string in production
test/property/auth-rate-limit.property.test.ts — property 2 needs a decorated-spelling path through checkAuthRateLimit to validate gateError.accountId
Important Files Changed
Prompt To Fix All With AI
Reviews (3): Last reviewed commit: "Merge branch 'main' into claude/audit-74..." | Re-trigger Greptile