token-bucket.test.js 3.02 KB
import { describe, it, expect } from 'vitest'
import {
  initialState,
  refill,
  waitMsFor,
  take,
  peek,
} from '../src/token-bucket.js'

const rule = { capacity: 3, refillPerSec: 1 } // 1 token/s, 桶容量 3

describe('token-bucket', () => {
  it('初始状态是满桶', () => {
    const s = initialState(rule, 1000)
    expect(s.tokens).toBe(3)
    expect(s.lastRefillMs).toBe(1000)
  })

  it('突发:满桶内连续扣减全放行,扣到空', () => {
    let s = initialState(rule, 0)
    for (let i = 0; i < 3; i++) {
      const r = take(s, 1, rule, 0)
      expect(r.allowed).toBe(true)
      s = r.next
    }
    expect(s.tokens).toBe(0)
  })

  it('空桶不放行,retryAfterMs 准确(缺 1 个、速率 1/s → 1000ms)', () => {
    const empty = { tokens: 0, lastRefillMs: 0 }
    const r = take(empty, 1, rule, 0)
    expect(r.allowed).toBe(false)
    expect(r.remaining).toBe(0)
    expect(r.retryAfterMs).toBe(1000)
    expect(r.next.tokens).toBe(0) // 拒绝不扣
  })

  it('按注入 nowMs 推进补充令牌', () => {
    const empty = { tokens: 0, lastRefillMs: 0 }
    // 2.5s 后应补 2.5 个令牌
    const filled = refill(empty, rule, 2500)
    expect(filled.tokens).toBeCloseTo(2.5)
    // 取 1 个应放行
    const r = take(empty, 1, rule, 2500)
    expect(r.allowed).toBe(true)
  })

  it('补充不超过 capacity', () => {
    const empty = { tokens: 0, lastRefillMs: 0 }
    const filled = refill(empty, rule, 10_000) // 想补 10 个
    expect(filled.tokens).toBe(3) // 封顶 capacity
  })

  it('突发到 capacity 后稳态退回 refill 速率', () => {
    let s = initialState(rule, 0)
    // 突发吃光 3 个
    for (let i = 0; i < 3; i++) s = take(s, 1, rule, 0).next
    // 此后每秒只补 1 个:t=1000 放行 1 个,t=1001 再取被拒
    const a = take(s, 1, rule, 1000)
    expect(a.allowed).toBe(true)
    const b = take(a.next, 1, rule, 1001)
    expect(b.allowed).toBe(false)
    expect(b.retryAfterMs).toBeGreaterThan(0)
  })

  it('refillPerSec=0:固定桶耗尽不恢复,等待为无穷', () => {
    const fixed = { capacity: 2, refillPerSec: 0 }
    const empty = { tokens: 0, lastRefillMs: 0 }
    const r = take(empty, 1, fixed, 999_999)
    expect(r.allowed).toBe(false)
    expect(r.retryAfterMs).toBe(Number.POSITIVE_INFINITY)
  })

  it('时钟回拨不倒扣令牌', () => {
    const s = { tokens: 2, lastRefillMs: 5000 }
    const filled = refill(s, rule, 1000) // nowMs 早于 lastRefillMs
    expect(filled.tokens).toBe(2)
  })

  it('peek 不修改余额(无 next 写回语义)', () => {
    const s = { tokens: 1, lastRefillMs: 0 }
    const r = peek(s, 1, rule, 0)
    expect(r.allowed).toBe(true)
    expect(r.remaining).toBe(1)
  })

  it('waitMsFor: deficit<=0 → 0; 速率>0 向上取整', () => {
    expect(waitMsFor(0, rule)).toBe(0)
    expect(waitMsFor(-1, rule)).toBe(0)
    expect(waitMsFor(0.5, { refillPerSec: 1 })).toBe(500)
    expect(waitMsFor(0.001, { refillPerSec: 1 })).toBe(1) // ceil
    expect(waitMsFor(1, { refillPerSec: 0 })).toBe(Number.POSITIVE_INFINITY)
  })
})