from __future__ import annotations import asyncio import time from yuxi.channel.routing.cache import RouteCache, get_default_route_cache class TestRouteCache: async def test_get_returns_none_for_missing_key(self): cache = RouteCache() assert await cache.get("missing") is None async def test_set_and_get(self): cache = RouteCache() await cache.set("key", "value") assert await cache.get("key") == "value" async def test_get_moves_entry_to_end(self): cache = RouteCache(maxsize=2) await cache.set("a", 1) await cache.set("b", 2) await cache.get("a") await cache.set("c", 3) assert await cache.get("a") == 1 assert await cache.get("b") is None assert await cache.get("c") == 3 async def test_expired_entry_is_removed(self): cache = RouteCache(ttl_seconds=0.01) await cache.set("key", "value") await asyncio.sleep(0.02) assert await cache.get("key") is None async def test_set_updates_existing_key(self): cache = RouteCache() await cache.set("key", "old") await cache.set("key", "new") assert await cache.get("key") == "new" async def test_maxsize_evicts_oldest(self): cache = RouteCache(maxsize=2) await cache.set("a", 1) await cache.set("b", 2) await cache.set("c", 3) assert await cache.get("a") is None assert await cache.get("b") == 2 assert await cache.get("c") == 3 async def test_maxsize_at_least_one(self): cache = RouteCache(maxsize=0) await cache.set("a", 1) await cache.set("b", 2) assert await cache.get("a") is None assert await cache.get("b") == 2 async def test_clear_removes_all(self): cache = RouteCache() await cache.set("a", 1) await cache.set("b", 2) await cache.clear() assert await cache.get("a") is None assert await cache.get("b") is None async def test_invalidate_by_channel(self): cache = RouteCache() await cache.set("feishu:acc1:dm:user1", 1) await cache.set("feishu:acc1:dm:user2", 2) await cache.set("slack:acc1:dm:user1", 3) removed = await cache.invalidate_by_channel("feishu") assert removed == 2 assert await cache.get("feishu:acc1:dm:user1") is None assert await cache.get("slack:acc1:dm:user1") == 3 async def test_invalidate_by_account(self): cache = RouteCache() await cache.set("feishu:acc1:dm:user1", 1) await cache.set("feishu:acc2:dm:user1", 2) await cache.set("feishu:acc1:group:g1", 3) removed = await cache.invalidate_by_account("feishu", "acc1") assert removed == 2 assert await cache.get("feishu:acc1:dm:user1") is None assert await cache.get("feishu:acc2:dm:user1") == 2 async def test_concurrent_access_does_not_raise(self): cache = RouteCache() async def worker(idx: int) -> None: for i in range(20): await cache.set(f"key-{idx}-{i}", i) await cache.get(f"key-{idx}-{i}") await asyncio.gather(*[worker(i) for i in range(5)]) async def test_custom_ttl_is_used(self, monkeypatch): cache = RouteCache(ttl_seconds=10) now = time.time() monkeypatch.setattr(time, "time", lambda: now) await cache.set("key", "value") monkeypatch.setattr(time, "time", lambda: now + 5) assert await cache.get("key") == "value" monkeypatch.setattr(time, "time", lambda: now + 11) assert await cache.get("key") is None class TestDefaultRouteCache: def test_returns_same_instance(self): first = get_default_route_cache() second = get_default_route_cache() assert first is second assert isinstance(first, RouteCache) def test_global_instance_has_expected_defaults(self): cache = get_default_route_cache() assert cache.maxsize == 4000 assert cache.ttl == 60