Expanded integration coverage and enforced test categories.
Audit / Dependencies (push) Successful in 9s
CI / Formatting (push) Successful in 5s
CI / Linting (push) Successful in 4s
CI / Tests (push) Successful in 1m7s
CI / Type Checking (push) Successful in 12s
CI / Spelling (push) Successful in 8s

This commit is contained in:
2026-06-18 16:10:40 -04:00
parent 49062159f9
commit 5bbb0bbde5
22 changed files with 2433 additions and 550 deletions
+77 -66
View File
@@ -14,39 +14,59 @@
"""Unit tests for the IngestCache TTL cache."""
import asyncio
import time
import pytest
from crabstero.cache import CachedMessage, IngestCache
class _FakeClock:
"""Mutable monotonic clock for deterministic cache expiry tests."""
def __init__(self) -> None:
self.now = 1000.0
def __call__(self) -> float:
"""Return the current fake monotonic timestamp."""
return self.now
def advance(self, seconds: float) -> None:
"""Move the fake clock forward by the given number of seconds."""
self.now += seconds
@pytest.fixture
def cached_message() -> CachedMessage:
"""Return a representative cache entry for put/pop behavior tests."""
return CachedMessage(
channel_id=1,
user_id=100,
content="Hello world",
embed_texts=["embed title"],
image_urls=["https://example.com/cat.png"],
)
@pytest.fixture
def fake_clock(monkeypatch: pytest.MonkeyPatch) -> _FakeClock:
"""Patch the cache's clock and return a controllable timestamp source."""
clock = _FakeClock()
monkeypatch.setattr("crabstero.cache.time.monotonic", clock)
return clock
class TestIngestCache:
"""IngestCache put, pop, and expiry behavior."""
def test_put_and_pop(self) -> None:
def test_put_and_pop(self, cached_message: CachedMessage) -> None:
"""A cached message can be retrieved by message ID."""
cache = IngestCache()
entry = CachedMessage(
channel_id=1,
user_id=100,
content="Hello world",
embed_texts=["embed title"],
image_urls=["https://example.com/cat.png"],
)
cache.put(12345, entry)
assert cache.pop(12345) is entry
cache.put(12345, cached_message)
assert cache.pop(12345) is cached_message
def test_pop_removes_entry(self) -> None:
def test_pop_removes_entry(self, cached_message: CachedMessage) -> None:
"""Popping an entry removes it from the cache."""
cache = IngestCache()
entry = CachedMessage(
channel_id=1,
user_id=100,
content="Hello",
embed_texts=[],
image_urls=[],
)
cache.put(12345, entry)
cache.put(12345, cached_message)
cache.pop(12345)
assert cache.pop(12345) is None
@@ -55,64 +75,55 @@ class TestIngestCache:
cache = IngestCache()
assert cache.pop(99999) is None
def test_expired_entry_not_returned(self) -> None:
def test_expired_entry_not_returned(
self,
cached_message: CachedMessage,
fake_clock: _FakeClock,
) -> None:
"""An expired entry is not returned by pop."""
cache = IngestCache(ttl_seconds=0.01)
entry = CachedMessage(
channel_id=1,
user_id=100,
content="Hello",
embed_texts=[],
image_urls=[],
)
cache.put(12345, entry)
time.sleep(0.02)
cache = IngestCache(ttl_seconds=10)
cache.put(12345, cached_message)
fake_clock.advance(11)
assert cache.pop(12345) is None
def test_cleanup_removes_expired(self) -> None:
def test_cleanup_removes_expired(
self,
cached_message: CachedMessage,
fake_clock: _FakeClock,
) -> None:
"""Cleanup evicts all expired entries."""
cache = IngestCache(ttl_seconds=0.01)
entry = CachedMessage(
channel_id=1,
user_id=100,
content="Hello",
embed_texts=[],
image_urls=[],
)
cache.put(1, entry)
cache.put(2, entry)
time.sleep(0.02)
cache = IngestCache(ttl_seconds=10)
cache.put(1, cached_message)
cache.put(2, cached_message)
fake_clock.advance(11)
cache._cleanup()
assert cache.pop(1) is None
assert cache.pop(2) is None
def test_cleanup_keeps_unexpired(self) -> None:
def test_cleanup_keeps_unexpired(self, cached_message: CachedMessage) -> None:
"""Cleanup does not evict entries that are still valid."""
cache = IngestCache(ttl_seconds=300)
entry = CachedMessage(
channel_id=1,
user_id=100,
content="Hello",
embed_texts=[],
image_urls=[],
)
cache.put(1, entry)
cache.put(1, cached_message)
cache._cleanup()
assert cache.pop(1) is entry
assert cache.pop(1) is cached_message
async def test_start_and_stop(self) -> None:
"""The background cleanup task can be started and stopped."""
cache = IngestCache(ttl_seconds=0, cleanup_interval_seconds=0.01)
entry = CachedMessage(
channel_id=1,
user_id=100,
content="Hello",
embed_texts=[],
image_urls=[],
)
cache.put(1, entry)
cache = IngestCache()
cache.start()
await asyncio.sleep(0.05)
assert cache._task is not None
await cache.stop()
# Entry should have been cleaned up by the background task.
assert cache.pop(1) is None
assert cache._task is None
async def test_start_is_idempotent(self) -> None:
"""Starting an already-started cache keeps the existing cleanup task."""
cache = IngestCache()
cache.start()
try:
task = cache._task
cache.start()
assert cache._task is task
finally:
await cache.stop()