mirror of
https://github.com/ruvnet/RuVector.git
synced 2026-05-30 03:53:34 +00:00
test(hailo): lock in iter-200 check_n behavior (iter 201)
iter-200 added `RateLimiter::check_n(peer, n)` to debit the
streaming-batch length against the per-peer rate limiter, then
wired it into `embed_stream`. Both code paths shipped without
direct test coverage. Add five focused unit tests covering the
contract:
check_n_zero_is_a_noop
n=0 must not consume tokens (the embed_stream caller passes
n-1 after the interceptor's 1, so for batch=1 the call is
n=0). Repeated zero-calls don't burn the bucket; a normal
check still succeeds afterwards.
check_n_within_burst_consumes_n_tokens
1 rps / burst 5: check_n(3) leaves 2 tokens; two more singleton
checks pass; the third fails. Locks in the "actually consumes
n tokens" property.
check_n_exceeding_burst_is_denied
1 rps / burst 4: check_n(8) returns Err (governor's
InsufficientCapacity collapsed to RateLimitDenied). The bucket
is unchanged — the failed attempt does NOT burn any tokens, so
4 singleton checks still pass after.
check_n_partial_capacity_denied_without_consuming
Burn 2 of 4, then check_n(3) — tokens-needed (2 + 3 = 5) > 4 so
denied. The 2 already-burned tokens stay burned; the failed
check_n doesn't roll them back. Verifies the failure mode is
"deny + don't side-effect."
check_n_separate_peers_have_independent_buckets
A streaming-batch debit on peer-a must not bleed into peer-b's
quota — proves the per-peer keying still holds for check_n.
Validated:
- rate_limit lib tests: 7 → 12 (+5 iter 201)
- full lib : 103 → 108
- full integration sweep : 181 → 186 tests, 0 failures
- all flaky tests still green (iter-196/197 fixes hold)
Pi worker untouched; pure test-side addition.
Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
parent
0ffff492bf
commit
1d8d64b26f
1 changed files with 72 additions and 0 deletions
|
|
@ -223,6 +223,78 @@ mod tests {
|
|||
assert!(RateLimiter::new(0, 0).is_none());
|
||||
}
|
||||
|
||||
// ---- check_n tests (iter 200 API, locked in iter 201) ----
|
||||
|
||||
#[test]
|
||||
fn check_n_zero_is_a_noop() {
|
||||
// n=0 must not consume tokens and must not error — the
|
||||
// embed_stream caller passes n-1 after the interceptor's 1
|
||||
// already debited, so for batch=1 the call is n=0.
|
||||
let r = RateLimiter::new(1, 1).expect("non-zero quota");
|
||||
for _ in 0..10 {
|
||||
assert!(r.check_n("peer-a", 0).is_ok());
|
||||
}
|
||||
// Bucket untouched: a single normal check still passes.
|
||||
assert!(r.check("peer-a").is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_n_within_burst_consumes_n_tokens() {
|
||||
// 1 rps, burst 5. check_n(3) consumes 3; one more check
|
||||
// succeeds (4th token); two more fail.
|
||||
let r = RateLimiter::new(1, 5).expect("non-zero quota");
|
||||
assert!(r.check_n("peer-a", 3).is_ok());
|
||||
assert!(r.check("peer-a").is_ok(), "4th token should still fit");
|
||||
assert!(r.check("peer-a").is_ok(), "5th token should still fit");
|
||||
assert!(r.check("peer-a").is_err(), "6th must be rate-limited");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_n_exceeding_burst_is_denied() {
|
||||
// 1 rps, burst 4. check_n(8) is bigger than the bucket can
|
||||
// ever hold → governor returns InsufficientCapacity, which
|
||||
// we collapse to RateLimitDenied. The bucket itself is
|
||||
// unchanged (still has all 4 tokens available).
|
||||
let r = RateLimiter::new(1, 4).expect("non-zero quota");
|
||||
assert!(r.check_n("peer-a", 8).is_err());
|
||||
// Verify no tokens were burned by the failed attempt: 4
|
||||
// singletons should still pass.
|
||||
for _ in 0..4 {
|
||||
assert!(r.check("peer-a").is_ok());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_n_partial_capacity_denied_without_consuming() {
|
||||
// 1 rps, burst 4. Burn 2 with check, then check_n(3) — that's
|
||||
// 2 + 3 = 5 > 4 → denied. The 2 already-burned tokens stay
|
||||
// burned; check_n's denial does NOT roll back.
|
||||
let r = RateLimiter::new(1, 4).expect("non-zero quota");
|
||||
assert!(r.check("peer-a").is_ok());
|
||||
assert!(r.check("peer-a").is_ok());
|
||||
assert!(
|
||||
r.check_n("peer-a", 3).is_err(),
|
||||
"3 tokens beyond the remaining 2 must be denied"
|
||||
);
|
||||
// 2 tokens remaining: 2 singleton checks pass.
|
||||
assert!(r.check("peer-a").is_ok());
|
||||
assert!(r.check("peer-a").is_ok());
|
||||
assert!(r.check("peer-a").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_n_separate_peers_have_independent_buckets() {
|
||||
// Streaming-batch debits on one peer must not bleed into
|
||||
// another peer's quota.
|
||||
let r = RateLimiter::new(1, 4).expect("non-zero quota");
|
||||
assert!(r.check_n("peer-a", 4).is_ok());
|
||||
assert!(r.check("peer-a").is_err(), "peer-a fully consumed");
|
||||
// peer-b's bucket is untouched.
|
||||
assert!(r.check_n("peer-b", 4).is_ok());
|
||||
assert!(r.check("peer-b").is_err());
|
||||
assert_eq!(r.tracked_peers(), 2);
|
||||
}
|
||||
|
||||
// Iter 197 — both tests below mutate the same process-global env
|
||||
// vars (`RUVECTOR_RATE_LIMIT_RPS` / `_BURST`). Cargo runs tests in
|
||||
// parallel by default, so without serialization the wipe in
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue