logos-delivery/tests/messaging/test_rate_limit_manager.nim
Ivan FB 861376cd3c
feat: enforce per-epoch budget in RateLimitManager.admit
admit now meters each transmission against messagesPerEpoch and rolls the
epoch window when epochPeriodSec elapses, instead of admitting every call.
This is the client-side rate gate the SendService already calls into.

Drop the unused queue field and dequeueReady: the SendService owns the
pending work as DeliveryTasks and re-offers parked ones each loop tick, so
the manager needs no queue of its own. admit keeps its signature so both
call sites stay untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 17:49:17 +02:00

36 lines
1.2 KiB
Nim

{.used.}
import chronos, testutils/unittests, stew/byteutils
import logos_delivery/messaging/rate_limit_manager/rate_limit_manager
suite "RateLimitManager - admission":
asyncTest "admit is a pass-through when disabled":
let rl = RateLimitManager.new(
RateLimitConfig(enabled: false, epochPeriodSec: 600, messagesPerEpoch: 1)
)
for _ in 0 ..< 10:
let res = await rl.admit("payload".toBytes())
check res.isOk()
asyncTest "admit enforces the per-epoch budget when enabled":
let rl = RateLimitManager.new(
RateLimitConfig(enabled: true, epochPeriodSec: 600, messagesPerEpoch: 1)
)
check (await rl.admit("first".toBytes())).isOk()
let overBudget = await rl.admit("second".toBytes())
check overBudget.isErr()
check overBudget.error == RateLimitError.OverBudget
asyncTest "admit reopens the budget after the epoch rolls over":
let rl = RateLimitManager.new(
RateLimitConfig(enabled: true, epochPeriodSec: 600, messagesPerEpoch: 1)
)
check (await rl.admit("first".toBytes())).isOk()
check (await rl.admit("second".toBytes())).isErr()
# Simulate the epoch window elapsing; the next call must be admitted again.
rl.resetEpoch()
check (await rl.admit("third".toBytes())).isOk()