"""RedisLimiter 纯函数部分(key 布局、秒毫秒换算)——不需要真实 Redis。""" import pytest from polygateway.backends.redis.limiter import RedisLimiter from polygateway.types import GlobalLimits, SourceConfig class _StubRedis: """仅满足构造期 register_script 的桩;任何执行路径不可达。""" def register_script(self, script: str): def _never(**kwargs): raise AssertionError("unit 测试不应执行 Lua") return _never def _limiter(**overrides) -> RedisLimiter: src = SourceConfig( name="s1", provider="p", base_url="https://gw.example/v1", api_key="sk", model="m", timeout_s=10.0, ) base = { "scope": "llm", "sources": {"s1": src}, "global_limits": GlobalLimits(max_concurrency=0, rpm=0, tpm=0), "redis": _StubRedis(), "lease_ttl_s": 30.0, } base.update(overrides) return RedisLimiter(**base) class TestKeyLayout: def test_lease_keys_prefixed_pgw(self): gl, sl = _limiter()._lease_keys("s1") assert gl == "pgw:limit:GLOBAL:llm:lease" assert sl == "pgw:limit:llm:s1:lease" def test_window_keys_carry_window_suffix(self): wk = _limiter()._window_keys("s1", 12345) assert wk["g_rpm"] == "pgw:limit:GLOBAL:llm:rpm:12345" assert wk["s_rpm"] == "pgw:limit:llm:s1:rpm:12345" assert wk["g_tpm"] == "pgw:limit:GLOBAL:llm:tpm:12345" assert wk["s_tpm"] == "pgw:limit:llm:s1:tpm:12345" def test_progress_key_scope_global(self): assert _limiter()._progress_key() == "pgw:limit:GLOBAL:llm:progress_ms" def test_scope_lowercased(self): gl, _ = _limiter(scope="LLM")._lease_keys("s1") assert gl == "pgw:limit:GLOBAL:llm:lease" class TestConversions: def test_lease_ttl_seconds_to_ms(self): # 契约量纲为秒,Redis 内部毫秒是后端私事(ports.py docstring) assert _limiter(lease_ttl_s=30.0)._lease_ttl_ms == 30_000 assert _limiter(lease_ttl_s=0.5)._lease_ttl_ms == 500 def test_invalid_lease_ttl_rejected(self): with pytest.raises(ValueError): _limiter(lease_ttl_s=0) def test_unknown_source_rejected(self): """未知源是装配缺陷,不是后端故障(issue #7 §3.4)。""" from polygateway.errors import GatewayUnavailableError, SourceNotConfiguredError with pytest.raises(SourceNotConfiguredError) as ei: _limiter()._cfg("nope") # 关键: 若归入 scope 级家族,配置写错的任务会永远延期重投、永不进死信 assert not isinstance(ei.value, GatewayUnavailableError) class TestLuaFidelity: """Lua 常量的移植锚点:守卫与判据语义(逐段比对 CHS scripts.py:6-34)。""" def test_acquire_has_zero_disabled_guards(self): """0=闸不启用 是对 CHS 的有意偏离(设计 §9 勘误):每道闸带 limit>0 守卫。""" from polygateway.backends.redis.limiter import ACQUIRE assert ACQUIRE.count("> 0 and") == 6 def test_acquire_keeps_chs_gate_operators(self): """并发/RPM 用 >=(占后即满),TPM 用 + est >(预扣后是否超)——CHS 同款。""" from polygateway.backends.redis.limiter import ACQUIRE assert ACQUIRE.count(">=") == 4 assert ACQUIRE.count("+ est >") == 2 def test_settle_lands_on_acquire_window(self): """SETTLE 的 key 由 Python 侧按 acquire 时窗口生成;Lua 只做 INCRBY。""" from polygateway.backends.redis.limiter import SETTLE assert "INCRBY" in SETTLE and "TIME" not in SETTLE